Skip to content
Merged
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
154 changes: 154 additions & 0 deletions src/W-Fix.Core/Abstractions/PairRepairContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System.Net;
using WFix.Core.Models;

namespace WFix.Core.Abstractions;

public interface IPairInvitationValidator
{
void Validate(PairInvitation invitation, DateTimeOffset now);
}

public interface IPairFileService
{
Task WriteInvitationAsync(string path, PairInvitation invitation, CancellationToken cancellationToken = default);
Task<PairInvitation> ReadInvitationAsync(string path, CancellationToken cancellationToken = default);
Task WriteOfflineSnapshotAsync(string path, PairEndpointSnapshot snapshot, CancellationToken cancellationToken = default);
Task<PairEndpointSnapshot> ReadOfflineSnapshotAsync(string path, CancellationToken cancellationToken = default);
}

public interface IPairSession : IAsyncDisposable
{
PairInvitation Invitation { get; }
PairEndpointRole LocalRole { get; }
PairSessionState State { get; }
string ConfirmationCode { get; }

Task<bool> ApproveAsync(bool approved, CancellationToken cancellationToken = default);
Task SendAsync<T>(PairMessageKind kind, T message, CancellationToken cancellationToken = default);
Task<T> ReceiveAsync<T>(PairMessageKind expectedKind, CancellationToken cancellationToken = default);
}

public interface IPairHost : IAsyncDisposable
{
PairInvitation Invitation { get; }
Task<IPairSession> AcceptAsync(CancellationToken cancellationToken = default);
}

public sealed record PairHostOptions
{
public string HostComputerName { get; init; } = Environment.MachineName;
public IReadOnlyList<IPAddress>? ListenAddresses { get; init; }
public string? PrinterName { get; init; }
public string? ShareName { get; init; }
public TimeSpan InvitationLifetime { get; init; } = TimeSpan.FromMinutes(15);
}

public interface IPairSessionTransport
{
Task<IPairHost> StartHostAsync(PairHostOptions options, CancellationToken cancellationToken = default);
Task<IPairSession> JoinAsync(PairInvitation invitation, CancellationToken cancellationToken = default);
}

public interface IPairFirewallLeaseService
{
Task<IAsyncDisposable> OpenAsync(string sessionId, int port, string executablePath, CancellationToken cancellationToken = default);
Task CleanupStaleAsync(CancellationToken cancellationToken = default);
}

public interface IPairInventoryService
{
Task<PairEndpointSnapshot> CaptureAsync(
TargetDescriptor target,
PairEndpointRole role,
string peerName,
string? printerName = null,
string? shareName = null,
CancellationToken cancellationToken = default);
}

public interface IPairDiagnosticRule
{
string Id { get; }
Task<IReadOnlyList<PairDiagnosticFinding>> EvaluateAsync(
PairEndpointSnapshot host,
PairEndpointSnapshot client,
CancellationToken cancellationToken = default);
}

public interface IPairDiagnosticService
{
Task<IReadOnlyList<PairDiagnosticFinding>> DiagnoseAsync(
PairEndpointSnapshot host,
PairEndpointSnapshot client,
CancellationToken cancellationToken = default);
}

public interface IPairRepairPlanner
{
PairRepairPlan CreatePlan(
PairEndpointSnapshot host,
PairEndpointSnapshot client,
PairTransportMode transportMode,
IReadOnlyList<PairDiagnosticFinding> findings,
bool includeExpertActions = false);
}

public sealed record PairActionContext(
TargetDescriptor Target,
PairRepairStep Step,
string RunDirectory,
IProgress<LogEntry>? Progress = null);

public interface IPairRepairAction
{
string Id { get; }
string Name { get; }
RepairRisk Risk { get; }
bool ExpertOnly { get; }
bool IsIdempotent { get; }

Task<PairActionCheckpoint> PrepareAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<PairActionResult> ExecuteAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<bool> VerifyAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<PairActionResult> RollbackAsync(PairActionContext context, PairActionCheckpoint checkpoint, CancellationToken cancellationToken = default);
}

public interface IPairRepairActionRegistry
{
IReadOnlyList<IPairRepairAction> GetAll();
IPairRepairAction? Get(string actionId);
}

public interface IPairActionDispatcher
{
Task<PairActionCheckpoint> PrepareAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<PairActionResult> ExecuteAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<bool> VerifyAsync(PairActionContext context, CancellationToken cancellationToken = default);
Task<PairActionResult> RollbackAsync(PairActionContext context, PairActionCheckpoint checkpoint, CancellationToken cancellationToken = default);
Task CompleteAsync(bool commit, CancellationToken cancellationToken = default) => Task.CompletedTask;
}

public interface IPairAgentCommandLoop
{
Task RunAsync(IPairSession session, CancellationToken cancellationToken = default);
}

public interface IPairRepairExecutor
{
Task<PairRun> ExecuteAsync(
PairRepairPlan plan,
IReadOnlyDictionary<PairEndpointRole, TargetDescriptor> targets,
IProgress<PairRun>? progress = null,
CancellationToken cancellationToken = default);
}

public interface INetworkCredentialProvisioner
{
Task SaveForHostAsync(string hostName, NetworkCredential credential, CancellationToken cancellationToken = default);
Task DeleteForHostAsync(string hostName, CancellationToken cancellationToken = default);
}

public interface IPairRunReportService
{
Task<string> WriteAsync(PairRun run, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.ComponentModel;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using WFix.Core.Abstractions;

namespace WFix.Core.Infrastructure;

public sealed class WindowsNetworkCredentialProvisioner : INetworkCredentialProvisioner
{
private const int CredentialTypeDomainPassword = 2;
private const int CredentialPersistLocalMachine = 2;
private const int ErrorNotFound = 1168;

public Task SaveForHostAsync(string hostName, NetworkCredential credential, CancellationToken cancellationToken = default)
{
var normalized = NormalizeHost(hostName);
ArgumentNullException.ThrowIfNull(credential);
if (string.IsNullOrWhiteSpace(credential.UserName) || string.IsNullOrEmpty(credential.Password))
throw new ArgumentException("Для SMB-подключения нужны имя пользователя и непустой пароль.", nameof(credential));
cancellationToken.ThrowIfCancellationRequested();
return Task.Run(() => Write(normalized, credential), cancellationToken);
}

public Task DeleteForHostAsync(string hostName, CancellationToken cancellationToken = default)
{
var normalized = NormalizeHost(hostName);
cancellationToken.ThrowIfCancellationRequested();
return Task.Run(() =>
{
if (CredDelete(normalized, CredentialTypeDomainPassword, 0)) return;
var error = Marshal.GetLastWin32Error();
if (error != ErrorNotFound) throw new Win32Exception(error);
}, cancellationToken);
}

private static void Write(string hostName, NetworkCredential credential)
{
var bytes = Encoding.Unicode.GetBytes(credential.Password);
if (bytes.Length > 512) throw new ArgumentException("Пароль превышает ограничение Windows Credential Manager.", nameof(credential));
var pointer = Marshal.AllocCoTaskMem(bytes.Length);
try
{
Marshal.Copy(bytes, 0, pointer, bytes.Length);
var native = new NativeCredential
{
Type = CredentialTypeDomainPassword,
TargetName = hostName,
UserName = credential.UserName,
CredentialBlob = pointer,
CredentialBlobSize = bytes.Length,
Persist = CredentialPersistLocalMachine,
Comment = "W-Fix Pair Repair: authenticated SMB access to the selected printer host"
};
if (!CredWrite(ref native, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
CryptographicOperations.ZeroMemory(bytes);
Marshal.FreeCoTaskMem(pointer);
}
}

private static string NormalizeHost(string hostName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(hostName);
var value = hostName.Trim().TrimStart('\\').TrimEnd('.').ToUpperInvariant();
if (value.Length is 0 or > 255 || value.Contains('\\') || value.Contains('/') || IPAddress.TryParse(value, out _))
throw new ArgumentException("SMB credential должен быть привязан к имени компьютера, а не к IP или UNC-пути.", nameof(hostName));
return value;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NativeCredential
{
public int Flags;
public int Type;
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)] public string? Comment;
public long LastWritten;
public int CredentialBlobSize;
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)] public string? TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
}

[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CredWrite([In] ref NativeCredential credential, int flags);

[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CredDelete(string target, int type, int flags);
}
Loading