fix(releases): Lizenzschluessel nicht mehr im Klartext, Selbsttest, Zielorte
Vier Befunde aus einer externen Durchsicht der 2.4-Integration.
1. Die .htpasswd war eine Klartext-Kundenliste
Das htpasswd-Format hasht nur die Passwortspalte. Benutzername UND Passwort
waren der Lizenzschluessel - der Schluessel stand also im Klartext direkt
neben seinem eigenen bcrypt-Hash, und der Hash war Dekoration. Geschuetzt
hat das Ganze nur die FilesMatch-Regel in derselben Datei.
Der Benutzername wird jetzt abgeleitet: lic_<sha256(schluessel), 16 Hex>.
Die Datei enthaelt damit nur noch eine Einwegableitung und einen Hash ueber
einen hochentropen Schluessel.
Server und SDK muessen dabei zeichengenau uebereinstimmen; ein Test prueft
die C#-Ableitung gegen die PHP-Formel.
2. Ein Formatwechsel blieb unbemerkt liegen
Beim Umbau auf 1. faellt auf: reconcile() sah keinen Anlass zur
Neuerzeugung, die Dateien behielten das alte Format, waehrend die Clients
bereits das neue schickten. Die erzeugten Dateien tragen deshalb jetzt eine
Formatkennung; weicht sie ab, wird neu erzeugt.
3. Doku beschrieb Nginx, der Schutz ist Apache-only
.htaccess wird von Nginx ignoriert - dort waeren die Verzeichnisse offen und
die .htpasswd oeffentlich abrufbar. Die Statusanzeige pruefte nur, ob die
Dateien existieren, und haette in dem Fall "GESCHUETZT" gemeldet.
Neu: ein echter Selbsttest ruft die eigene Paket-Adresse OHNE Zugangsdaten
ab und erwartet 401. Er laeuft beim manuellen Erzeugen und nach jeder
automatischen Neuerzeugung; das Ergebnis steht in der Oberflaeche, ein
Fehlschlag im Log. Er findet nebenbei auch abgeschaltetes AllowOverride und
Tippfehler in der erzeugten Datei. Doku korrigiert, Nginx-Vorlage ergaenzt.
4. Erstinstallation schrieb an einen Ort, an dem Linux-Anwendungen nicht lesen
setup.json-Ziele waren immer installationsrelativ. Eine Anwendung, die sich
unter Linux richtig verhaelt, liest aus $XDG_CONFIG_HOME - /opt/<app> ist
fuer den Dienstbenutzer meist nicht schreibbar. Der Installer legte die
Datei also dorthin, wo nie jemand nachsieht.
Ziele haben jetzt ein "location": install (Vorgabe), config, data, home,
plus ${VAR}- und %VAR%-Ersetzung in "file". Unbekannte Variablen bleiben
stehen statt leer zu werden - ein Platzhalter faellt auf, ein falscher Pfad
nicht. Der Installer gibt den aufgeloesten Pfad aus, weil bei config das
Konto entscheidet, unter dem er laeuft.
Ausserdem
- Doku zeigte "status": "ok" fuer update/delete; Http::ok() erzeugt
"status": "success".
- UPGRADE §16.1 deckte Neuprodukte nicht ab: Fuer ein Produkt ohne Release
existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis
entsteht erst mit dem ersten Upload, der naechste Tick schuetzt es. Der erste
ausgelieferte Build muss die Zugangsdaten also schon mitbringen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ceb977187e
commit
7a3a5dad69
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Deploymentcenter.Client.Models;
|
||||
|
||||
namespace Deploymentcenter.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Loest die Zielpfade einer <c>setup.json</c> plattformrichtig auf.
|
||||
///
|
||||
/// Ohne das schrieb der Installer ausschliesslich ins
|
||||
/// Installationsverzeichnis. Eine Anwendung, die sich unter Linux richtig
|
||||
/// verhaelt, liest ihre Konfiguration aber aus $XDG_CONFIG_HOME und unter
|
||||
/// Windows aus %APPDATA% - schon deshalb, weil /opt/<app> fuer den
|
||||
/// Dienstbenutzer typischerweise nicht schreibbar ist. Der Installer legte
|
||||
/// die Datei also dorthin, wo die Anwendung nie nachsieht.
|
||||
/// </summary>
|
||||
public static class SetupPaths
|
||||
{
|
||||
/// <summary>
|
||||
/// Ermittelt den vollstaendigen Pfad eines Ziels.
|
||||
/// </summary>
|
||||
/// <param name="target">Ziel aus der setup.json.</param>
|
||||
/// <param name="installDir">Installationsverzeichnis der Anwendung.</param>
|
||||
public static string Resolve(SetupTarget target, string installDir)
|
||||
{
|
||||
string file = Expand(target?.File ?? string.Empty);
|
||||
|
||||
// Ein absoluter Pfad ist bereits die Antwort - etwa nachdem
|
||||
// ${XDG_CONFIG_HOME} ersetzt wurde.
|
||||
if (Path.IsPathRooted(file))
|
||||
{
|
||||
return Path.GetFullPath(file);
|
||||
}
|
||||
|
||||
string root = BaseDirectory(target?.Location, installDir);
|
||||
return Path.GetFullPath(Path.Combine(root, file.Replace('/', Path.DirectorySeparatorChar)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wurzelverzeichnis fuer eine Ortsangabe.
|
||||
///
|
||||
/// install (Vorgabe) - das Installationsverzeichnis
|
||||
/// config - %APPDATA% bzw. $XDG_CONFIG_HOME (~/.config)
|
||||
/// data - %LOCALAPPDATA% bzw. $XDG_DATA_HOME (~/.local/share)
|
||||
/// home - Benutzerverzeichnis
|
||||
/// </summary>
|
||||
public static string BaseDirectory(string? location, string installDir)
|
||||
{
|
||||
switch ((location ?? string.Empty).Trim().ToLowerInvariant())
|
||||
{
|
||||
case "config":
|
||||
return ConfigDirectory();
|
||||
|
||||
case "data":
|
||||
return DataDirectory();
|
||||
|
||||
case "home":
|
||||
return HomeDirectory();
|
||||
|
||||
case "":
|
||||
case "install":
|
||||
return installDir;
|
||||
|
||||
default:
|
||||
// Eine unbekannte Angabe ins Installationsverzeichnis
|
||||
// umzubiegen ist die harmlosere Auslegung: dort landet die
|
||||
// Datei sichtbar statt an einem geratenen Ort.
|
||||
return installDir;
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConfigDirectory()
|
||||
{
|
||||
if (OperatingSystemHelpers.IsWindows())
|
||||
{
|
||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
return string.IsNullOrWhiteSpace(appData)
|
||||
? Path.Combine(HomeDirectory(), "AppData", "Roaming")
|
||||
: appData;
|
||||
}
|
||||
|
||||
string xdg = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") ?? string.Empty;
|
||||
return string.IsNullOrWhiteSpace(xdg)
|
||||
? Path.Combine(HomeDirectory(), ".config")
|
||||
: xdg;
|
||||
}
|
||||
|
||||
public static string DataDirectory()
|
||||
{
|
||||
if (OperatingSystemHelpers.IsWindows())
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
return string.IsNullOrWhiteSpace(localAppData)
|
||||
? Path.Combine(HomeDirectory(), "AppData", "Local")
|
||||
: localAppData;
|
||||
}
|
||||
|
||||
string xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? string.Empty;
|
||||
return string.IsNullOrWhiteSpace(xdg)
|
||||
? Path.Combine(HomeDirectory(), ".local", "share")
|
||||
: xdg;
|
||||
}
|
||||
|
||||
public static string HomeDirectory()
|
||||
{
|
||||
string home = Environment.GetEnvironmentVariable("HOME") ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(home))
|
||||
{
|
||||
home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(home) ? Directory.GetCurrentDirectory() : home;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ersetzt Umgebungsvariablen in beiden gebraeuchlichen Schreibweisen:
|
||||
/// <c>${NAME}</c> und <c>%NAME%</c>. Unbekannte Namen bleiben stehen -
|
||||
/// eine leere Ersetzung wuerde stillschweigend einen falschen Pfad
|
||||
/// ergeben, ein stehengebliebener Platzhalter faellt dagegen auf.
|
||||
/// </summary>
|
||||
public static string Expand(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
// ${NAME}
|
||||
if (value[i] == '$' && i + 1 < value.Length && value[i + 1] == '{')
|
||||
{
|
||||
int close = value.IndexOf('}', i + 2);
|
||||
if (close > i + 2)
|
||||
{
|
||||
string name = value.Substring(i + 2, close - i - 2);
|
||||
string? resolved = Lookup(name);
|
||||
|
||||
if (resolved != null)
|
||||
{
|
||||
builder.Append(resolved);
|
||||
i = close;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// %NAME%
|
||||
if (value[i] == '%')
|
||||
{
|
||||
int close = value.IndexOf('%', i + 1);
|
||||
if (close > i + 1)
|
||||
{
|
||||
string name = value.Substring(i + 1, close - i - 1);
|
||||
string? resolved = Lookup(name);
|
||||
|
||||
if (resolved != null)
|
||||
{
|
||||
builder.Append(resolved);
|
||||
i = close;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append(value[i]);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schlaegt einen Namen nach. Neben echten Umgebungsvariablen werden
|
||||
/// die XDG-Namen auch dann beantwortet, wenn sie nicht gesetzt sind -
|
||||
/// sonst muesste jede setup.json den Rueckfall auf ~/.config selbst
|
||||
/// ausformulieren.
|
||||
/// </summary>
|
||||
private static string? Lookup(string name)
|
||||
{
|
||||
string value = Environment.GetEnvironmentVariable(name) ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
|
||||
switch (name.ToUpperInvariant())
|
||||
{
|
||||
case "XDG_CONFIG_HOME": return ConfigDirectory();
|
||||
case "XDG_DATA_HOME": return DataDirectory();
|
||||
case "HOME":
|
||||
case "USERPROFILE": return HomeDirectory();
|
||||
case "APPDATA": return ConfigDirectory();
|
||||
case "LOCALAPPDATA": return DataDirectory();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user