Browse Source

Gemini library

master
Adam Pippin 2 years ago
parent
commit
5a4b3a4b4e
  1. 16
      pyxis.gemini/Protocol/Exception/ProtocolViolationException.cs
  2. 86
      pyxis.gemini/Protocol/Request.cs
  3. 200
      pyxis.gemini/Protocol/Response.cs
  4. 68
      pyxis.gemini/Protocol/Server.cs
  5. 214
      pyxis.gemini/Server/Server.cs
  6. 7
      pyxis.gemini/pyxis.gemini.csproj

16
pyxis.gemini/Protocol/Exception/ProtocolViolationException.cs

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace pyxis.gemini.Protocol.Exception
{
public class ProtocolViolationException : System.Exception
{
public ProtocolViolationException(string message)
: base(message)
{
}
}
}

86
pyxis.gemini/Protocol/Request.cs

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace pyxis.gemini.Protocol
{
public class Request
{
// 1024 meta
// 2 status code
// 1 separator space
// 2 \r\n
private const int MAX_REQUEST_LENGTH = 1024 + 2 + 1 + 2;
public string Url { private set; get; }
// TODO: Provide properties for fetching specific parts of the URL
public void setUrl(string url)
{
this.Url = url;
}
public static int Parse(byte[] data, int length, out Request request)
{
int i;
bool found_cr = false, found_lf = false;
request = null;
// Minimum request length would be 3 -- one byte url and \r\n. If it's less than that, just
// bail now. This guards against any potential bug later when we start trying to trim the
// crlf, etc.
if (length < 3)
{
return 0;
}
for (i = 0; i < length; i++)
{
if (i >= MAX_REQUEST_LENGTH)
{
throw new Exception.ProtocolViolationException(String.Format("Request length exceeded maximum: %d", MAX_REQUEST_LENGTH));
}
if (data[i] == '\r')
{
found_cr = true;
}
else if (found_cr && data[i] == '\n')
{
found_lf = true;
break;
}
else
{
found_cr = false;
found_lf = false;
}
}
if (!found_cr || !found_lf)
{
return 0;
}
request = new Request();
request.setUrl(Encoding.UTF8.GetString(data, 0, i - 2 /* trim \r\n */));
return i + 1;
}
public byte[] Serialize()
{
System.IO.MemoryStream ms = new System.IO.MemoryStream();
System.IO.StreamWriter sw = new System.IO.StreamWriter(ms);
// TODO: Validate URL
sw.Write(this.Url);
sw.Write("\r\n");
sw.Close();
return ms.ToArray();
}
}
}

200
pyxis.gemini/Protocol/Response.cs

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace pyxis.gemini.Protocol
{
public class Response
{
private const int MAX_HEADER_LENGTH = 1029;
public int Status { private set; get; }
public string Meta { private set; get; }
public byte[] Body { private set; get; }
public void setStatus(int status)
{
this.Status = status;
}
public void setMeta(string meta)
{
this.Meta = meta;
}
public void setBody(byte[] body)
{
this.Body = body;
}
public static int Parse(byte[] data, int length, out Response response)
{
int i;
bool found_cr = false, found_lf = false;
response = null;
// Minimum response length would be 6 bytes (status=2, space=1, meta=1, crlf=2)
// Anything less just bail early.
if (length < 6)
{
return 0;
}
for (i = 0; i < length; i++)
{
// If we haven't found the header terminating crlf by this point, then it can't be a valid response
if (i >= MAX_HEADER_LENGTH)
{
throw new Exception.ProtocolViolationException(String.Format("Request header length exceeded maximum: %d", MAX_HEADER_LENGTH));
}
if (data[i] == '\r')
{
found_cr = true;
}
if (found_cr && data[i] == '\n')
{
found_lf = true;
break;
}
else
{
found_cr = false;
}
}
if (!(found_cr && found_lf))
{
return 0;
}
int code;
string meta;
(code, meta) = Response.ParseHeader(data, 0, i);
response = new Response();
response.setStatus(code);
response.setMeta(meta);
return i + 1;
}
private static (int, string) ParseHeader(byte[] buffer, int start, int count)
{
int state = 0;
// 0 - reading code
// 1 - reading separator
// 2 - reading meta
// 3 - reading cr
// 4 - reading lf
int field_start = start;
string code = "";
string meta = "";
for (int i = start; i <= start + count; i++)
{
switch (state)
{
case 0: // reading code
if (buffer[i] == ' ')
{
if (field_start == i)
{
throw new Exception.ProtocolViolationException("Found separator in header before reading status code.");
}
code = Encoding.UTF8.GetString(buffer, field_start, i - field_start);
field_start = i;
state = 1;
i--;
}
else
{
if (i - field_start > 2)
{
throw new Exception.ProtocolViolationException("Status code too long");
}
}
break;
case 1: // reading separator
if (buffer[i] != ' ')
{
field_start = i;
state = 2;
i--;
}
else
{
if (i - field_start > 0)
{
throw new Exception.ProtocolViolationException("Found multiple spaces in header separator.");
}
}
break;
case 2: // reading meta
if (buffer[i] == '\r')
{
meta = Encoding.UTF8.GetString(buffer, field_start, i - field_start);
field_start = i;
state = 3;
i--;
}
else
{
if (i - field_start > 1024)
{
throw new Exception.ProtocolViolationException("Meta field too long.");
}
}
break;
case 3: // reading cr
if (buffer[i] == '\r')
{
state = 4;
}
else
{
throw new Exception.ProtocolViolationException("Could not find carriage return to terminate header.");
}
break;
case 4: // reading lf
if (buffer[i] == '\n')
{
state = 5;
}
else
{
throw new Exception.ProtocolViolationException("Could not find line feed to terminate header.");
}
break;
case 5: // shouldn't get here, read past \r\n
throw new Exception.ProtocolViolationException("Header contained data past terminated crlf.");
break;
}
}
return (int.Parse(code), meta);
}
public byte[] Serialize()
{
// TODO: Validate Meta is <=1024 bytes
// TODO: Validate Meta is valid for specific response codes?
// TODO: Validate no response body unless Status >=20 && <=29
List<byte> data = new List<byte>();
data.AddRange(Encoding.UTF8.GetBytes(this.Status.ToString() + ' ' + this.Meta + "\r\n"));
if (this.Body != null)
{
data.AddRange(this.Body);
}
return data.ToArray();
}
}
}

68
pyxis.gemini/Protocol/Server.cs

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace pyxis.gemini.Protocol
{
public class Server
{
private Request _request;
private const int CONNECTION_BUFFER_SIZE = 2048;
private byte[] _connection_buffer;
private int _connection_buffer_length;
public delegate void RequestEventDelegate(Server sender, Request request);
public event RequestEventDelegate OnRequest;
public Server()
{
this._connection_buffer = new byte[CONNECTION_BUFFER_SIZE];
this._connection_buffer_length = 0;
}
public void Receive(byte[] data, int length)
{
// Copy read bytes into connection buffer
for (int i = 0; i < length; i++)
{
if (i >= CONNECTION_BUFFER_SIZE)
{
throw new System.Exception("Connection buffer overflow");
}
this._connection_buffer[i + this._connection_buffer_length] = data[i];
}
this._connection_buffer_length += length;
Request r;
int consumed = Request.Parse(this._connection_buffer, this._connection_buffer_length, out r);
if (consumed == 0)
{
if (length == 0)
{
throw new System.Exception("Stream closed without receiving request");
}
else
{
return;
}
}
if (consumed != this._connection_buffer_length)
{
throw new Exception.ProtocolViolationException("Received trailing bytes after request");
}
if (this.OnRequest != null)
{
this.OnRequest(this, r);
}
}
}
}

214
pyxis.gemini/Server/Server.cs

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
namespace pyxis.gemini.Server
{
public class Server
{
const int READ_BUFFER_SIZE = 2048;
struct Transaction
{
public TcpClient client;
public SslStream stream;
public State state;
public Protocol.Request request;
public Protocol.Response response;
public Protocol.Server server;
public byte[] buffer;
public enum State
{
INITIALIZE,
GET_REQUEST,
PROCESS_REQUEST,
SEND_RESPONSE,
CLOSE,
DISPOSE
}
}
private IPEndPoint _endpoint;
private TcpListener _listener;
public delegate Protocol.Response ProcessRequestDelegate(Server sender, Protocol.Request request);
public ProcessRequestDelegate OnProcessRequest;
Dictionary<string, X509Certificate2> _certificates;
public Server(IPEndPoint endpoint, X509Certificate2 defaultCertificate)
{
this._endpoint = endpoint;
this._certificates = new Dictionary<string, X509Certificate2>();
this._certificates.Add("default", defaultCertificate);
}
public void AddCertificate(string hostname, X509Certificate2 cert)
{
this._certificates.Add(hostname, cert);
}
public void Start()
{
this._listener = new TcpListener(this._endpoint);
this._listener.Start();
this._listener.BeginAcceptTcpClient(this.ListenerAccept, null);
}
public void Stop()
{
this._listener.Stop();
}
private void ListenerAccept(IAsyncResult result)
{
TcpClient tcp = this._listener.EndAcceptTcpClient(result);
if (tcp == null)
{
return;
}
Transaction t = new Transaction();
t.client = tcp;
t.stream = new SslStream(tcp.GetStream());
t.state = Transaction.State.INITIALIZE;
t.request = null;
t.response = null;
t.server = new Protocol.Server();
t.server.OnRequest += (Protocol.Server server, Protocol.Request request) =>
{
this.ServerEmitRequest(t, request);
};
t.buffer = new byte[READ_BUFFER_SIZE];
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
_listener.BeginAcceptTcpClient(this.ListenerAccept, null);
}
private void TickTransaction(object state)
{
Transaction t = (Transaction)state;
switch (t.state)
{
case Transaction.State.INITIALIZE:
// List<TlsCipherSuite> cipherSuites = new List<TlsCipherSuite>();
// TODO: Go through and list all the valid ciphersuites
// https://docs.microsoft.com/en-us/windows/win32/secauthn/cipher-suites-in-schannel -- how to decode these
// https://gemini.circumlunar.space/docs/best-practices.gmi -- bottom includes info on best practices for encryption
SslServerAuthenticationOptions sslauth = new SslServerAuthenticationOptions()
{
//CipherSuitesPolicy = new CipherSuitesPolicy(cipherSuites),
//ClientCertificateRequired = true, // _request_ a client cert if available, can use this in the future for client auth, docblock says it won't fail if one isn't provided
//EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
ServerCertificateSelectionCallback = this.SelectServerCertificate
};
try
{
t.stream.AuthenticateAsServer(sslauth);
}
catch
{
t.state = Transaction.State.DISPOSE;
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
return;
}
t.state = Transaction.State.GET_REQUEST;
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
break;
case Transaction.State.GET_REQUEST:
t.stream.BeginRead(t.buffer, 0, READ_BUFFER_SIZE, this.StreamRead, t);
break;
case Transaction.State.PROCESS_REQUEST:
if (this.OnProcessRequest == null)
{
// ?!
throw new Exception("Cannot process request -- no listener registered.");
}
try
{
t.response = this.OnProcessRequest(this, t.request);
}
catch (Exception ex)
{
t.response = new Protocol.Response();
t.response.setStatus(40);
t.response.setMeta("Internal Server Error");
t.response.setBody(Encoding.UTF8.GetBytes("Internal Server Error: " + ex.Message));
}
t.state = Transaction.State.SEND_RESPONSE;
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
break;
case Transaction.State.SEND_RESPONSE:
byte[] serialized = t.response.Serialize();
string tmp = Encoding.UTF8.GetString(serialized);
t.stream.Write(t.response.Serialize());
t.state = Transaction.State.CLOSE;
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
break;
case Transaction.State.CLOSE:
t.stream.Close();
t.state = Transaction.State.DISPOSE;
ThreadPool.QueueUserWorkItem(this.TickTransaction, t);
break;
case Transaction.State.DISPOSE:
t.client.Close();
// Any other cleanup?
break;
}
}
private X509Certificate SelectServerCertificate(object sender, string? hostname)
{
X509Certificate2 cert = null;
if (this._certificates.ContainsKey(hostname))
{
cert = this._certificates[hostname];
}
else
{
cert = this._certificates["default"];
}
return cert;
}
private void StreamRead(IAsyncResult result)
{
Transaction t = (Transaction)result.AsyncState;
int bytesRead;
try
{
bytesRead = t.stream.EndRead(result);
}
catch (ObjectDisposedException ex)
{
return;
}
t.server.Receive(t.buffer, bytesRead);
if (t.request == null)
{
t.stream.BeginRead(t.buffer, 0, READ_BUFFER_SIZE, this.StreamRead, t);
}
}
private void ServerEmitRequest(Transaction transaction, Protocol.Request request)
{
transaction.request = request;
transaction.state = Transaction.State.PROCESS_REQUEST;
ThreadPool.QueueUserWorkItem(this.TickTransaction, transaction);
}
}
}

7
pyxis.gemini/pyxis.gemini.csproj

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Loading…
Cancel
Save