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 _certificates; public Server(IPEndPoint endpoint, X509Certificate2 defaultCertificate) { this._endpoint = endpoint; this._certificates = new Dictionary(); 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 cipherSuites = new List(); // 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); } } }