Zwei Beobachtungen aus der laufenden Integration. 1. Der Installer schrieb Geheimnisse weltlesbar SetupWriter benutzte File.WriteAllText ohne Rechteanpassung - unter Linux also die Standardmaske und damit ueblicherweise 644. In genau dieser Datei stehen Lizenzschluessel und Anwendungstoken; jeder Benutzer des Systems konnte sie lesen. Der Lizenz-Cache in StateStore wird aus demselben Grund seit jeher auf 600 gesetzt - der Installer zog nicht nach. Enthaelt ein Ziel mindestens einen geheimen Wert, wird die Datei jetzt auf den eigenen Benutzer beschraenkt. Als geheim gilt type=secret UND source=provision: ein so geholtes Token traegt oft den Typ "string", ist aber genauso schutzbeduerftig. Unter Windows bleibt es beim Profil-ACL. 2. Zielnamen koennen plattformabhaengig unterschiedlich sein %APPDATA%\MeineAnwendung gegen $XDG_CONFIG_HOME/meineanwendung - eine setup.json kannte nur eine Schreibweise. Die kleingeschriebene Form allein traegt, weil NTFS die Schreibweise ignoriert, aber nur solange das Dateisystem tatsaechlich unempfindlich ist; auf APFS mit Beachtung der Schreibweise oder bei groesseren Unterschieden entstuende ein zweites, leeres Verzeichnis neben dem, aus dem die Anwendung liest. Ziele haben deshalb optional fileWindows, fileLinux und fileMacOS; ohne Angabe gilt weiterhin file. Die Dokumentation haelt ausserdem fest, dass der Installer bewusst Klartext schreibt und die Anwendung selbst entscheidet, ob und wie sie ihn danach schuetzt - und dass eine Entschluesselung, die Klartext durchreicht, deshalb kein Altlast-Zweig mehr ist, sondern ein aktiv genutzter Pfad. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
222 lines
8.3 KiB
C#
222 lines
8.3 KiB
C#
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(FileFor(target));
|
|
|
|
// 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>
|
|
/// Waehlt den Pfad fuer die laufende Plattform.
|
|
///
|
|
/// Konfigurationsordner heissen plattformabhaengig unterschiedlich -
|
|
/// "ClawdDotNet" unter Windows, "clawddotnet" unter Linux. Die
|
|
/// kleingeschriebene Form allein traegt nur, solange das Dateisystem
|
|
/// die Schreibweise ignoriert; auf einem case-sensitiven Volume
|
|
/// entstuende sonst ein zweites, leeres Verzeichnis neben dem, aus dem
|
|
/// die Anwendung liest.
|
|
/// </summary>
|
|
public static string FileFor(SetupTarget? target)
|
|
{
|
|
if (target == null)
|
|
return string.Empty;
|
|
|
|
string specific = OperatingSystemHelpers.IsWindows() ? target.FileWindows
|
|
: OperatingSystemHelpers.IsMacOS() ? target.FileMacOS
|
|
: target.FileLinux;
|
|
|
|
return string.IsNullOrWhiteSpace(specific) ? (target.File ?? string.Empty) : specific;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|
|
}
|