Skip to content

Commit 2a4814e

Browse files
authored
feat: add support for RDP URIs (#87)
Adds basic support for `coder:/` URIs for opening RDP. relates to #52 but I still need to add support for checking the authority.
1 parent 119e52a commit 2a4814e

File tree

8 files changed

+596
-86
lines changed

8 files changed

+596
-86
lines changed

App/App.xaml.cs

+17-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,19 @@ public void OnActivated(object? sender, AppActivationArguments args)
190194
_logger.LogWarning("URI activation with null data");
191195
return;
192196
}
193-
HandleURIActivation(protoArgs.Uri);
197+
198+
// don't need to wait for it to complete.
199+
_uriHandler.HandleUri(protoArgs.Uri).ContinueWith(t =>
200+
{
201+
if (t.Exception != null)
202+
{
203+
// don't log query params, as they contain secrets.
204+
_logger.LogError(t.Exception,
205+
"unhandled exception while processing URI coder://{authority}{path}",
206+
protoArgs.Uri.Authority, protoArgs.Uri.AbsolutePath);
207+
}
208+
});
209+
194210
break;
195211

196212
case ExtendedActivationKind.AppNotification:
@@ -204,12 +220,6 @@ public void OnActivated(object? sender, AppActivationArguments args)
204220
}
205221
}
206222

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-
213223
public void HandleNotification(AppNotificationManager? sender, AppNotificationActivatedEventArgs args)
214224
{
215225
// right now, we don't do anything other than log

App/Services/CredentialManager.cs

+141-77
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ public WindowsCredentialBackend(string credentialsTargetName)
307307

308308
public Task<RawCredentials?> ReadCredentials(CancellationToken ct = default)
309309
{
310-
var raw = NativeApi.ReadCredentials(_credentialsTargetName);
310+
var raw = Wincred.ReadCredentials(_credentialsTargetName);
311311
if (raw == null) return Task.FromResult<RawCredentials?>(null);
312312

313313
RawCredentials? credentials;
@@ -326,115 +326,179 @@ public WindowsCredentialBackend(string credentialsTargetName)
326326
public Task WriteCredentials(RawCredentials credentials, CancellationToken ct = default)
327327
{
328328
var raw = JsonSerializer.Serialize(credentials, RawCredentialsJsonContext.Default.RawCredentials);
329-
NativeApi.WriteCredentials(_credentialsTargetName, raw);
329+
Wincred.WriteCredentials(_credentialsTargetName, raw);
330330
return Task.CompletedTask;
331331
}
332332

333333
public Task DeleteCredentials(CancellationToken ct = default)
334334
{
335-
NativeApi.DeleteCredentials(_credentialsTargetName);
335+
Wincred.DeleteCredentials(_credentialsTargetName);
336336
return Task.CompletedTask;
337337
}
338338

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

346-
public static string? ReadCredentials(string targetName)
362+
try
347363
{
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-
}
364+
var cred = Marshal.PtrToStructure<CREDENTIALW>(credentialPtr);
365+
return Marshal.PtrToStringUni(cred.CredentialBlob, cred.CredentialBlobSize / sizeof(char));
366+
}
367+
finally
368+
{
369+
CredFree(credentialPtr);
370+
}
371+
}
354372

355-
try
356-
{
357-
var cred = Marshal.PtrToStructure<CREDENTIAL>(credentialPtr);
358-
return Marshal.PtrToStringUni(cred.CredentialBlob, cred.CredentialBlobSize / sizeof(char));
359-
}
360-
finally
373+
public static void WriteCredentials(string targetName, string secret)
374+
{
375+
var byteCount = Encoding.Unicode.GetByteCount(secret);
376+
if (byteCount > CredMaxCredentialBlobSize)
377+
throw new ArgumentOutOfRangeException(nameof(secret),
378+
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
379+
380+
var credentialBlob = Marshal.StringToHGlobalUni(secret);
381+
var cred = new CREDENTIALW
382+
{
383+
Type = CredentialTypeGeneric,
384+
TargetName = targetName,
385+
CredentialBlobSize = byteCount,
386+
CredentialBlob = credentialBlob,
387+
Persist = PersistenceTypeLocalComputer,
388+
};
389+
try
390+
{
391+
if (!CredWriteW(ref cred, 0))
361392
{
362-
CredFree(credentialPtr);
393+
var error = Marshal.GetLastWin32Error();
394+
throw new InvalidOperationException($"Failed to write credentials (Error {error})");
363395
}
364396
}
365-
366-
public static void WriteCredentials(string targetName, string secret)
397+
finally
367398
{
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");
399+
Marshal.FreeHGlobal(credentialBlob);
400+
}
401+
}
372402

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-
}
403+
public static void DeleteCredentials(string targetName)
404+
{
405+
if (!CredDeleteW(targetName, CredentialTypeGeneric, 0))
406+
{
407+
var error = Marshal.GetLastWin32Error();
408+
if (error == ErrorNotFound) return;
409+
throw new InvalidOperationException($"Failed to delete credentials (Error {error})");
394410
}
411+
}
412+
413+
public static void WriteDomainCredentials(string domainName, string serverName, string username, string password)
414+
{
415+
var targetName = $"{domainName}/{serverName}";
416+
var targetInfo = new CREDENTIAL_TARGET_INFORMATIONW
417+
{
418+
TargetName = targetName,
419+
DnsServerName = serverName,
420+
DnsDomainName = domainName,
421+
PackageName = PackageNTLM,
422+
};
423+
var byteCount = Encoding.Unicode.GetByteCount(password);
424+
if (byteCount > CredMaxCredentialBlobSize)
425+
throw new ArgumentOutOfRangeException(nameof(password),
426+
$"The secret is greater than {CredMaxCredentialBlobSize} bytes");
395427

396-
public static void DeleteCredentials(string targetName)
428+
var credentialBlob = Marshal.StringToHGlobalUni(password);
429+
var cred = new CREDENTIALW
397430
{
398-
if (!CredDeleteW(targetName, CredentialTypeGeneric, 0))
431+
Type = CredentialTypeDomainPassword,
432+
TargetName = targetName,
433+
CredentialBlobSize = byteCount,
434+
CredentialBlob = credentialBlob,
435+
Persist = PersistenceTypeLocalComputer,
436+
UserName = username,
437+
};
438+
try
439+
{
440+
if (!CredWriteDomainCredentialsW(ref targetInfo, ref cred, 0))
399441
{
400442
var error = Marshal.GetLastWin32Error();
401-
if (error == ErrorNotFound) return;
402-
throw new InvalidOperationException($"Failed to delete credentials (Error {error})");
443+
throw new InvalidOperationException($"Failed to write credentials (Error {error})");
403444
}
404445
}
446+
finally
447+
{
448+
Marshal.FreeHGlobal(credentialBlob);
449+
}
450+
}
405451

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

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

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

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)