Added command line parsing for example, fixed non text responses, added request object so you can access parameters and base url

Also, various fixes and improvements
This commit is contained in:
Egidijus Lileika 2021-02-08 22:18:14 +02:00
parent c4d8c9db8c
commit 68dda5b2c6
6 changed files with 218 additions and 77 deletions

2
.gitignore vendored
View File

@ -348,3 +348,5 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
CuipodExample/Properties/launchSettings.json

View File

@ -8,6 +8,8 @@ using System.Text;
using System.Security.Authentication;
using System.IO;
using RequestCallback = System.Action<Cuipod.Request, Cuipod.Response>;
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<string, Action<Response>> _requestCallbacks;
private readonly Dictionary<string, RequestCallback> _requestCallbacks;
private Action<Response> _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<string, Action<Response>>();
_requestCallbacks = new Dictionary<string, RequestCallback>();
_serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath);
}
public void OnRequest(string route, Action<Response> callback)
public void OnRequest(string route, RequestCallback callback)
{
_requestCallbacks.Add(route, callback);
}
public void OnBadRequest(Action<Response> 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<Response> 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);
line = new string(chars);
requestData.Append(line);
} while (!line.EndsWith("\r\n"));
return requestData.ToString().TrimEnd('\r', '\n'); ;
string line = new string(chars);
if (line.EndsWith("\r\n"))
{
return line.TrimEnd('\r', '\n');
}
return null;
}
}
}

29
Cuipod/Request.cs Normal file
View File

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

View File

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

View File

@ -5,6 +5,10 @@
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Cuipod\Cuipod.csproj" />
</ItemGroup>

View File

@ -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>/", // directory to serve
"<dir_with_cert>/certificate.crt", // path to certificate
"<dir_with_cert>/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();
}
}
}