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; using RequestCallback = System.Action; namespace Cuipod { public class App { private readonly string _directoryToServe; private readonly TcpListener _listener; private readonly X509Certificate2 _serverCertificate; private readonly Dictionary _requestCallbacks; private RequestCallback _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(); _serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath); } public void OnRequest(string route, RequestCallback callback) { _requestCallbacks.Add(route, callback); } public void OnBadRequest(RequestCallback callback) { _onBadRequestCallback = callback; } public int Run() { int status = 0; 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); status = 1; } finally { _listener.Stop(); } return status; } private void ProcessRequest(TcpClient client) { SslStream sslStream = null; try { sslStream = new SslStream(client.GetStream(), false); Response response = ProcessRequest(sslStream); 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 Response ProcessRequest(SslStream sslStream) { sslStream.ReadTimeout = 5000; sslStream.WriteTimeout = 5000; sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false); // Read a message from the client. string rawURL = ReadRequest(sslStream); Response response = new Response(_directoryToServe); if (rawURL == null) { response.Status = StatusCode.BadRequest; return response; } Console.WriteLine(rawURL); int protocolDelimiter = rawURL.IndexOf("://"); if (protocolDelimiter == -1) { response.Status = StatusCode.BadRequest; return response; } string protocol = rawURL.Substring(0, protocolDelimiter); if (protocol != "gemini") { response.Status = StatusCode.BadRequest; return response; } string url = rawURL.Substring(protocolDelimiter + 3); int domainNameDelimiter = url.IndexOf("/"); if (domainNameDelimiter == -1) { response.Status = StatusCode.BadRequest; return response; } string domainName = url.Substring(0, domainNameDelimiter); // TODO: validate domain name from cert? Request request = new Request("gemini://" + domainName , url.Substring(domainNameDelimiter)); if (response.Status == StatusCode.Success) { RequestCallback callback; _requestCallbacks.TryGetValue(request.Route, out callback); if (callback != null) { callback(request, response); } else if (_onBadRequestCallback != null) { _onBadRequestCallback(request, response); } else { response.Status = StatusCode.BadRequest; return response; } } return response; } private string ReadRequest(SslStream sslStream) { byte[] buffer = new byte[2048]; Decoder decoder = Encoding.UTF8.GetDecoder(); StringBuilder requestData = new StringBuilder(); int bytes = sslStream.Read(buffer, 0, buffer.Length); char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; decoder.GetChars(buffer, 0, bytes, chars, 0); string line = new string(chars); if (line.EndsWith("\r\n")) { return line.TrimEnd('\r', '\n'); } return null; } } }