diff --git a/.gitignore b/.gitignore index dfcfd56..7cd11e6 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,5 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +CuipodExample/Properties/launchSettings.json diff --git a/Cuipod/App.cs b/Cuipod/App.cs index e253e70..52b7443 100644 --- a/Cuipod/App.cs +++ b/Cuipod/App.cs @@ -8,6 +8,8 @@ using System.Text; using System.Security.Authentication; using System.IO; +using RequestCallback = System.Action; + namespace Cuipod { public class App @@ -15,31 +17,32 @@ namespace Cuipod private readonly string _directoryToServe; private readonly TcpListener _listener; private readonly X509Certificate2 _serverCertificate; - private readonly Dictionary> _requestCallbacks; + private readonly Dictionary _requestCallbacks; - private Action _onBadRequestCallback; + 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>(); + _requestCallbacks = new Dictionary(); _serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath); } - public void OnRequest(string route, Action callback) + public void OnRequest(string route, RequestCallback callback) { _requestCallbacks.Add(route, callback); } - public void OnBadRequest(Action callback) + public void OnBadRequest(RequestCallback callback) { _onBadRequestCallback = callback; } - public void Run() + public int Run() { + int status = 0; Console.WriteLine("Serving capsule on 127.0.0.1:1965"); try { @@ -52,64 +55,24 @@ namespace Cuipod catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); + status = 1; } finally { _listener.Stop(); } + + return status; } private void ProcessRequest(TcpClient client) { - SslStream sslStream = new SslStream(client.GetStream(), false); + SslStream sslStream = null; + try { - sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false); - - // Read a message from the client. - string rawURI = ReadRequest(sslStream); - - Response response = new Response(_directoryToServe); - - int protocolDelimiter = rawURI.IndexOf("://"); - if (protocolDelimiter == -1) - { - response.Status = StatusCode.BadRequest; - } - - string protocol = rawURI.Substring(0, protocolDelimiter); - if (protocol != "gemini") - { - response.Status = StatusCode.BadRequest; - } - - string url = rawURI.Substring(protocolDelimiter + 3); - int domainNameDelimiter = url.IndexOf("/"); - if (domainNameDelimiter == -1) - { - response.Status = StatusCode.BadRequest; - } - string domainName = url.Substring(0, domainNameDelimiter); - // TODO: validate domain name from cert? - - string route = url.Substring(domainNameDelimiter); - - if (response.Status == StatusCode.Success) - { - Action callback = null; - _requestCallbacks.TryGetValue(route, out callback); - if (callback != null) - { - callback(response); - } else if (_onBadRequestCallback != null) - { - _onBadRequestCallback(response); - } else - { - response.Status = StatusCode.BadRequest; - } - } - + sslStream = new SslStream(client.GetStream(), false); + Response response = ProcessRequest(sslStream); sslStream.Write(response.Encode()); } catch (AuthenticationException e) @@ -132,24 +95,87 @@ namespace Cuipod } } + 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(); - string line; - do + 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")) { - int bytes = sslStream.Read(buffer, 0, buffer.Length); - char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)]; - decoder.GetChars(buffer, 0, bytes, chars, 0); - line = new string(chars); - requestData.Append(line); - } while (!line.EndsWith("\r\n")); - - - return requestData.ToString().TrimEnd('\r', '\n'); ; + return line.TrimEnd('\r', '\n'); + } + return null; } } } diff --git a/Cuipod/Request.cs b/Cuipod/Request.cs new file mode 100644 index 0000000..9e080dd --- /dev/null +++ b/Cuipod/Request.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Cuipod +{ + public class Request + { + public string BaseURL { get; internal set; } + public string Route { get; internal set; } + public string Parameters { get; internal set; } + + public Request(string baseURL, string route) + { + BaseURL = baseURL; + + int parametersDelimiter = route.IndexOf("?"); + if (parametersDelimiter != -1) + { + Parameters = route.Substring(parametersDelimiter + 1); + Route = route.Substring(0, parametersDelimiter); + } + else + { + Route = route; + } + } + } +} diff --git a/Cuipod/Response.cs b/Cuipod/Response.cs index 5e1f739..bf6c85e 100644 --- a/Cuipod/Response.cs +++ b/Cuipod/Response.cs @@ -6,7 +6,7 @@ namespace Cuipod { public class Response { - public StatusCode Status { get; internal set; } + public StatusCode Status { get; set; } private string _directoryToServe; private string _requestBody = ""; @@ -27,14 +27,31 @@ namespace Cuipod _requestBody += File.ReadAllText(_directoryToServe + relativePathToFile, Encoding.UTF8) + "\r\n"; } + public void SetRedirectURL(string route) + { + _requestBody = route + "\r\n"; + } + + public void SetInputHint(string hint) + { + _requestBody = hint + "\r\n"; + } + internal static string WriteHeader(StatusCode statusCode) { - return ((int)statusCode).ToString() + " text/gemini\r\n"; + return ((int)statusCode).ToString() + " "; } internal byte[] Encode() { - string wholeResponse = WriteHeader(Status) + _requestBody; + string wholeResponse = ((int)Status).ToString() + " "; + if (Status == StatusCode.Success) + { + wholeResponse += "text/gemini\r\n"; + } + + wholeResponse += _requestBody; + Console.WriteLine(wholeResponse); return Encoding.UTF8.GetBytes(wholeResponse); } } diff --git a/CuipodExample/CuipodExample.csproj b/CuipodExample/CuipodExample.csproj index 613bc36..8cceee7 100644 --- a/CuipodExample/CuipodExample.csproj +++ b/CuipodExample/CuipodExample.csproj @@ -5,6 +5,10 @@ netcoreapp3.0 + + + + diff --git a/CuipodExample/Server.cs b/CuipodExample/Server.cs index 028110a..2b05256 100644 --- a/CuipodExample/Server.cs +++ b/CuipodExample/Server.cs @@ -1,39 +1,102 @@ using Cuipod; +using Microsoft.Extensions.CommandLineUtils; +using System; namespace CuipodExample { class Server { - static void Main(string[] args) + static int Main(string[] args) + { + CommandLineApplication commandLineApplication = new CommandLineApplication(); + commandLineApplication.HelpOption("-h | --help"); + CommandArgument directoryToServe = commandLineApplication.Argument( + "directory", + "Directory to server (required)" + ); + CommandArgument certificateFile = commandLineApplication.Argument( + "certificate", + "Path to certificate (required)" + ); + CommandArgument privateRSAKeyFilePath = commandLineApplication.Argument( + "key", + "Path to private Pkcs8 RSA key (required)" + ); + commandLineApplication.OnExecute(() => + { + if (directoryToServe.Value == null || certificateFile.Value == null || privateRSAKeyFilePath.Value == null) + { + commandLineApplication.ShowHelp(); + return 1; + } + return AppMain(directoryToServe.Value, certificateFile.Value, privateRSAKeyFilePath.Value); + }); + + try + { + return commandLineApplication.Execute(args); + } catch (Exception e) + { + Console.WriteLine("Error: {0}", e.Message); + return 1; + } + } + + private static int AppMain(string directoryToServe, string certificateFile, string privateRSAKeyFilePath) { App app = new App( - "/", // directory to serve - "/certificate.crt", // path to certificate - "/privatekey.key" // path to private Pkcs8 RSA key + directoryToServe, + certificateFile, + privateRSAKeyFilePath ); // Serve files - app.OnRequest("/", response => { + app.OnRequest("/", (request, response) => { response.RenderFileContent("index.gmi"); }); - app.OnRequest("/about/", response => { - response.RenderFileContent("about_me.gmi"); + // Input example + app.OnRequest("/input", (request, response) => { + if (request.Parameters == null) + { + response.SetInputHint("Please enter something: "); + response.Status = StatusCode.Input; + } + else + { + // redirect to show/ route with input parameters + response.SetRedirectURL(request.BaseURL + "/show?" + request.Parameters); + response.Status = StatusCode.RedirectTemp; + } + }); + + app.OnRequest("/show", (request, response) => { + if (request.Parameters == null) + { + // redirect to input + response.SetRedirectURL(request.BaseURL + "/input"); + response.Status = StatusCode.RedirectTemp; + } + else + { + // show what has been entered + response.RenderPlainTextLine("# " + request.Parameters); + } }); // Or dynamically render content - app.OnRequest("/dynamic/content/", response => { + app.OnRequest("/dynamic/content", (request, response) => { response.RenderPlainTextLine("# woah much content!"); response.RenderPlainTextLine("More utilities to render content will come soon!"); }); // Optional but nice. In case it is specified and client will do a bad route // request we will respond with Success status and render result from this lambda - app.OnBadRequest(response => { + app.OnBadRequest((request, response) => { response.RenderPlainTextLine("# Ohh No!!! Request is bad :("); }); - app.Run(); + return app.Run(); } } }