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:
parent
c4d8c9db8c
commit
68dda5b2c6
2
.gitignore
vendored
2
.gitignore
vendored
@ -348,3 +348,5 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
.ionide/
|
.ionide/
|
||||||
|
|
||||||
|
CuipodExample/Properties/launchSettings.json
|
||||||
|
|||||||
154
Cuipod/App.cs
154
Cuipod/App.cs
@ -8,6 +8,8 @@ using System.Text;
|
|||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
|
using RequestCallback = System.Action<Cuipod.Request, Cuipod.Response>;
|
||||||
|
|
||||||
namespace Cuipod
|
namespace Cuipod
|
||||||
{
|
{
|
||||||
public class App
|
public class App
|
||||||
@ -15,31 +17,32 @@ namespace Cuipod
|
|||||||
private readonly string _directoryToServe;
|
private readonly string _directoryToServe;
|
||||||
private readonly TcpListener _listener;
|
private readonly TcpListener _listener;
|
||||||
private readonly X509Certificate2 _serverCertificate;
|
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)
|
public App(string directoryToServe, string certificateFile, string privateRSAKeyFilePath)
|
||||||
{
|
{
|
||||||
_directoryToServe = directoryToServe;
|
_directoryToServe = directoryToServe;
|
||||||
IPAddress localAddress = IPAddress.Parse("127.0.0.1");
|
IPAddress localAddress = IPAddress.Parse("127.0.0.1");
|
||||||
_listener = new TcpListener(localAddress, 1965);
|
_listener = new TcpListener(localAddress, 1965);
|
||||||
_requestCallbacks = new Dictionary<string, Action<Response>>();
|
_requestCallbacks = new Dictionary<string, RequestCallback>();
|
||||||
_serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath);
|
_serverCertificate = CertificateUtils.LoadCertificate(certificateFile, privateRSAKeyFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnRequest(string route, Action<Response> callback)
|
public void OnRequest(string route, RequestCallback callback)
|
||||||
{
|
{
|
||||||
_requestCallbacks.Add(route, callback);
|
_requestCallbacks.Add(route, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnBadRequest(Action<Response> callback)
|
public void OnBadRequest(RequestCallback callback)
|
||||||
{
|
{
|
||||||
_onBadRequestCallback = callback;
|
_onBadRequestCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run()
|
public int Run()
|
||||||
{
|
{
|
||||||
|
int status = 0;
|
||||||
Console.WriteLine("Serving capsule on 127.0.0.1:1965");
|
Console.WriteLine("Serving capsule on 127.0.0.1:1965");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -52,64 +55,24 @@ namespace Cuipod
|
|||||||
catch (SocketException e)
|
catch (SocketException e)
|
||||||
{
|
{
|
||||||
Console.WriteLine("SocketException: {0}", e);
|
Console.WriteLine("SocketException: {0}", e);
|
||||||
|
status = 1;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_listener.Stop();
|
_listener.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessRequest(TcpClient client)
|
private void ProcessRequest(TcpClient client)
|
||||||
{
|
{
|
||||||
SslStream sslStream = new SslStream(client.GetStream(), false);
|
SslStream sslStream = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sslStream.AuthenticateAsServer(_serverCertificate, false, SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12, false);
|
sslStream = new SslStream(client.GetStream(), false);
|
||||||
|
Response response = ProcessRequest(sslStream);
|
||||||
// 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.Write(response.Encode());
|
sslStream.Write(response.Encode());
|
||||||
}
|
}
|
||||||
catch (AuthenticationException e)
|
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)
|
private string ReadRequest(SslStream sslStream)
|
||||||
{
|
{
|
||||||
byte[] buffer = new byte[2048];
|
byte[] buffer = new byte[2048];
|
||||||
Decoder decoder = Encoding.UTF8.GetDecoder();
|
Decoder decoder = Encoding.UTF8.GetDecoder();
|
||||||
|
|
||||||
StringBuilder requestData = new StringBuilder();
|
StringBuilder requestData = new StringBuilder();
|
||||||
string line;
|
int bytes = sslStream.Read(buffer, 0, buffer.Length);
|
||||||
do
|
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);
|
return line.TrimEnd('\r', '\n');
|
||||||
char[] chars = new char[decoder.GetCharCount(buffer, 0, bytes)];
|
}
|
||||||
decoder.GetChars(buffer, 0, bytes, chars, 0);
|
return null;
|
||||||
line = new string(chars);
|
|
||||||
requestData.Append(line);
|
|
||||||
} while (!line.EndsWith("\r\n"));
|
|
||||||
|
|
||||||
|
|
||||||
return requestData.ToString().TrimEnd('\r', '\n'); ;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
Cuipod/Request.cs
Normal file
29
Cuipod/Request.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ namespace Cuipod
|
|||||||
{
|
{
|
||||||
public class Response
|
public class Response
|
||||||
{
|
{
|
||||||
public StatusCode Status { get; internal set; }
|
public StatusCode Status { get; set; }
|
||||||
|
|
||||||
private string _directoryToServe;
|
private string _directoryToServe;
|
||||||
private string _requestBody = "";
|
private string _requestBody = "";
|
||||||
@ -27,14 +27,31 @@ namespace Cuipod
|
|||||||
_requestBody += File.ReadAllText(_directoryToServe + relativePathToFile, Encoding.UTF8) + "\r\n";
|
_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)
|
internal static string WriteHeader(StatusCode statusCode)
|
||||||
{
|
{
|
||||||
return ((int)statusCode).ToString() + " text/gemini\r\n";
|
return ((int)statusCode).ToString() + " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal byte[] Encode()
|
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);
|
return Encoding.UTF8.GetBytes(wholeResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,10 @@
|
|||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.CommandLineUtils" Version="1.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Cuipod\Cuipod.csproj" />
|
<ProjectReference Include="..\Cuipod\Cuipod.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -1,39 +1,102 @@
|
|||||||
using Cuipod;
|
using Cuipod;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace CuipodExample
|
namespace CuipodExample
|
||||||
{
|
{
|
||||||
class Server
|
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(
|
App app = new App(
|
||||||
"<directory_to_serve>/", // directory to serve
|
directoryToServe,
|
||||||
"<dir_with_cert>/certificate.crt", // path to certificate
|
certificateFile,
|
||||||
"<dir_with_cert>/privatekey.key" // path to private Pkcs8 RSA key
|
privateRSAKeyFilePath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Serve files
|
// Serve files
|
||||||
app.OnRequest("/", response => {
|
app.OnRequest("/", (request, response) => {
|
||||||
response.RenderFileContent("index.gmi");
|
response.RenderFileContent("index.gmi");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.OnRequest("/about/", response => {
|
// Input example
|
||||||
response.RenderFileContent("about_me.gmi");
|
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
|
// Or dynamically render content
|
||||||
app.OnRequest("/dynamic/content/", response => {
|
app.OnRequest("/dynamic/content", (request, response) => {
|
||||||
response.RenderPlainTextLine("# woah much content!");
|
response.RenderPlainTextLine("# woah much content!");
|
||||||
response.RenderPlainTextLine("More utilities to render content will come soon!");
|
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
|
// 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
|
// 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 :(");
|
response.RenderPlainTextLine("# Ohh No!!! Request is bad :(");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.Run();
|
return app.Run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user