176 lines
6.1 KiB
C#
176 lines
6.1 KiB
C#
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.Json;
|
|
|
|
namespace Dada.Supervisor;
|
|
|
|
internal enum ChildRole
|
|
{
|
|
Api,
|
|
Worker,
|
|
}
|
|
|
|
internal static class CredentialCatalog
|
|
{
|
|
internal const string ApiResend = "Dada/P0A/api/resend";
|
|
internal const string ApiAmap = "Dada/P0A/api/amap";
|
|
internal const string WorkerAiGateway = "Dada/P0A/worker/ai-gateway";
|
|
internal const string AdminPepper = "Dada/P0A/admin/pepper";
|
|
|
|
internal static IReadOnlyList<string> RequiredFor(ChildRole role) => role switch
|
|
{
|
|
ChildRole.Api => [ApiResend, ApiAmap],
|
|
ChildRole.Worker => [WorkerAiGateway],
|
|
_ => throw new ArgumentOutOfRangeException(nameof(role)),
|
|
};
|
|
}
|
|
|
|
internal interface ICredentialStore
|
|
{
|
|
string? Read(string target);
|
|
void Write(string target, string value);
|
|
void Delete(string target);
|
|
bool IsConfigured(string target);
|
|
}
|
|
|
|
internal sealed class MissingCredentialException(string target)
|
|
: InvalidOperationException($"Required credential is not configured: {target}");
|
|
|
|
internal static class CredentialProcessLauncher
|
|
{
|
|
internal static async Task<Process> StartAsync(
|
|
ProcessStartInfo startInfo,
|
|
ChildRole role,
|
|
ICredentialStore store,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var credentials = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
foreach (var target in CredentialCatalog.RequiredFor(role))
|
|
{
|
|
credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target);
|
|
}
|
|
|
|
startInfo.UseShellExecute = false;
|
|
startInfo.CreateNoWindow = true;
|
|
startInfo.RedirectStandardInput = true;
|
|
var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start managed child process.");
|
|
var payload = JsonSerializer.SerializeToUtf8Bytes(credentials);
|
|
try
|
|
{
|
|
await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken);
|
|
await process.StandardInput.BaseStream.FlushAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
|
process.Dispose();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
Array.Clear(payload);
|
|
process.StandardInput.Close();
|
|
foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty;
|
|
credentials.Clear();
|
|
}
|
|
return process;
|
|
}
|
|
}
|
|
|
|
internal sealed class WindowsCredentialStore : ICredentialStore
|
|
{
|
|
public string? Read(string target)
|
|
{
|
|
if (!CredRead(target, CredentialType.Generic, 0, out var pointer))
|
|
{
|
|
var error = Marshal.GetLastWin32Error();
|
|
if (error == ErrorNotFound) return null;
|
|
throw new Win32Exception(error);
|
|
}
|
|
|
|
try
|
|
{
|
|
var credential = Marshal.PtrToStructure<NativeCredential>(pointer);
|
|
return credential.CredentialBlobSize == 0
|
|
? string.Empty
|
|
: Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / sizeof(char));
|
|
}
|
|
finally
|
|
{
|
|
CredFree(pointer);
|
|
}
|
|
}
|
|
|
|
public void Write(string target, string value)
|
|
{
|
|
var bytes = System.Text.Encoding.Unicode.GetBytes(value);
|
|
if (bytes.Length > MaximumGenericBlobSize) throw new ArgumentOutOfRangeException(nameof(value));
|
|
var pointer = Marshal.AllocHGlobal(bytes.Length);
|
|
try
|
|
{
|
|
Marshal.Copy(bytes, 0, pointer, bytes.Length);
|
|
var credential = new NativeCredential
|
|
{
|
|
Type = CredentialType.Generic,
|
|
TargetName = target,
|
|
CredentialBlobSize = bytes.Length,
|
|
CredentialBlob = pointer,
|
|
Persist = CredentialPersistence.LocalMachine,
|
|
UserName = Environment.UserName,
|
|
};
|
|
if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
}
|
|
finally
|
|
{
|
|
Marshal.Copy(new byte[bytes.Length], 0, pointer, bytes.Length);
|
|
Marshal.FreeHGlobal(pointer);
|
|
Array.Clear(bytes);
|
|
}
|
|
}
|
|
|
|
public void Delete(string target)
|
|
{
|
|
if (CredDelete(target, CredentialType.Generic, 0)) return;
|
|
var error = Marshal.GetLastWin32Error();
|
|
if (error != ErrorNotFound) throw new Win32Exception(error);
|
|
}
|
|
|
|
public bool IsConfigured(string target) => Read(target) is not null;
|
|
|
|
private const int ErrorNotFound = 1168;
|
|
private const int MaximumGenericBlobSize = 5 * 512;
|
|
|
|
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern bool CredRead(string target, CredentialType type, int flags, out IntPtr credential);
|
|
|
|
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern bool CredWrite([In] ref NativeCredential credential, int flags);
|
|
|
|
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
private static extern bool CredDelete(string target, CredentialType type, int flags);
|
|
|
|
[DllImport("Advapi32.dll")]
|
|
private static extern void CredFree(IntPtr buffer);
|
|
|
|
private enum CredentialType : uint { Generic = 1 }
|
|
private enum CredentialPersistence : uint { LocalMachine = 2 }
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct NativeCredential
|
|
{
|
|
public uint Flags;
|
|
public CredentialType Type;
|
|
public string TargetName;
|
|
public string? Comment;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
public int CredentialBlobSize;
|
|
public IntPtr CredentialBlob;
|
|
public CredentialPersistence Persist;
|
|
public int AttributeCount;
|
|
public IntPtr Attributes;
|
|
public string? TargetAlias;
|
|
public string UserName;
|
|
}
|
|
}
|