Gemini protocol server in C#
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
6.0 KiB

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);
}
}
}