Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Data/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public class Configuration
{
public bool SlowMode { get; set; }
public int SlowModeMaxMilliSeconds { get; set; }
public ushort SpartanPort { get; set; } = 300;
public ushort GeminiPort { get; set; } = 1965;
public ushort SpartanPort { get; set; } = Defaults.SpartanPort;
public ushort GeminiPort { get; set; } = Defaults.GeminiPort;
public Dictionary<string, Capsule> Capsules { get; set; } = new();

public static Configuration Load()
Expand Down Expand Up @@ -80,21 +80,21 @@ public static object CreateSampleConfig()
{
var Config = new Configuration()
{
GeminiPort = 1965,
SpartanPort = (ushort)(Debugger.IsAttached ? 3000 : 300),
GeminiPort = Defaults.GeminiPort,
SpartanPort = (ushort)(Debugger.IsAttached ? 3000 : Defaults.SpartanPort),
Capsules = new()
{
[Environment.MachineName] = new Capsule()
{
FQDN = Environment.MachineName,
AbsoluteRootPath = $"/srv/gemini/{Environment.MachineName}/",
MaxUploadSize = 1024 * 1024 * 4,
Index = "index.gmi",
MaxUploadSize = Defaults.MaxUploadSize,
Index = Defaults.IndexFile,
Locations = new()
{
new Location()
{
Index = "index.gmi",
Index = Defaults.IndexFile,
AbsoluteRootPath = $"/srv/gemini/{Environment.MachineName}/",
},
new Location()
Expand Down
11 changes: 11 additions & 0 deletions Data/Defaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace atlas.Data
{
public static class Defaults
{
public const ushort GeminiPort = 1965;
public const ushort SpartanPort = 300;
public const string IndexFile = "index.gmi";
public const string DefaultMimeType = "text/gemini";
public const int MaxUploadSize = 4 * 1024 * 1024; // 4MB
}
}
25 changes: 23 additions & 2 deletions Data/Location.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace atlas.Data
{
public class Location
{
public string Index { get; set; } = "index.gmi";
public string Index { get; set; } = Defaults.IndexFile;
public bool CGI { get; set; }
public bool DirectoryListing { get; set; }
public string AbsoluteRootPath { get; set; } = string.Empty;
public bool AllowFileUploads { get; set; }
public int MaxUploadSize { get; set; }
public bool RequireClientCert { get; set; }
public string DefaultMimeType { get; set; } = "text/gemini";
public string DefaultMimeType { get; set; } = Defaults.DefaultMimeType;
public Dictionary<string, MimeConfig> AllowedMimeTypes { get; set; } = new();

public bool IsAllowedMimeType(string mimeType)
{
var mimeTypeLower = mimeType.ToLowerInvariant();
return AllowedMimeTypes.Any(allowed =>
{
var allowedLower = allowed.Key.ToLowerInvariant();
if (allowedLower == mimeTypeLower)
return true;

var parts = allowedLower.Split('/');
if (parts.Length == 2 && parts[1] == "*")
{
var mimeTypeParts = mimeTypeLower.Split('/');
return mimeTypeParts.Length == 2 && mimeTypeParts[0] == parts[0];
}
return false;
});
}
}
}
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# herstfortress/atlas:latest

FROM mcr.microsoft.com/dotnet/nightly/sdk:8.0-alpine as build
FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0-alpine as build
WORKDIR /app
COPY . .
RUN dotnet restore
RUN dotnet publish -o /app/published-app --configuration Release


FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0-alpine as runtime
FROM mcr.microsoft.com/dotnet/nightly/runtime:10.0-alpine as runtime
WORKDIR /app
VOLUME ["/etc/atlas"]

Expand Down
9 changes: 8 additions & 1 deletion Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ public static void Log(Context ctx, string text)
if (string.IsNullOrWhiteSpace(ctx.Request))
Console.WriteLine($"[{ctx.Capsule?.FQDN}] [{(ctx.IsGemini ? "Gemini" : "Spartan")}] {ctx.ClientIP} -> {text}");
else
Console.WriteLine($"[{ctx.Capsule?.FQDN}] [{(ctx.IsGemini ? "Gemini" : "Spartan")}] {ctx.ClientIP} -> {ctx.Request.Trim()} -> {text}");
Console.WriteLine($"[{ctx.Capsule?.FQDN}] [{(ctx.IsGemini ? "Gemini" : "Spartan")}] {ctx.ClientIP} -> {ctx.Request} -> {text}");
}

public static void LogError(Context ctx, Exception e, string action = "")
{
var prefix = string.IsNullOrEmpty(action) ? "Error" : $"Error during {action}";
Log(ctx, $"{prefix}: {e.Message}");
Console.WriteLine(e.StackTrace);
}
}
}
110 changes: 60 additions & 50 deletions Servers/CGI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,63 +10,38 @@ namespace atlas.Servers
{
public static class CGI
{
public static IEnumerable<string> ExecuteScript(Context ctx, string scriptName, string path, string PATHINFO)
public static IEnumerable<string> ExecuteScript(Context ctx, string scriptName, string path, string pathInfo)
{
var info = new ProcessStartInfo();
var info = new ProcessStartInfo
{
WorkingDirectory = path,
UseShellExecute = false,
FileName = "sh",
Arguments = $"-c {Path.Combine(path, Path.GetFileName(scriptName)).Replace("//", "/")}",
RedirectStandardOutput = true,
RedirectStandardError = true
};

info.EnvironmentVariables.Clear();
info.EnvironmentVariables.Add("DOTNET_CLI_HOME", "/home/trbl/.dotnet");
info.EnvironmentVariables.Add("GATEWAY_INTERFACE", "CGI/1.1");
info.EnvironmentVariables.Add("SERVER_PROTOCOL", $"{(ctx.IsGemini ? "GEMINI" : "SPARTAN")}");
info.EnvironmentVariables.Add("SERVER_PORT", $"{(ctx.IsGemini ? Program.Cfg.GeminiPort : Program.Cfg.SpartanPort)}");
info.EnvironmentVariables.Add("SERVER_SOFTWARE", $"atlas/{Program.Version}");
info.EnvironmentVariables.Add("URL", ctx.Request.Replace("\r\n", ""));
info.EnvironmentVariables.Add("SCRIPT_NAME", scriptName);
info.EnvironmentVariables.Add("PATH_INFO", PATHINFO);
info.EnvironmentVariables.Add("QUERY_STRING", ctx.Uri.Query);
info.EnvironmentVariables.Add("SERVER_NAME", ctx.Capsule.FQDN);
info.EnvironmentVariables.Add("REMOTE_HOST", ctx.ClientIP);
info.EnvironmentVariables.Add("REMOTE_ADDR", ctx.ClientIP);
AddBaseEnvironmentVariables(info, ctx, scriptName, pathInfo);

if (ctx is GeminiCtx gCtx)
{
info.EnvironmentVariables.Add("TLS_VERSION", "1.3");
if (gCtx.Certificate != null)
{
info.EnvironmentVariables.Add("REMOTE_USER", $"{gCtx.CertSubject}");
info.EnvironmentVariables.Add("TLS_CLIENT_VALID", $"{gCtx.IsValidCert}");
info.EnvironmentVariables.Add("TLS_CLIENT_TRUSTED", $"{gCtx.IsTrustedCert}");
info.EnvironmentVariables.Add("TLS_CLIENT_SUBJECT", $"{gCtx.CertSubject}");
info.EnvironmentVariables.Add("TLS_CLIENT_HASH", gCtx.CertThumbprint);
info.EnvironmentVariables.Add("TLS_CLIENT_NOT_BEFORE", gCtx.Certificate.NotBefore.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_NOT_AFTER", gCtx.Certificate.NotAfter.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_SERIAL_NUMBER", gCtx.Certificate.GetSerialNumberString());
info.EnvironmentVariables.Add("AUTH_TYPE", "certificate");
}
else
info.EnvironmentVariables.Add("AUTH_TYPE", "none");
}
AddGeminiEnvironmentVariables(info, gCtx);
else
info.EnvironmentVariables.Add("AUTH_TYPE", "none");

info.WorkingDirectory = path;
info.UseShellExecute = false;
info.FileName = $"sh";
info.Arguments = $"-c {Path.Combine(path, Path.GetFileName(scriptName)).Replace("//","/")}";
info.RedirectStandardOutput = true;
info.RedirectStandardError = true;

return ExecuteScript(info, ctx);
return RunScript(info, ctx);
}

public static IEnumerable<string> ExecuteScript(ProcessStartInfo info, Context ctx)
private static IEnumerable<string> RunScript(ProcessStartInfo info, Context ctx)
{
var bc = new BlockingCollection<string>();
var process = new Process
using var process = new Process
{
StartInfo = info,
EnableRaisingEvents = true
};

process.OutputDataReceived += (s, e) => bc.Add(e.Data);
process.Exited += (x, p) => bc.Add(null);

Expand All @@ -75,26 +50,61 @@ public static IEnumerable<string> ExecuteScript(ProcessStartInfo info, Context c

foreach (var line in bc.GetConsumingEnumerable())
{
if (process.HasExited && line == null)
break;

if (line == null)
{
if (process.HasExited)
break;
continue;
}

Console.WriteLine(line);

yield return line;
}

var errors = process.StandardError.ReadToEnd();
if (process.ExitCode != 0)
{
yield return ctx.IsGemini ? $"{(int)GeminiCode.CGIError} {errors}\r\n" : $"{(int)SpartanCode.ServerError} {errors}\r\n";
}
Console.WriteLine(errors);

process.Close();
process.Dispose();
if (!string.IsNullOrWhiteSpace(errors))
Console.WriteLine(errors);
}

private static void AddBaseEnvironmentVariables(ProcessStartInfo info, Context ctx, string scriptName, string pathInfo)
{
info.EnvironmentVariables.Add("DOTNET_CLI_HOME", Environment.GetEnvironmentVariable("DOTNET_CLI_HOME") ?? "/tmp/.dotnet");
info.EnvironmentVariables.Add("GATEWAY_INTERFACE", "CGI/1.1");
info.EnvironmentVariables.Add("SERVER_PROTOCOL", ctx.IsGemini ? "GEMINI" : "SPARTAN");
info.EnvironmentVariables.Add("SERVER_PORT", (ctx.IsGemini ? Program.Cfg.GeminiPort : Program.Cfg.SpartanPort).ToString());
info.EnvironmentVariables.Add("SERVER_SOFTWARE", $"atlas/{Program.Version}");
info.EnvironmentVariables.Add("URL", ctx.Request);
info.EnvironmentVariables.Add("SCRIPT_NAME", scriptName);
info.EnvironmentVariables.Add("PATH_INFO", pathInfo);
info.EnvironmentVariables.Add("QUERY_STRING", ctx.Uri.Query);
info.EnvironmentVariables.Add("SERVER_NAME", ctx.Capsule.FQDN);
info.EnvironmentVariables.Add("REMOTE_HOST", ctx.ClientIP);
info.EnvironmentVariables.Add("REMOTE_ADDR", ctx.ClientIP);
}

private static void AddGeminiEnvironmentVariables(ProcessStartInfo info, GeminiCtx ctx)
{
info.EnvironmentVariables.Add("TLS_VERSION", "1.3");

if (ctx.Certificate != null)
{
info.EnvironmentVariables.Add("REMOTE_USER", ctx.CertSubject);
info.EnvironmentVariables.Add("TLS_CLIENT_VALID", ctx.IsValidCert.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_TRUSTED", ctx.IsTrustedCert.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_SUBJECT", ctx.CertSubject);
info.EnvironmentVariables.Add("TLS_CLIENT_HASH", ctx.CertThumbprint);
info.EnvironmentVariables.Add("TLS_CLIENT_NOT_BEFORE", ctx.Certificate.NotBefore.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_NOT_AFTER", ctx.Certificate.NotAfter.ToString());
info.EnvironmentVariables.Add("TLS_CLIENT_SERIAL_NUMBER", ctx.Certificate.GetSerialNumberString());
info.EnvironmentVariables.Add("AUTH_TYPE", "certificate");
}
else
{
info.EnvironmentVariables.Add("AUTH_TYPE", "none");
}
}
}
}
9 changes: 8 additions & 1 deletion Servers/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ public abstract class Context
public StreamReader Reader;
public Capsule Capsule;
public Uri Uri;
public string Request = string.Empty;

private string _request = string.Empty;
public string Request
{
get => _request;
set => _request = value?.Replace("\r\n", "").Trim() ?? string.Empty;
}

public string ClientIP => Socket.RemoteEndPoint.ToString().Split(':')[0];
internal DateTime RequestStart;

Expand Down
57 changes: 26 additions & 31 deletions Servers/Gemini/GeminiServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,12 @@ public async ValueTask<bool> HandShake(GeminiCtx ctx)
ctx.Certificate = new X509Certificate2(ClientCertificate);

// Allow 5 minutes of clock skew
var notBeforeMinutesDelta = (ctx.Certificate.NotBefore.ToUniversalTime() - DateTime.UtcNow).TotalMinutes;
var notAfterMinutesDelta = (DateTime.UtcNow - ctx.Certificate.NotAfter.ToUniversalTime()).TotalMinutes;
var now = DateTime.UtcNow;
var skew = TimeSpan.FromMinutes(5);
var notBefore = ctx.Certificate.NotBefore.ToUniversalTime();
var notAfter = ctx.Certificate.NotAfter.ToUniversalTime();

if (notBeforeMinutesDelta > 5 || notAfterMinutesDelta > 5)
return false;

return true;
return notBefore - skew <= now && now <= notAfter + skew;
};

await ctx.SslStream.AuthenticateAsServerAsync(TlsOptions).ConfigureAwait(false);
Expand All @@ -106,7 +105,7 @@ public async ValueTask<bool> HandShake(GeminiCtx ctx)
}
catch (Exception e)
{
Program.Log(ctx, "TLS HandShake aborted. Reason: " + e.Message);
Program.LogError(ctx, e, "TLS HandShake");
return false;
}
return true;
Expand Down Expand Up @@ -149,43 +148,39 @@ private static async ValueTask ProcessSocket(GeminiCtx ctx)
else
await ctx.SslStream.WriteAsync(response);
}
catch (Exception e) { Console.WriteLine(e); }
catch (Exception e) { Program.LogError(ctx, e, "request processing"); }
finally { CloseConnection(ctx); }
}

private static async ValueTask AnimatedResponse(GeminiCtx ctx, Response response)
{
var lines = Encoding.UTF8.GetString(response.Data.ToArray()).Split('\n'); ;
var lines = Encoding.UTF8.GetString(response.Data.ToArray()).Split('\n');
var timeCount = 0;
var maxTime = Program.Cfg.SlowModeMaxMilliSeconds;

for (int i = 0; i < lines.Length; i++)
{
var line = lines[i] + '\n';
if (i == 0)
{
await ctx.SslStream.WriteAsync(Encoding.UTF8.GetBytes(line));
timeCount += 32;
await Task.Delay(32);
continue;
}
else if (line.StartsWith('#') && timeCount < maxTime)
var isHeader = i == 0;
var isHeading = line.StartsWith('#');
var delay = isHeader ? 32 : 16;

if (isHeading && timeCount < maxTime)
{
var bytes = Encoding.UTF8.GetBytes(line);
foreach (var b in bytes)
foreach (var b in Encoding.UTF8.GetBytes(line))
{
await ctx.SslStream.WriteAsync(new []{b});
timeCount += 16;
await Task.Delay(16);
await ctx.SslStream.WriteAsync(new[] { b });
timeCount += delay;
await Task.Delay(delay);
}
await ctx.SslStream.WriteAsync(Encoding.UTF8.GetBytes("\n"));
continue;
}

await ctx.SslStream.WriteAsync(Encoding.UTF8.GetBytes(line));
timeCount += 16;

if (timeCount < maxTime)
await Task.Delay(16);
else
{
await ctx.SslStream.WriteAsync(Encoding.UTF8.GetBytes(line));
timeCount += delay;
if (timeCount < maxTime)
await Task.Delay(delay);
}
}
}

Expand All @@ -194,7 +189,7 @@ public static void CloseConnection(Context ctx)
try {
ctx?.Socket?.Dispose();
}
catch (Exception e) { Console.WriteLine(e); }
catch (Exception e) { Program.LogError(ctx, e, "connection cleanup"); }
finally { Program.Log(ctx, "complete"); }
}
}
Expand Down
Loading
Loading