Files
tyx_AI_xhs/supervisor/Dada.Supervisor/Credentials.cs
T

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;
}
}