Skip to content

Commit 3115c93

Browse files
committed
feat: add support for RDP URIs
1 parent 78ff6da commit 3115c93

File tree

8 files changed

+605
-86
lines changed

8 files changed

+605
-86
lines changed

App/App.xaml.cs

+16-7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public partial class App : Application
4141
#endif
4242

4343
private readonly ILogger<App> _logger;
44+
private readonly IUriHandler _uriHandler;
4445

4546
public App()
4647
{
@@ -72,6 +73,8 @@ public App()
7273
.Bind(builder.Configuration.GetSection(MutagenControllerConfigSection));
7374
services.AddSingleton<ISyncSessionController, MutagenController>();
7475
services.AddSingleton<IUserNotifier, UserNotifier>();
76+
services.AddSingleton<IRdpConnector, RdpConnector>();
77+
services.AddSingleton<IUriHandler, UriHandler>();
7578

7679
// SignInWindow views and view models
7780
services.AddTransient<SignInViewModel>();
@@ -98,6 +101,7 @@ public App()
98101

99102
_services = services.BuildServiceProvider();
100103
_logger = (ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
104+
_uriHandler = (IUriHandler)_services.GetService(typeof(IUriHandler))!;
101105

102106
InitializeComponent();
103107
}
@@ -190,7 +194,18 @@ public void OnActivated(object? sender, AppActivationArguments args)
190194
_logger.LogWarning("URI activation with null data");
191195
return;
192196
}
193-
HandleURIActivation(protoArgs.Uri);
197+
198+
try
199+
{
200+
// don't need to wait for it to complete.
201+
_ = _uriHandler.HandleUri(protoArgs.Uri);
202+
}
203+
catch (System.Exception e)
204+
{
205+
_logger.LogError(e, "unhandled exception while processing URI coder://{authority}{path}",
206+
protoArgs.Uri.Authority, protoArgs.Uri.AbsolutePath);
207+
}
208+
194209
break;
195210

196211
case ExtendedActivationKind.AppNotification:
@@ -204,12 +219,6 @@ public void OnActivated(object? sender, AppActivationArguments args)
204219
}
205220
}
206221

207-
public void HandleURIActivation(Uri uri)
208-
{
209-
// don't log the query string as that's where we include some sensitive information like passwords
210-
_logger.LogInformation("handling URI activation for {path}", uri.AbsolutePath);
211-
}
212-
213222
public void HandleNotification(AppNotificationManager? sender, AppNotificationActivatedEventArgs args)
214223
{
215224
// right now, we don't do anything other than log

App/Services/CredentialManager.cs

+142-77
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Net.Sockets;
23
using System.Runtime.InteropServices;
34
using System.Text;
45
using System.Text.Json;
@@ -307,7 +308,7 @@ public WindowsCredentialBackend(string credentialsTargetName)
307308

308309
public Task<RawCredentials?> ReadCredentials(CancellationToken ct = default)
309310
{
310-
var raw = NativeApi.ReadCredentials(_credentialsTargetName);
311+
var raw = Wincred.ReadCredentials(_credentialsTargetName);
311312
if (raw == null) return Task.FromResult<RawCredentials?>(null);
312313

313314
RawCredentials? credentials;
@@ -326,115 +327,179 @@ public WindowsCredentialBackend(string credentialsTargetName)
326327
public Task WriteCredentials(RawCredentials credentials, CancellationToken ct = default)
327328
{
328329
var raw = JsonSerializer.Serialize(credentials, RawCredentialsJsonContext.Default.RawCredentials);
329-
NativeApi.WriteCredentials(_credentialsTargetName, raw);
330+
Wincred.WriteCredentials(_credentialsTargetName, raw);
330331
return Task.CompletedTask;
331332
}
332333

333334
public Task DeleteCredentials(CancellationToken ct = default)
334335
{
335-
NativeApi.DeleteCredentials(_credentialsTargetName);
336+
Wincred.DeleteCredentials(_credentialsTargetName);
336337
return Task.CompletedTask;
337338
}
338339

339-
private static class NativeApi
340+
}
341+
342+
/// <summary>
343+
/// Wincred provides relatively low level wrapped calls to the Wincred.h native API.
344+
/// </summary>
345+
internal static class Wincred
346+
{
347+
private const int CredentialTypeGeneric = 1;
348+
private const int CredentialTypeDomainPassword = 2;
349+
private const int PersistenceTypeLocalComputer = 2;
350+
private const int ErrorNotFound = 1168;
351+
private const int CredMaxCredentialBlobSize = 5 * 512;
352+
private const string PackageNTLM = "NTLM";
353+
354+
public static string? ReadCredentials(string targetName)
340355
{
341-
private const int CredentialTypeGeneric = 1;
342-
private const int PersistenceTypeLocalComputer = 2;
343-
private const int ErrorNotFound = 1168;
344-
private const int CredMaxCredentialBlobSize = 5 * 512;
356+
if (!CredReadW(targetName, CredentialTypeGeneric, 0, out var credentialPtr))
357+
{
358+
var error = Marshal.GetLastWin32Error();
359+
if (error == ErrorNotFound) return null;
360+
throw new InvalidOperationException($"Failed to read credentials (Error {error})");
361+
}
345362

346-
public static string? ReadCredentials(string targetName)
363+
try
347364
{
348-
if (!CredReadW(targetName, CredentialTypeGeneric, 0, out var credentialPtr))
349-
{
350-
var error = Marshal.GetLastWin32Error();
351-
if (error == ErrorNotFound) return null;
352-
throw new InvalidOperationException($"Failed to read credentials (Error {error})");
353-
}
365+
var cred = Marshal.PtrToStructure<CREDENTIALW>(credentialPtr);
366+
return Marshal.PtrToStringUni(cred.CredentialBlob, cred.CredentialBlobSize / sizeof(char));
367+
}
368+
finally
369+
{
370+
CredFree(credentialPtr);
371+
}
372+
}
354373

355-
try
356-
{
357-
var cred = Marshal.PtrToStructure<CREDENTIAL>(credentialPtr);
358-
return Marshal.PtrToStringUni(cred.CredentialBlob, cred.CredentialBlobSize / sizeof(char));
359-
}
360-
finally
374+
public static void WriteCredentials(string targetName, string secret)
375+
{
376+
var byteCount = Encoding.Unicode.GetByteCount(secret);
377+
if (byteCount > CredMaxCredentialBlobSize)
378+
throw new ArgumentOutOfRangeException(nameof(secret),
379+
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
380+
381+
var credentialBlob = Marshal.StringToHGlobalUni(secret);
382+
var cred = new CREDENTIALW
383+
{
384+
Type = CredentialTypeGeneric,
385+
TargetName = targetName,
386+
CredentialBlobSize = byteCount,
387+
CredentialBlob = credentialBlob,
388+
Persist = PersistenceTypeLocalComputer,
389+
};
390+
try
391+
{
392+
if (!CredWriteW(ref cred, 0))
361393
{
362-
CredFree(credentialPtr);
394+
var error = Marshal.GetLastWin32Error();
395+
throw new InvalidOperationException($"Failed to write credentials (Error {error})");
363396
}
364397
}
365-
366-
public static void WriteCredentials(string targetName, string secret)
398+
finally
367399
{
368-
var byteCount = Encoding.Unicode.GetByteCount(secret);
369-
if (byteCount > CredMaxCredentialBlobSize)
370-
throw new ArgumentOutOfRangeException(nameof(secret),
371-
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
400+
Marshal.FreeHGlobal(credentialBlob);
401+
}
402+
}
372403

373-
var credentialBlob = Marshal.StringToHGlobalUni(secret);
374-
var cred = new CREDENTIAL
375-
{
376-
Type = CredentialTypeGeneric,
377-
TargetName = targetName,
378-
CredentialBlobSize = byteCount,
379-
CredentialBlob = credentialBlob,
380-
Persist = PersistenceTypeLocalComputer,
381-
};
382-
try
383-
{
384-
if (!CredWriteW(ref cred, 0))
385-
{
386-
var error = Marshal.GetLastWin32Error();
387-
throw new InvalidOperationException($"Failed to write credentials (Error {error})");
388-
}
389-
}
390-
finally
391-
{
392-
Marshal.FreeHGlobal(credentialBlob);
393-
}
404+
public static void DeleteCredentials(string targetName)
405+
{
406+
if (!CredDeleteW(targetName, CredentialTypeGeneric, 0))
407+
{
408+
var error = Marshal.GetLastWin32Error();
409+
if (error == ErrorNotFound) return;
410+
throw new InvalidOperationException($"Failed to delete credentials (Error {error})");
394411
}
412+
}
413+
414+
public static void WriteDomainCredentials(string domainName, string serverName, string username, string password)
415+
{
416+
var targetName = $"{domainName}/{serverName}";
417+
var targetInfo = new CREDENTIAL_TARGET_INFORMATIONW
418+
{
419+
TargetName = targetName,
420+
DnsServerName = serverName,
421+
DnsDomainName = domainName,
422+
PackageName = PackageNTLM,
423+
};
424+
var byteCount = Encoding.Unicode.GetByteCount(password);
425+
if (byteCount > CredMaxCredentialBlobSize)
426+
throw new ArgumentOutOfRangeException(nameof(password),
427+
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
395428

396-
public static void DeleteCredentials(string targetName)
429+
var credentialBlob = Marshal.StringToHGlobalUni(password);
430+
var cred = new CREDENTIALW
397431
{
398-
if (!CredDeleteW(targetName, CredentialTypeGeneric, 0))
432+
Type = CredentialTypeDomainPassword,
433+
TargetName = targetName,
434+
CredentialBlobSize = byteCount,
435+
CredentialBlob = credentialBlob,
436+
Persist = PersistenceTypeLocalComputer,
437+
UserName = username,
438+
};
439+
try
440+
{
441+
if (!CredWriteDomainCredentialsW(ref targetInfo, ref cred, 0))
399442
{
400443
var error = Marshal.GetLastWin32Error();
401-
if (error == ErrorNotFound) return;
402-
throw new InvalidOperationException($"Failed to delete credentials (Error {error})");
444+
throw new InvalidOperationException($"Failed to write credentials (Error {error})");
403445
}
404446
}
447+
finally
448+
{
449+
Marshal.FreeHGlobal(credentialBlob);
450+
}
451+
}
405452

406-
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
407-
private static extern bool CredReadW(string target, int type, int reservedFlag, out IntPtr credentialPtr);
453+
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
454+
private static extern bool CredReadW(string target, int type, int reservedFlag, out IntPtr credentialPtr);
408455

409-
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
410-
private static extern bool CredWriteW([In] ref CREDENTIAL userCredential, [In] uint flags);
456+
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
457+
private static extern bool CredWriteW([In] ref CREDENTIALW userCredential, [In] uint flags);
411458

412-
[DllImport("Advapi32.dll", SetLastError = true)]
413-
private static extern void CredFree([In] IntPtr cred);
459+
[DllImport("Advapi32.dll", SetLastError = true)]
460+
private static extern void CredFree([In] IntPtr cred);
414461

415-
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
416-
private static extern bool CredDeleteW(string target, int type, int flags);
462+
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
463+
private static extern bool CredDeleteW(string target, int type, int flags);
417464

418-
[StructLayout(LayoutKind.Sequential)]
419-
private struct CREDENTIAL
420-
{
421-
public int Flags;
422-
public int Type;
465+
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
466+
private static extern bool CredWriteDomainCredentialsW([In] ref CREDENTIAL_TARGET_INFORMATIONW target, [In] ref CREDENTIALW userCredential, [In] uint flags);
423467

424-
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
468+
[StructLayout(LayoutKind.Sequential)]
469+
private struct CREDENTIALW
470+
{
471+
public int Flags;
472+
public int Type;
425473

426-
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
474+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
427475

428-
public long LastWritten;
429-
public int CredentialBlobSize;
430-
public IntPtr CredentialBlob;
431-
public int Persist;
432-
public int AttributeCount;
433-
public IntPtr Attributes;
476+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
434477

435-
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
478+
public long LastWritten;
479+
public int CredentialBlobSize;
480+
public IntPtr CredentialBlob;
481+
public int Persist;
482+
public int AttributeCount;
483+
public IntPtr Attributes;
436484

437-
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
438-
}
485+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
486+
487+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
488+
}
489+
490+
[StructLayout(LayoutKind.Sequential)]
491+
private struct CREDENTIAL_TARGET_INFORMATIONW
492+
{
493+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
494+
[MarshalAs(UnmanagedType.LPWStr)] public string NetbiosServerName;
495+
[MarshalAs(UnmanagedType.LPWStr)] public string DnsServerName;
496+
[MarshalAs(UnmanagedType.LPWStr)] public string NetbiosDomainName;
497+
[MarshalAs(UnmanagedType.LPWStr)] public string DnsDomainName;
498+
[MarshalAs(UnmanagedType.LPWStr)] public string DnsTreeName;
499+
[MarshalAs(UnmanagedType.LPWStr)] public string PackageName;
500+
501+
public uint Flags;
502+
public uint CredTypeCount;
503+
public IntPtr CredTypes;
439504
}
440505
}

0 commit comments

Comments
 (0)