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