tabby-rdp 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +94 -5
- package/dist/accountForm.js +4 -3
- package/dist/accountForm.js.map +1 -1
- package/dist/desktop.service.js +454 -54
- package/dist/desktop.service.js.map +1 -1
- package/dist/desktopForm.js +26 -1
- package/dist/desktopForm.js.map +1 -1
- package/dist/desktops.js +17 -1
- package/dist/desktops.js.map +1 -1
- package/dist/drives.js +284 -0
- package/dist/drives.js.map +1 -0
- package/dist/fileTransfer.js +128 -11
- package/dist/fileTransfer.js.map +1 -1
- package/dist/gateway.js +492 -0
- package/dist/gateway.js.map +1 -0
- package/dist/hyperv.js +79 -0
- package/dist/hyperv.js.map +1 -0
- package/dist/ntlm.js +226 -0
- package/dist/ntlm.js.map +1 -0
- package/dist/rdcleanpath.js +167 -26
- package/dist/rdcleanpath.js.map +1 -1
- package/dist/rdpFile.js +34 -0
- package/dist/rdpFile.js.map +1 -1
- package/dist/rdpProfile.js +39 -4
- package/dist/rdpProfile.js.map +1 -1
- package/dist/settingsPage.js +107 -17
- package/dist/settingsPage.js.map +1 -1
- package/dist/signin.js +48 -17
- package/dist/signin.js.map +1 -1
- package/dist/sshChannelStream.js +11 -1
- package/dist/sshChannelStream.js.map +1 -1
- package/dist/ui.js +10 -0
- package/dist/ui.js.map +1 -1
- package/dist/vms.js +20 -3
- package/dist/vms.js.map +1 -1
- package/dist/wake.js +16 -4
- package/dist/wake.js.map +1 -1
- package/package.json +1 -1
- package/vendor/iron-remote-desktop-rdp.js +418 -390
- package/vendor/ironrdp_web_bg.wasm +0 -0
package/dist/desktop.service.js
CHANGED
|
@@ -24,6 +24,7 @@ const deskScript_1 = require("./deskScript");
|
|
|
24
24
|
const audio_1 = require("./audio");
|
|
25
25
|
const connectionStatus_1 = require("./connectionStatus");
|
|
26
26
|
const microphone_1 = require("./microphone");
|
|
27
|
+
const drives_1 = require("./drives");
|
|
27
28
|
const desktopForm_1 = require("./desktopForm");
|
|
28
29
|
const accounts_1 = require("./accounts");
|
|
29
30
|
const accountForm_1 = require("./accountForm");
|
|
@@ -37,6 +38,8 @@ const rdcleanpath_1 = require("./rdcleanpath");
|
|
|
37
38
|
const signin_1 = require("./signin");
|
|
38
39
|
const ssh_1 = require("./ssh");
|
|
39
40
|
const wake_1 = require("./wake");
|
|
41
|
+
const hyperv_1 = require("./hyperv");
|
|
42
|
+
const gateway_1 = require("./gateway");
|
|
40
43
|
const vms_1 = require("./vms");
|
|
41
44
|
/** How long a desktop that was started (`wake`) gets to answer. */
|
|
42
45
|
const WAKE_TIMEOUT_MS = 3 * 60 * 1000;
|
|
@@ -139,6 +142,13 @@ function remoteSizeFor(rect, settings) {
|
|
|
139
142
|
};
|
|
140
143
|
}
|
|
141
144
|
class DesktopSession {
|
|
145
|
+
/**
|
|
146
|
+
* What a sign-in and a certificate are remembered under: the desktop; for a Hyper-V VM, its host, whose account
|
|
147
|
+
* and certificate they are, the same for all of its VMs.
|
|
148
|
+
*/
|
|
149
|
+
get authKey() {
|
|
150
|
+
return this.spec.hyperv ? this.key.replace(/#hyperv:[^#]*$/, '#hyperv') : this.key;
|
|
151
|
+
}
|
|
142
152
|
/** `key`: see sessionKey(); one desktop per key. */
|
|
143
153
|
constructor(container, key, spec) {
|
|
144
154
|
this.key = key;
|
|
@@ -157,6 +167,8 @@ class DesktopSession {
|
|
|
157
167
|
this.mic = null;
|
|
158
168
|
/** Files through the clipboard (drop on the desktop, or copy on the remote). */
|
|
159
169
|
this.files = null;
|
|
170
|
+
/** The shared folders as drives on the remote (see drives.ts), when there were any at connect. */
|
|
171
|
+
this.drives = null;
|
|
160
172
|
/** Decodes H.264 for the graphics pipeline (IronRDP's WebCodecsH264Decoder), when this connection uses it. */
|
|
161
173
|
this.h264 = null;
|
|
162
174
|
/** No keyboard or mouse input goes to the remote (keys: see keyboard.ts; the mouse: a layer over the picture). */
|
|
@@ -165,12 +177,18 @@ class DesktopSession {
|
|
|
165
177
|
this.pinnedCertificate = null;
|
|
166
178
|
/** Set when the proxy refused the server's certificate on the current connection attempt. */
|
|
167
179
|
this.certificateProblem = null;
|
|
180
|
+
/** Set when the desktop's RD Gateway refused the sign-in on the current connection attempt: what it said. */
|
|
181
|
+
this.gatewayRefused = null;
|
|
182
|
+
/** Set when the proxy stopped at a server without Network Level Authentication on the current connection attempt. */
|
|
183
|
+
this.withoutNla = false;
|
|
168
184
|
/** The connection-status indicator, made the first time it shows. */
|
|
169
185
|
this.indicator = null;
|
|
170
186
|
/** How the pictures come (for the indicator): the graphics pipeline, bitmaps, or IronRDP's choice. */
|
|
171
187
|
this.graphics = '';
|
|
172
188
|
/** "What does this mean?" under a message (see status()): opens help about it. */
|
|
173
189
|
this.onHelp = null;
|
|
190
|
+
/** Keys this desktop was sent and not yet let go of (by `code`): see RemoteDesktopService.broadcastKey. */
|
|
191
|
+
this.keysDown = new Map();
|
|
174
192
|
this.reclaimFocus = (event) => {
|
|
175
193
|
if (this.visible && !this.overlay.contains(event.target)) {
|
|
176
194
|
setTimeout(() => this.visible && this.focusDesktop());
|
|
@@ -204,6 +222,30 @@ class DesktopSession {
|
|
|
204
222
|
this.host.querySelector('iron-remote-desktop')?.shadowRoot?.querySelector('.screen-wrapper')?.scrollBy(wheel.deltaX, wheel.deltaY);
|
|
205
223
|
}, { passive: true });
|
|
206
224
|
}
|
|
225
|
+
/** Notes a key sent to this desktop as held, or let go. */
|
|
226
|
+
noteKey(type, init) {
|
|
227
|
+
if (type === 'keydown' && init.code) {
|
|
228
|
+
this.keysDown.set(init.code, init);
|
|
229
|
+
}
|
|
230
|
+
else if (type === 'keyup' && init.code) {
|
|
231
|
+
this.keysDown.delete(init.code);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Lets go of every key still held on this desktop. For when their keyups won't reach it: view only coming on, or
|
|
236
|
+
* typing into all going off, by a shortcut whose modifiers are down at that moment. They would stay down there.
|
|
237
|
+
*/
|
|
238
|
+
releaseKeys() {
|
|
239
|
+
for (const [code, init] of [...this.keysDown]) {
|
|
240
|
+
this.keysDown.delete(code);
|
|
241
|
+
try {
|
|
242
|
+
this.ui?.sendKeyboardEvent(new KeyboardEvent('keyup', { key: init.key, code, location: init.location, cancelable: true }));
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
this.log.push(`keys: ${code} not released: ${e?.message ?? e}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
207
249
|
setViewOnly(viewOnly) {
|
|
208
250
|
this.viewOnly = viewOnly;
|
|
209
251
|
this.overlay.classList.toggle('trd-view-only-on', viewOnly);
|
|
@@ -291,6 +333,7 @@ class DesktopSession {
|
|
|
291
333
|
this.h264?.close();
|
|
292
334
|
this.mic?.close();
|
|
293
335
|
this.files?.dispose();
|
|
336
|
+
this.drives?.dispose();
|
|
294
337
|
this.proxy?.close();
|
|
295
338
|
this.overlay.remove();
|
|
296
339
|
}
|
|
@@ -329,7 +372,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
329
372
|
* with a wrong password it shows its own login window rather than refusing, so the form never comes back.
|
|
330
373
|
*/
|
|
331
374
|
this.askAgain = new WeakSet();
|
|
332
|
-
/** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops. */
|
|
375
|
+
/** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops; and what the scans learnt of the host. */
|
|
333
376
|
this.vms = new Map();
|
|
334
377
|
this.lastUsed = new Map();
|
|
335
378
|
/** Automatic reconnection after a desktop dropped, per pane: attempts so far and the pending timer. */
|
|
@@ -399,7 +442,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
399
442
|
async signInAgain(pane) {
|
|
400
443
|
const session = this.sessions.get(pane);
|
|
401
444
|
if (session && this.signsIn(session.spec)) {
|
|
402
|
-
await (0, signin_1.forgetCredentials)(session.
|
|
445
|
+
await (0, signin_1.forgetCredentials)(session.authKey);
|
|
403
446
|
// With a saved account, the form asks for its password anew (the saved one stays until then).
|
|
404
447
|
this.askAgain.add(pane);
|
|
405
448
|
await this.reopen(pane, session.spec);
|
|
@@ -413,9 +456,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
413
456
|
if (target.direct) {
|
|
414
457
|
return [target.direct];
|
|
415
458
|
}
|
|
459
|
+
// With their group's defaults (an account for the whole group), as Tabby resolves a profile when opening it.
|
|
416
460
|
const viaProfile = target.profileId
|
|
417
461
|
? (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && p.options?.host && p.options.via === target.profileId)
|
|
418
|
-
.map((p) => ({ ...p.options, name: p.name }))
|
|
462
|
+
.map((p) => ({ ...this.profiles.getConfigProxyForProfile(p).options, name: p.name }))
|
|
419
463
|
: [];
|
|
420
464
|
const specs = (0, desktops_1.desktopsFor)(target, this.config.store.remoteDesktop?.desktops, this.ownDesktops.get(target.key), viaProfile);
|
|
421
465
|
// Found VMs come last, and not where a configured desktop has the same address.
|
|
@@ -437,13 +481,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
437
481
|
if (known && Date.now() - known.at < 60000) {
|
|
438
482
|
return Promise.resolve();
|
|
439
483
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
484
|
+
const host = known?.host ?? { windows: this.ownDesktops.get(target.key)?.kind === 'windows' || undefined };
|
|
485
|
+
const scan = (0, vms_1.scanVMs)(target, host).then(found => {
|
|
486
|
+
this.vms.set(target.key, { at: Date.now(), specs: found.map(vms_1.vmSpec), host });
|
|
442
487
|
this.changed$.next();
|
|
443
488
|
}, () => {
|
|
444
|
-
|
|
489
|
+
// Not asked, rather than nothing found (the connection wasn't up yet, say): the next look asks again.
|
|
490
|
+
this.vms.set(target.key, { at: known?.at ?? 0, specs: known?.specs ?? [], host });
|
|
445
491
|
});
|
|
446
|
-
this.vms.set(target.key, { at: known?.at ?? 0, specs: known?.specs ?? [], scan });
|
|
492
|
+
this.vms.set(target.key, { at: known?.at ?? 0, specs: known?.specs ?? [], scan, host });
|
|
447
493
|
return scan;
|
|
448
494
|
}
|
|
449
495
|
/** Keeps a found VM open in the pane among the host's desktops (`remoteDesktop.desktops`), under its name. */
|
|
@@ -453,7 +499,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
453
499
|
if (!session?.remote || !spec?.found) {
|
|
454
500
|
return;
|
|
455
501
|
}
|
|
456
|
-
const entry = { name: spec.name, via: session.remote.label,
|
|
502
|
+
const entry = spec.hyperv ? { name: spec.name, via: session.remote.label, hyperv: spec.hyperv }
|
|
503
|
+
: { name: spec.name, via: session.remote.label, host: spec.host, port: spec.port, kind: spec.kind, ...spec.wake ? { wake: spec.wake } : {} };
|
|
457
504
|
this.config.store.remoteDesktop.desktops = [...this.configuredDesktops(), entry];
|
|
458
505
|
this.config.save();
|
|
459
506
|
this.changed$.next();
|
|
@@ -490,15 +537,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
490
537
|
return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
|
|
491
538
|
}
|
|
492
539
|
/**
|
|
493
|
-
* The desktops that sign in with a saved account
|
|
494
|
-
* choice, or their profile group's default when they make none). Each comes
|
|
540
|
+
* The desktops that sign in with a saved account, or sign in to their RD Gateway with it: configured ones behind
|
|
541
|
+
* SSH hosts, and RDP profiles (their own choice, or their profile group's default when they make none). Each comes
|
|
542
|
+
* with what opens its editor.
|
|
495
543
|
*/
|
|
496
544
|
accountUses(id) {
|
|
497
|
-
const desktops = this.configuredDesktops().flatMap((d, i) => d.account === id ? [{ name: String(d.name ?? (0, desktops_1.desktopIdOf)(d)), desktopIndex: i }] : []);
|
|
545
|
+
const desktops = this.configuredDesktops().flatMap((d, i) => d.account === id || d.gatewayAccount === id ? [{ name: String(d.name ?? (0, desktops_1.desktopIdOf)(d)), desktopIndex: i }] : []);
|
|
498
546
|
const groups = this.config.store.groups ?? [];
|
|
499
547
|
const inherited = (p) => p.options?.account === undefined && groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account === id;
|
|
500
548
|
const profiles = (this.config.store.profiles ?? [])
|
|
501
|
-
.filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && (p.options?.account === id || inherited(p)))
|
|
549
|
+
.filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && (p.options?.account === id || p.options?.gatewayAccount === id || inherited(p)))
|
|
502
550
|
.map((p) => ({ name: String(p.name ?? p.options?.host ?? ''), profileId: p.id ? String(p.id) : undefined }));
|
|
503
551
|
return [...desktops, ...profiles];
|
|
504
552
|
}
|
|
@@ -516,16 +564,32 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
516
564
|
username: account.username.trim(),
|
|
517
565
|
...account.domain?.trim() ? { domain: account.domain.trim() } : {},
|
|
518
566
|
};
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
this.changed$.next();
|
|
567
|
+
// The password first: a new user name must not be written while the old password (for the old name) stays, nor
|
|
568
|
+
// a new password reported as saved when the store refused it.
|
|
522
569
|
let keychainError;
|
|
523
570
|
if (password !== undefined && password !== '') {
|
|
524
|
-
await (0, signin_1.saveCredentials)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }).catch(e => { keychainError = String(e?.message ?? e); });
|
|
571
|
+
await (0, signin_1.saveCredentials)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }, `saved account ${entry.name}`).catch(e => { keychainError = String(e?.message ?? e); });
|
|
525
572
|
}
|
|
526
573
|
else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
|
|
527
|
-
await (0, signin_1.
|
|
574
|
+
await (0, signin_1.forgetCredentialsOrFail)((0, accounts_1.accountKey)(entry.id)).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
|
|
528
575
|
}
|
|
576
|
+
if (keychainError && !old) {
|
|
577
|
+
// Not added at all: a retry would otherwise make a second account.
|
|
578
|
+
return { id: entry.id, keychainError: `${keychainError}. The account wasn't added` };
|
|
579
|
+
}
|
|
580
|
+
if (keychainError && old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
|
|
581
|
+
// The sign-in name stays as it was; the account's name can still change.
|
|
582
|
+
entry.username = old.username;
|
|
583
|
+
if (old.domain) {
|
|
584
|
+
entry.domain = old.domain;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
delete entry.domain;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.config.store.remoteDesktop.accounts = old ? list.map(a => a === old ? entry : a) : [...list, entry];
|
|
591
|
+
await this.config.save();
|
|
592
|
+
this.changed$.next();
|
|
529
593
|
return { id: entry.id, keychainError };
|
|
530
594
|
}
|
|
531
595
|
/** A desktop form's "New account…": saves it and hands it back for the form's list. */
|
|
@@ -567,12 +631,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
567
631
|
async removeAccount(id) {
|
|
568
632
|
const store = this.config.store.remoteDesktop;
|
|
569
633
|
store.accounts = this.accounts().filter(a => a.id !== id);
|
|
570
|
-
if (this.configuredDesktops().some(d => d.account === id)) {
|
|
634
|
+
if (this.configuredDesktops().some(d => d.account === id || d.gatewayAccount === id)) {
|
|
571
635
|
store.desktops = this.configuredDesktops().map(d => {
|
|
572
|
-
|
|
573
|
-
|
|
636
|
+
const rest = { ...d };
|
|
637
|
+
if (rest.account === id) {
|
|
638
|
+
delete rest.account;
|
|
639
|
+
}
|
|
640
|
+
// A gateway that signed in with it takes the desktop's sign-in from now on.
|
|
641
|
+
if (rest.gatewayAccount === id) {
|
|
642
|
+
delete rest.gatewayAccount;
|
|
574
643
|
}
|
|
575
|
-
const { account: _account, ...rest } = d;
|
|
576
644
|
return rest;
|
|
577
645
|
});
|
|
578
646
|
}
|
|
@@ -580,10 +648,20 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
580
648
|
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.account === id) {
|
|
581
649
|
profile.options.account = '';
|
|
582
650
|
}
|
|
651
|
+
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.gatewayAccount === id) {
|
|
652
|
+
profile.options.gatewayAccount = '';
|
|
653
|
+
}
|
|
583
654
|
}
|
|
584
|
-
|
|
655
|
+
// A profile group's default for remote desktop profiles, too.
|
|
656
|
+
for (const group of this.config.store.groups ?? []) {
|
|
657
|
+
const options = group?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options;
|
|
658
|
+
if (options?.account === id) {
|
|
659
|
+
options.account = '';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
await this.config.save();
|
|
585
663
|
this.changed$.next();
|
|
586
|
-
await (0, signin_1.
|
|
664
|
+
await (0, signin_1.forgetCredentialsOrFail)((0, accounts_1.accountKey)(id)).catch(e => this.notifications.error(`The account is removed, but its password could not be: ${e?.message ?? e}`));
|
|
587
665
|
}
|
|
588
666
|
/** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
|
|
589
667
|
configuredDesktops() {
|
|
@@ -704,7 +782,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
704
782
|
}
|
|
705
783
|
const moved = (key) => direct ? key === `${desktops_1.DIRECT_KEY}#${fromId}` : key.endsWith(`#${fromId}`) && !key.startsWith(`${desktops_1.DIRECT_KEY}#`);
|
|
706
784
|
const store = this.config.store.remoteDesktop;
|
|
707
|
-
for (const list of ['desktopSharpness', 'trustedCertificates']) {
|
|
785
|
+
for (const list of ['desktopSharpness', 'trustedCertificates', 'withoutNla']) {
|
|
708
786
|
if (Array.isArray(store[list])) {
|
|
709
787
|
store[list] = store[list].map((e) => typeof e?.desktop === 'string' && moved(e.desktop)
|
|
710
788
|
? { ...e, desktop: `${e.desktop.slice(0, -fromId.length)}${toId}` }
|
|
@@ -760,23 +838,53 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
760
838
|
return null;
|
|
761
839
|
}
|
|
762
840
|
const existing = (this.config.store.profiles ?? []).find((p) => p?.type === targets_1.RDP_PROFILE_TYPE && !p.options?.via &&
|
|
763
|
-
p.options?.host === parsed.host && (p.options.port || 3389) === parsed.port && (p.options.username ?? '') === (parsed.username ?? '')
|
|
841
|
+
p.options?.host === parsed.host && (p.options.port || 3389) === parsed.port && (p.options.username ?? '') === (parsed.username ?? '') &&
|
|
842
|
+
(p.options.gateway ?? '') === (parsed.gateway ?? ''));
|
|
764
843
|
if (existing) {
|
|
765
844
|
this.notifications.info(`Opening "${existing.name}", which is already a profile for ${fileName}`);
|
|
766
845
|
await this.profiles.openNewTabForProfile(existing);
|
|
767
846
|
return existing;
|
|
768
847
|
}
|
|
848
|
+
// A file is someone else's word for where to connect: where it leads is said before anything goes there. The
|
|
849
|
+
// account typed into the sign-in form goes to that address, and to the gateway before it.
|
|
850
|
+
const base = fileName.replace(/^.*[\\/]/, '');
|
|
851
|
+
const { response } = await this.platform.showMessageBox({
|
|
852
|
+
type: 'warning',
|
|
853
|
+
message: `Add a remote desktop from ${base}?`,
|
|
854
|
+
detail: `It connects to ${(0, desktopForm_1.formatAddress)(parsed.host, parsed.port)}${parsed.gateway ? `, through the gateway ${parsed.gateway}` : ''}` +
|
|
855
|
+
`${parsed.username ? `, as ${parsed.username}` : ''}. What you sign in with there goes to that address` +
|
|
856
|
+
`${parsed.gateway ? ' and that gateway' : ''}, and once signed in, the desktop gets the clipboard and the folders you share. ` +
|
|
857
|
+
'Add it only if you know where it leads, or trust who gave you the file.',
|
|
858
|
+
buttons: ['Add and connect', 'Add only', 'Cancel'],
|
|
859
|
+
defaultId: 0,
|
|
860
|
+
cancelId: 2,
|
|
861
|
+
});
|
|
862
|
+
if (response !== 0 && response !== 1) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
769
865
|
const profile = {
|
|
770
866
|
type: targets_1.RDP_PROFILE_TYPE,
|
|
771
|
-
name:
|
|
867
|
+
name: base.replace(/\.rdp$/i, '') || parsed.host,
|
|
772
868
|
icon: 'fas fa-desktop',
|
|
773
869
|
group: await this.remoteDesktopGroup(),
|
|
774
|
-
options: { host: parsed.host, port: parsed.port, kind: 'windows', username: parsed.username ?? '', domain: parsed.domain ?? '', via: '' },
|
|
870
|
+
options: { host: parsed.host, port: parsed.port, kind: 'windows', username: parsed.username ?? '', domain: parsed.domain ?? '', via: '', gateway: parsed.gateway ?? '', gatewayAccount: '' },
|
|
775
871
|
};
|
|
776
872
|
await this.profiles.newProfile(profile);
|
|
873
|
+
// The file's display scale, as this desktop's own sharpness (the key a direct desktop's settings are kept by).
|
|
874
|
+
if (parsed.sharpness) {
|
|
875
|
+
const store = this.config.store.remoteDesktop;
|
|
876
|
+
const key = `${desktops_1.DIRECT_KEY}#${(0, desktops_1.desktopIdOf)(profile.options)}`;
|
|
877
|
+
store.desktopSharpness = [...(Array.isArray(store.desktopSharpness) ? store.desktopSharpness : []).filter((e) => e?.desktop !== key), { desktop: key, sharpness: parsed.sharpness }];
|
|
878
|
+
}
|
|
777
879
|
this.config.save();
|
|
778
880
|
this.notifications.notice(`Added the remote desktop profile "${profile.name}" (Settings › Profiles & connections)`);
|
|
779
|
-
|
|
881
|
+
if (parsed.ignored.length) {
|
|
882
|
+
// What the file asked for beyond that, so nobody wonders why the desktop doesn't behave as in mstsc.
|
|
883
|
+
this.notifications.info(`${base} also asks for ${parsed.ignored.join('; ')}. Not applied.`);
|
|
884
|
+
}
|
|
885
|
+
if (response === 0) {
|
|
886
|
+
await this.profiles.openNewTabForProfile(profile);
|
|
887
|
+
}
|
|
780
888
|
return profile;
|
|
781
889
|
}
|
|
782
890
|
/** Asks, then removes a configured desktop (see removeDesktop). The menus and the settings page use this. */
|
|
@@ -1095,6 +1203,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1095
1203
|
sharpness: store.sharpness === 'retina' ? 'retina' : 'standard',
|
|
1096
1204
|
desk: store.desk === true,
|
|
1097
1205
|
macShortcuts: store.macShortcuts !== false,
|
|
1206
|
+
unicodeKeys: store.unicodeKeys === true,
|
|
1098
1207
|
sound: store.sound !== false,
|
|
1099
1208
|
h264: store.h264 !== false,
|
|
1100
1209
|
zoom: store.zoom === 'actual' ? 'actual' : 'fit',
|
|
@@ -1106,6 +1215,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1106
1215
|
shutDownIdle: [5, 15, 60].includes(Number(store.shutDownIdle)) ? Number(store.shutDownIdle) : 0,
|
|
1107
1216
|
};
|
|
1108
1217
|
}
|
|
1218
|
+
/** The folders shared with remote desktops as drives (`remoteDesktop.sharedFolders`), tidied. */
|
|
1219
|
+
sharedFolders() {
|
|
1220
|
+
return (0, drives_1.sharedFolders)(this.config.store.remoteDesktop);
|
|
1221
|
+
}
|
|
1222
|
+
/** Replaces the shared folders; applies to desktops connecting from now on. */
|
|
1223
|
+
setSharedFolders(folders) {
|
|
1224
|
+
this.config.store.remoteDesktop.sharedFolders = folders.map(f => ({ path: f.path, name: f.name, readOnly: f.readOnly }));
|
|
1225
|
+
this.config.save();
|
|
1226
|
+
this.changed$.next();
|
|
1227
|
+
}
|
|
1109
1228
|
/** The sharpness chosen for the pane's desktop in particular (`remoteDesktop.desktopSharpness`), or null. */
|
|
1110
1229
|
ownSharpness(pane) {
|
|
1111
1230
|
const key = this.sessions.get(pane)?.key;
|
|
@@ -1152,10 +1271,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1152
1271
|
this.config.save();
|
|
1153
1272
|
for (const [pane, session] of this.sessions) {
|
|
1154
1273
|
this.applyZoom(session);
|
|
1274
|
+
this.applyKeyboardMode(session);
|
|
1155
1275
|
this.syncIndicator(session);
|
|
1156
1276
|
this.followPane(pane, session);
|
|
1157
1277
|
}
|
|
1158
1278
|
}
|
|
1279
|
+
/** IronRDP's keyboard mode for the session: characters (Unicode) or key positions (scancodes), per the setting. */
|
|
1280
|
+
applyKeyboardMode(session) {
|
|
1281
|
+
try {
|
|
1282
|
+
session.ui?.setKeyboardUnicodeMode(this.settings().unicodeKeys);
|
|
1283
|
+
}
|
|
1284
|
+
catch { }
|
|
1285
|
+
}
|
|
1159
1286
|
/** IronRDP's ScreenScale for the picture: Real (1:1, scrolling) only with a fixed resolution and zoom 'actual'. */
|
|
1160
1287
|
screenScale() {
|
|
1161
1288
|
const settings = this.settings();
|
|
@@ -1266,6 +1393,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1266
1393
|
setViewOnly(pane, viewOnly) {
|
|
1267
1394
|
const session = this.sessions.get(pane);
|
|
1268
1395
|
if (session) {
|
|
1396
|
+
if (viewOnly) {
|
|
1397
|
+
// From here on this desktop's keys stop at the plugin, their keyups too: let go of what is down, also
|
|
1398
|
+
// on the desktops typing into all had copy them.
|
|
1399
|
+
session.releaseKeys();
|
|
1400
|
+
if (this.isBroadcast(pane)) {
|
|
1401
|
+
this.othersInTab(pane).forEach(other => other.session.releaseKeys());
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1269
1404
|
session.setViewOnly(viewOnly);
|
|
1270
1405
|
this.changed$.next();
|
|
1271
1406
|
}
|
|
@@ -1403,9 +1538,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1403
1538
|
forgetCertificate(key) {
|
|
1404
1539
|
const store = this.config.store.remoteDesktop;
|
|
1405
1540
|
store.trustedCertificates = (Array.isArray(store.trustedCertificates) ? store.trustedCertificates : []).filter((e) => e?.desktop !== key);
|
|
1541
|
+
// What else was decided about that server goes with it: it is asked again.
|
|
1542
|
+
store.withoutNla = (Array.isArray(store.withoutNla) ? store.withoutNla : []).filter((e) => e?.desktop !== key);
|
|
1406
1543
|
this.config.save();
|
|
1407
1544
|
this.changed$.next();
|
|
1408
1545
|
}
|
|
1546
|
+
/** Whether the desktop was allowed to sign in without Network Level Authentication (see refusedWithoutNla). */
|
|
1547
|
+
signsInWithoutNla(key) {
|
|
1548
|
+
return this.allowedWithoutNla(key);
|
|
1549
|
+
}
|
|
1409
1550
|
/** The split tab holding the pane, if it has more than one pane. */
|
|
1410
1551
|
splitOf(pane) {
|
|
1411
1552
|
const split = this.app.tabs.find(t => t instanceof tabby_core_1.SplitTabComponent && t.getAllTabs().includes(pane));
|
|
@@ -1432,6 +1573,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1432
1573
|
if (!split) {
|
|
1433
1574
|
return;
|
|
1434
1575
|
}
|
|
1576
|
+
if (!on && this.broadcast.has(split)) {
|
|
1577
|
+
// The keyups of keys held right now (the shortcut's own modifiers) won't be copied any more.
|
|
1578
|
+
this.othersInTab(pane).forEach(other => other.session.releaseKeys());
|
|
1579
|
+
}
|
|
1435
1580
|
on ? this.broadcast.add(split) : this.broadcast.delete(split);
|
|
1436
1581
|
for (const p of split.getAllTabs()) {
|
|
1437
1582
|
const session = this.sessions.get(p);
|
|
@@ -1440,14 +1585,19 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1440
1585
|
}
|
|
1441
1586
|
this.changed$.next();
|
|
1442
1587
|
}
|
|
1443
|
-
/**
|
|
1588
|
+
/**
|
|
1589
|
+
* A key the pane's desktop was sent (see DesktopKeyboard): noted as held there or let go, and copied to the other
|
|
1590
|
+
* desktops of a tab typing into all.
|
|
1591
|
+
*/
|
|
1444
1592
|
broadcastKey(pane, type, init) {
|
|
1593
|
+
this.sessions.get(pane)?.noteKey(type, init);
|
|
1445
1594
|
if (!this.isBroadcast(pane)) {
|
|
1446
1595
|
return;
|
|
1447
1596
|
}
|
|
1448
1597
|
for (const { session } of this.othersInTab(pane)) {
|
|
1449
1598
|
try {
|
|
1450
1599
|
session.ui.sendKeyboardEvent(new KeyboardEvent(type, { ...init, cancelable: true }));
|
|
1600
|
+
session.noteKey(type, init);
|
|
1451
1601
|
}
|
|
1452
1602
|
catch (e) {
|
|
1453
1603
|
session.log.push(`typing into all desktops: ${e?.message ?? e}`);
|
|
@@ -1577,6 +1727,20 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1577
1727
|
spec.port = endpoint.port;
|
|
1578
1728
|
spec.username ??= endpoint.username;
|
|
1579
1729
|
}
|
|
1730
|
+
// A Hyper-V VM's console: which session the guest takes right now (enhanced once its Remote Desktop Services
|
|
1731
|
+
// are up, the basic console before that and for guests that have none). The sign-in is the host's.
|
|
1732
|
+
let hyperv;
|
|
1733
|
+
if (spec.hyperv) {
|
|
1734
|
+
session.status(`Connecting to ${spec.name} via ${target.label}…`);
|
|
1735
|
+
const state = await (0, hyperv_1.hyperVState)(target, spec.hyperv);
|
|
1736
|
+
if (!state.running) {
|
|
1737
|
+
throw new Error(`${spec.name} isn't running`);
|
|
1738
|
+
}
|
|
1739
|
+
hyperv = state.enhanced ? 'enhanced' : 'basic';
|
|
1740
|
+
session.log.push(`hyper-v: ${hyperv === 'enhanced' ? 'an enhanced session' : 'the basic console (the guest takes no enhanced session now)'}`);
|
|
1741
|
+
// The SSH user is who to try first: it got to list the VMs.
|
|
1742
|
+
spec.username ??= target.key.replace(/@[^@]*$/, '') || undefined;
|
|
1743
|
+
}
|
|
1580
1744
|
// A saved account (Settings): its user name, and its password from the keychain. One that has none yet, or that
|
|
1581
1745
|
// this desktop just refused, is asked for here and saved for every desktop that uses the account.
|
|
1582
1746
|
const account = spec.account ? this.accounts().find(a => a.id === spec.account) : undefined;
|
|
@@ -1584,17 +1748,24 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1584
1748
|
session.log.push('sign-in: its saved account no longer exists; asking');
|
|
1585
1749
|
}
|
|
1586
1750
|
const again = this.askAgain.delete(pane);
|
|
1587
|
-
|
|
1751
|
+
// A desktop whose account is gone asks, rather than falling back to a login of its own saved earlier.
|
|
1752
|
+
let saved = retryError || again || spec.account && !account ? null : await (0, signin_1.loadCredentials)(account ? (0, accounts_1.accountKey)(account.id) : session.authKey);
|
|
1753
|
+
if (saved && account && saved.username !== (0, accounts_1.signInName)(account)) {
|
|
1754
|
+
// Kept for an earlier user name of the account (its change didn't reach the store): not this one's.
|
|
1755
|
+
session.log.push(`sign-in: the account's saved password is for ${saved.username}, not ${(0, accounts_1.signInName)(account)}; asking`);
|
|
1756
|
+
saved = null;
|
|
1757
|
+
}
|
|
1588
1758
|
if (saved) {
|
|
1589
1759
|
if (account) {
|
|
1590
1760
|
session.log.push(`sign-in: the saved account "${account.name}"`);
|
|
1591
1761
|
}
|
|
1592
|
-
return { host: spec.host, port: spec.port, credentials: account ? { username: (0, accounts_1.signInName)(account), password: saved.password } : saved, remember: false };
|
|
1762
|
+
return { host: spec.host, port: spec.port, credentials: account ? { username: (0, accounts_1.signInName)(account), password: saved.password } : saved, remember: false, domain: account ? account.domain ?? '' : undefined, hyperv };
|
|
1593
1763
|
}
|
|
1594
1764
|
session.status('');
|
|
1595
1765
|
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
1596
1766
|
// The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via".
|
|
1597
|
-
title: spec.
|
|
1767
|
+
title: spec.hyperv ? `Sign in to ${target.label}, to open ${spec.name}`
|
|
1768
|
+
: spec.id === desktops_1.OWN_DESKTOP || target.direct || spec.kind === 'xrdp' && spec.host === '127.0.0.1' ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${target.label})`,
|
|
1598
1769
|
username: account ? (0, accounts_1.signInName)(account) : spec.username,
|
|
1599
1770
|
error: retryError,
|
|
1600
1771
|
canRemember: true,
|
|
@@ -1604,7 +1775,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1604
1775
|
setTimeout(() => session.visible && session.focusDesktop());
|
|
1605
1776
|
}
|
|
1606
1777
|
const entered = await asked;
|
|
1607
|
-
return entered && {
|
|
1778
|
+
return entered && {
|
|
1779
|
+
host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
|
|
1780
|
+
saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
|
|
1781
|
+
hyperv,
|
|
1782
|
+
};
|
|
1608
1783
|
}
|
|
1609
1784
|
async connect(pane, target, spec, session) {
|
|
1610
1785
|
const alive = () => this.sessions.get(pane) === session;
|
|
@@ -1615,7 +1790,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1615
1790
|
let woken = false;
|
|
1616
1791
|
let startupRetries = 0;
|
|
1617
1792
|
let endpoint = null;
|
|
1793
|
+
let gatewayError;
|
|
1618
1794
|
try {
|
|
1795
|
+
const gateway = spec.gateway ? (0, gateway_1.parseGateway)(spec.gateway) : null;
|
|
1796
|
+
if (spec.gateway && !gateway) {
|
|
1797
|
+
throw new Error(`"${spec.gateway}" isn't a gateway's address (a name or address, or name:port)`);
|
|
1798
|
+
}
|
|
1619
1799
|
if (spec.wake) {
|
|
1620
1800
|
const woke = await this.wakeIfDown(pane, target, spec, session, automatic);
|
|
1621
1801
|
if (woke === null) {
|
|
@@ -1646,7 +1826,23 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1646
1826
|
endpoint = asked;
|
|
1647
1827
|
session.remote = target;
|
|
1648
1828
|
session.pinnedCertificate = asked.certificate ?? null;
|
|
1649
|
-
|
|
1829
|
+
// A gateway with a saved account of its own: its password, asked for when there is none or it was refused.
|
|
1830
|
+
if (gateway && spec.gatewayAccount && !asked.gatewayAccount) {
|
|
1831
|
+
const signIn = await this.gatewayAccountFor(spec, session, gateway, gatewayError);
|
|
1832
|
+
if (!alive()) {
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (signIn === null) {
|
|
1836
|
+
session.state = 'ended';
|
|
1837
|
+
this.changed$.next();
|
|
1838
|
+
this.cancelReconnect(pane);
|
|
1839
|
+
session.status('Sign-in cancelled.', [{ label: 'Sign in', run: () => this.reopen(pane, spec) }]);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
asked.gatewayAccount = signIn;
|
|
1843
|
+
}
|
|
1844
|
+
// The proxy outlives an attempt: it reads the sign-in of the attempt it serves.
|
|
1845
|
+
session.proxy ??= await (0, rdcleanpath_1.startRDCleanPathProxy)(() => gateway ? this.throughGateway(target, spec, session, gateway, endpoint) : target.openTcp(asked.host, asked.port), fingerprint => this.checkCertificate(session, fingerprint), m => session.log.push(m), { autologon: spec.kind === 'xrdp', withoutNla: () => this.checkWithoutNla(session) });
|
|
1650
1846
|
const outcome = await this.run(pane, target, spec, session, rdp, asked);
|
|
1651
1847
|
if (outcome.certificate && alive()) {
|
|
1652
1848
|
// A changed certificate, now trusted: the same account again, without asking for it again.
|
|
@@ -1655,10 +1851,23 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1655
1851
|
}
|
|
1656
1852
|
return;
|
|
1657
1853
|
}
|
|
1854
|
+
if (outcome.withoutNla && alive()) {
|
|
1855
|
+
// Allowed now: the same account again, without asking for it again.
|
|
1856
|
+
if (await this.refusedWithoutNla(pane, target, spec, session)) {
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (outcome.gatewayRefused && asked.gatewayAccount && attempt < 5 && alive()) {
|
|
1862
|
+
// The gateway's own account was refused: asked for again, the desktop's sign-in kept.
|
|
1863
|
+
gatewayError = outcome.signInFailed;
|
|
1864
|
+
asked.gatewayAccount = undefined;
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1658
1867
|
if (outcome.signInFailed && this.signsIn(spec) && attempt < 5 && alive()) {
|
|
1659
1868
|
// The desktop's own saved account goes. A shared one stays until a new password is entered: this
|
|
1660
1869
|
// desktop refusing it doesn't make it wrong for the others.
|
|
1661
|
-
await (0, signin_1.forgetCredentials)(session.
|
|
1870
|
+
await (0, signin_1.forgetCredentials)(session.authKey);
|
|
1662
1871
|
retryError = outcome.signInFailed;
|
|
1663
1872
|
endpoint = null;
|
|
1664
1873
|
continue;
|
|
@@ -1695,6 +1904,147 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1695
1904
|
}
|
|
1696
1905
|
}
|
|
1697
1906
|
}
|
|
1907
|
+
/**
|
|
1908
|
+
* The sign-in to a desktop's RD Gateway with the saved account it names: the account's password from the keychain,
|
|
1909
|
+
* or asked for (when none is saved, or `retryError`: the gateway just refused it). Undefined when that account no
|
|
1910
|
+
* longer exists (the desktop's own sign-in is then tried); null when the form was cancelled.
|
|
1911
|
+
*/
|
|
1912
|
+
async gatewayAccountFor(spec, session, gateway, retryError) {
|
|
1913
|
+
const account = this.accounts().find(a => a.id === spec.gatewayAccount);
|
|
1914
|
+
if (!account) {
|
|
1915
|
+
session.log.push('gateway: its saved account no longer exists; signing in with the desktop\'s');
|
|
1916
|
+
return undefined;
|
|
1917
|
+
}
|
|
1918
|
+
const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account) };
|
|
1919
|
+
const saved = retryError ? null : await (0, signin_1.loadCredentials)(signIn.saveKey);
|
|
1920
|
+
if (saved && saved.username === (0, accounts_1.signInName)(account)) {
|
|
1921
|
+
session.log.push(`gateway: the saved account "${account.name}"`);
|
|
1922
|
+
return { ...signIn, credentials: { username: (0, accounts_1.signInName)(account), password: saved.password }, remember: false };
|
|
1923
|
+
}
|
|
1924
|
+
session.status('');
|
|
1925
|
+
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
1926
|
+
title: `Sign in to the gateway ${gateway.host}, to reach ${spec.name}`,
|
|
1927
|
+
username: (0, accounts_1.signInName)(account),
|
|
1928
|
+
error: retryError,
|
|
1929
|
+
canRemember: true,
|
|
1930
|
+
account: account.name,
|
|
1931
|
+
}, session.disposed);
|
|
1932
|
+
if (session.visible) {
|
|
1933
|
+
setTimeout(() => session.visible && session.focusDesktop());
|
|
1934
|
+
}
|
|
1935
|
+
const entered = await asked;
|
|
1936
|
+
return entered && { ...signIn, credentials: entered, remember: entered.remember };
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* The proxy's stream to a desktop behind an RD Gateway (see gateway.ts): to the gateway as the SSH host (or this
|
|
1940
|
+
* computer) reaches it, signed in with the gateway's own account or else the desktop's, then on to the desktop.
|
|
1941
|
+
*/
|
|
1942
|
+
async throughGateway(target, spec, session, gateway, endpoint) {
|
|
1943
|
+
session.gatewayRefused = null;
|
|
1944
|
+
const own = endpoint.gatewayAccount;
|
|
1945
|
+
const credentials = own?.credentials ?? endpoint.credentials;
|
|
1946
|
+
session.log.push(`gateway: ${gateway.host}:${gateway.port}, signing in with ${own ? 'its saved account' : 'the desktop\'s sign-in'}`);
|
|
1947
|
+
try {
|
|
1948
|
+
const stream = await (0, gateway_1.openThroughGateway)(() => target.openTcp(gateway.host, gateway.port), gateway, { username: credentials.username, password: credentials.password, domain: (own ? own.domain : endpoint.domain ?? spec.domain) || undefined }, { host: endpoint.host, port: endpoint.port }, (fingerprint, valid) => this.checkGatewayCertificate(session, gateway, fingerprint, valid), { log: m => session.log.push(m), clientName: os.hostname() });
|
|
1949
|
+
// Entered a moment ago and taken by the gateway: kept, while the account is still what it was signed in as.
|
|
1950
|
+
if (own?.remember) {
|
|
1951
|
+
own.remember = false;
|
|
1952
|
+
const accountNow = this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === own.saveKey);
|
|
1953
|
+
if (accountNow && (0, accounts_1.signInName)(accountNow) === own.saveFor) {
|
|
1954
|
+
(0, signin_1.saveCredentials)(own.saveKey, own.credentials, own.saveLabel).then(() => session.log.push(`gateway: saved as the account's password in ${(0, signin_1.storeName)()}`), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return stream;
|
|
1958
|
+
}
|
|
1959
|
+
catch (e) {
|
|
1960
|
+
if (e instanceof gateway_1.GatewaySignInError) {
|
|
1961
|
+
session.gatewayRefused = e.message;
|
|
1962
|
+
}
|
|
1963
|
+
throw e;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* A gateway's certificate, before the sign-in goes to it. One that is valid for the gateway's name by this
|
|
1968
|
+
* computer's certificate authorities is accepted (a gateway on the internet has one); any other is trusted on
|
|
1969
|
+
* first use, like a desktop's. Either way it is remembered, by the gateway's address: after a valid one, an invalid
|
|
1970
|
+
* one is a change to decide on, not a first use.
|
|
1971
|
+
*/
|
|
1972
|
+
checkGatewayCertificate(session, gateway, raw, valid) {
|
|
1973
|
+
const fingerprint = (0, rdcleanpath_1.normalizeFingerprint)(raw);
|
|
1974
|
+
const address = `${gateway.host}:${gateway.port}`;
|
|
1975
|
+
const key = `gateway#${address}`;
|
|
1976
|
+
const known = this.trustedCertificate(key);
|
|
1977
|
+
if (valid || !known) {
|
|
1978
|
+
if (known !== fingerprint) {
|
|
1979
|
+
this.trustCertificate(key, fingerprint);
|
|
1980
|
+
}
|
|
1981
|
+
session.log.push(`gateway certificate: SHA-256 ${fingerprint}, ${valid ? `valid for ${gateway.host}` : 'remembered (first connection)'}`);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (fingerprint !== known) {
|
|
1985
|
+
session.certificateProblem = { expected: known, actual: fingerprint, pinned: false, gateway: { address, key } };
|
|
1986
|
+
throw new Error(`the gateway's certificate SHA-256 ${fingerprint} is not the one remembered (${known})`);
|
|
1987
|
+
}
|
|
1988
|
+
session.log.push(`gateway certificate: SHA-256 ${fingerprint}, as remembered`);
|
|
1989
|
+
}
|
|
1990
|
+
/** The desktops allowed to sign in without Network Level Authentication (`remoteDesktop.withoutNla`), by session key. */
|
|
1991
|
+
allowedWithoutNla(key) {
|
|
1992
|
+
const list = this.config.store.remoteDesktop?.withoutNla;
|
|
1993
|
+
return (Array.isArray(list) ? list : []).some((e) => e?.desktop === key);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* The proxy found a server that doesn't use Network Level Authentication: the password would go to it as it is,
|
|
1997
|
+
* before the server has shown anything but a certificate. That is how xrdp signs in, so an xrdp desktop goes on.
|
|
1998
|
+
* A Windows machine or GNOME Remote Desktop always uses it unless it was turned off, and something posing as one
|
|
1999
|
+
* (the address in a file someone sent) doesn't: those stop here, until the desktop is allowed (refusedWithoutNla).
|
|
2000
|
+
*/
|
|
2001
|
+
checkWithoutNla(session) {
|
|
2002
|
+
if (session.spec.kind === 'xrdp') {
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
if (this.allowedWithoutNla(session.authKey)) {
|
|
2006
|
+
session.log.push('sign-in: without Network Level Authentication, as allowed for this desktop');
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
session.withoutNla = true;
|
|
2010
|
+
throw new Error('the server doesn\'t use Network Level Authentication');
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* After the proxy stopped at a server without Network Level Authentication; nothing but the connection request
|
|
2014
|
+
* was sent to it. Says what going on would mean and asks; automatic reconnecting stops here. Resolves true to
|
|
2015
|
+
* connect again (the desktop is then allowed, and remembered as such).
|
|
2016
|
+
*/
|
|
2017
|
+
async refusedWithoutNla(pane, target, spec, session) {
|
|
2018
|
+
this.cancelReconnect(pane);
|
|
2019
|
+
const alive = () => this.sessions.get(pane) === session;
|
|
2020
|
+
session.log.push('sign-in: the server doesn\'t use Network Level Authentication; asking before the password goes to it');
|
|
2021
|
+
if (!session.visible) {
|
|
2022
|
+
this.notifications.notice(`${spec.name}${target.direct ? '' : ` (via ${target.label})`} would get your password as it is. Open that desktop to decide.`);
|
|
2023
|
+
}
|
|
2024
|
+
const allowed = await new Promise(resolve => {
|
|
2025
|
+
session.status(`${spec.name} doesn't use Network Level Authentication, so your password would be sent to it as it is: ` +
|
|
2026
|
+
'encrypted on the way, but readable to whatever answered there, before it has proved anything. Nothing has been sent.\n\n' +
|
|
2027
|
+
'A Windows machine asks for Network Level Authentication unless that was turned off on it. A server posing as one doesn\'t, ' +
|
|
2028
|
+
`and neither does xrdp: if ${spec.name} is an xrdp server, set its kind to xrdp instead.`, [
|
|
2029
|
+
{ label: 'Send the password anyway', run: () => resolve(true) },
|
|
2030
|
+
{ label: 'Cancel', run: () => resolve(false) },
|
|
2031
|
+
], true);
|
|
2032
|
+
session.disposed.then(() => resolve(false));
|
|
2033
|
+
});
|
|
2034
|
+
if (!alive()) {
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
if (!allowed) {
|
|
2038
|
+
session.state = 'ended';
|
|
2039
|
+
this.changed$.next();
|
|
2040
|
+
session.status('Not connected: the password was not sent.', [{ label: 'Try again', run: () => this.reopen(pane, spec) }], true);
|
|
2041
|
+
return false;
|
|
2042
|
+
}
|
|
2043
|
+
const store = this.config.store.remoteDesktop;
|
|
2044
|
+
store.withoutNla = [...(Array.isArray(store.withoutNla) ? store.withoutNla : []).filter((e) => e?.desktop !== session.authKey), { desktop: session.authKey }];
|
|
2045
|
+
this.config.save();
|
|
2046
|
+
return true;
|
|
2047
|
+
}
|
|
1698
2048
|
/**
|
|
1699
2049
|
* The proxy's check of the server's certificate, before any credentials go out. The host's own desktop must show
|
|
1700
2050
|
* the certificate its setup reported. Others (behind a host, a Windows host's own, a remote desktop tab's) are
|
|
@@ -1714,9 +2064,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1714
2064
|
session.log.push(`certificate: SHA-256 ${fingerprint}, as set up`);
|
|
1715
2065
|
return;
|
|
1716
2066
|
}
|
|
1717
|
-
const known = this.trustedCertificate(session.
|
|
2067
|
+
const known = this.trustedCertificate(session.authKey);
|
|
1718
2068
|
if (!known) {
|
|
1719
|
-
this.trustCertificate(session.
|
|
2069
|
+
this.trustCertificate(session.authKey, fingerprint);
|
|
1720
2070
|
session.log.push(`certificate: SHA-256 ${fingerprint}, remembered (first connection)`);
|
|
1721
2071
|
return;
|
|
1722
2072
|
}
|
|
@@ -1789,13 +2139,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1789
2139
|
`Something other than GNOME Remote Desktop may be listening on port ${endpoint.port}.`);
|
|
1790
2140
|
return false;
|
|
1791
2141
|
}
|
|
2142
|
+
// The desktop's, or its gateway's.
|
|
2143
|
+
const whose = problem.gateway ? `${spec.name}'s gateway ${problem.gateway.address}` : spec.name;
|
|
1792
2144
|
if (!session.visible) {
|
|
1793
|
-
this.notifications.notice(`The certificate of ${
|
|
2145
|
+
this.notifications.notice(`The certificate of ${whose}${target.direct ? '' : ` (via ${target.label})`} has changed. Open that desktop to decide.`);
|
|
1794
2146
|
}
|
|
1795
2147
|
const trusted = await new Promise(resolve => {
|
|
1796
|
-
session.status(`The certificate of ${
|
|
2148
|
+
session.status(`The certificate of ${whose} has changed since it was last used. Nothing has been sent to it.\n\n` +
|
|
1797
2149
|
`${fingerprints}\n\nReinstalling the machine or renewing its certificate changes it. If neither happened, ` +
|
|
1798
|
-
`something else may be answering at ${spec.host}:${spec.port}.`, [
|
|
2150
|
+
`something else may be answering at ${problem.gateway?.address ?? `${spec.host}:${spec.port}`}.`, [
|
|
1799
2151
|
{ label: 'Trust the new certificate', run: () => resolve(true) },
|
|
1800
2152
|
{ label: 'Cancel', run: () => resolve(false) },
|
|
1801
2153
|
], true);
|
|
@@ -1808,8 +2160,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1808
2160
|
stop('Not connected: the new certificate was not trusted.');
|
|
1809
2161
|
return false;
|
|
1810
2162
|
}
|
|
1811
|
-
this.trustCertificate(session.
|
|
1812
|
-
session.log.push(
|
|
2163
|
+
this.trustCertificate(problem.gateway?.key ?? session.authKey, problem.actual);
|
|
2164
|
+
session.log.push(`${problem.gateway ? 'gateway ' : ''}certificate: SHA-256 ${problem.actual}, trusted instead of ${problem.expected}`);
|
|
1813
2165
|
return true;
|
|
1814
2166
|
}
|
|
1815
2167
|
/** The certificate remembered for a desktop behind a host (`remoteDesktop.trustedCertificates`), if any. */
|
|
@@ -1833,6 +2185,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1833
2185
|
const list = Array.isArray(store.trustedCertificates) ? store.trustedCertificates : [];
|
|
1834
2186
|
const forget = (key) => direct ? key === `${desktops_1.DIRECT_KEY}#${desktopId}` : key.endsWith(`#${desktopId}`) && !key.startsWith(`${desktops_1.DIRECT_KEY}#`);
|
|
1835
2187
|
store.trustedCertificates = list.filter((e) => !forget(String(e?.desktop ?? '')));
|
|
2188
|
+
store.withoutNla = (Array.isArray(store.withoutNla) ? store.withoutNla : []).filter((e) => !forget(String(e?.desktop ?? '')));
|
|
1836
2189
|
}
|
|
1837
2190
|
/**
|
|
1838
2191
|
* For a desktop with `wake`: when its RDP server doesn't answer (probed from the SSH host), starts it and waits
|
|
@@ -1842,7 +2195,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1842
2195
|
async wakeIfDown(pane, target, spec, session, automatic) {
|
|
1843
2196
|
const alive = () => this.sessions.get(pane) === session;
|
|
1844
2197
|
session.status(`Connecting to ${spec.name} via ${target.label}…`);
|
|
1845
|
-
if (await (0, wake_1.
|
|
2198
|
+
if (await (0, wake_1.desktopUp)(target, spec.host, spec.port, spec.wake)) {
|
|
1846
2199
|
return false;
|
|
1847
2200
|
}
|
|
1848
2201
|
if (!alive()) {
|
|
@@ -1867,7 +2220,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1867
2220
|
const started = Date.now();
|
|
1868
2221
|
const progress = () => session.status(`Starting ${spec.name}…\nWaiting for it to answer: ${Math.round((Date.now() - started) / 1000)} s`, [cancel]);
|
|
1869
2222
|
progress();
|
|
1870
|
-
const up = await this.zone.runOutsideAngular(() => (0, wake_1.waitForRdp)(target, spec.host, spec.port, WAKE_TIMEOUT_MS, () => cancelled || !alive(), progress));
|
|
2223
|
+
const up = await this.zone.runOutsideAngular(() => (0, wake_1.waitForRdp)(target, spec.host, spec.port, WAKE_TIMEOUT_MS, () => cancelled || !alive(), progress, spec.wake));
|
|
1871
2224
|
if (up) {
|
|
1872
2225
|
session.log.push(`wake: answered after ${Math.round((Date.now() - started) / 1000)} s`);
|
|
1873
2226
|
// Started here, so it can go back off when it's no longer used, if the settings say so (a VM only: a machine
|
|
@@ -1889,6 +2242,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1889
2242
|
const credentials = endpoint.credentials;
|
|
1890
2243
|
const alive = () => this.sessions.get(pane) === session;
|
|
1891
2244
|
session.certificateProblem = null;
|
|
2245
|
+
session.gatewayRefused = null;
|
|
2246
|
+
session.withoutNla = false;
|
|
1892
2247
|
const el = document.createElement('iron-remote-desktop');
|
|
1893
2248
|
try {
|
|
1894
2249
|
el.setAttribute('scale', this.screenScale() === 3 ? 'real' : 'fit');
|
|
@@ -1915,13 +2270,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1915
2270
|
const size = remoteSizeFor(session.overlay.getBoundingClientRect(), settings);
|
|
1916
2271
|
const { width, height } = size;
|
|
1917
2272
|
// DOMAIN\user or user@domain go to the server as typed; only the former needs splitting.
|
|
1918
|
-
const [domain, username] = /^([^\\]+)\\(.+)$/.exec(credentials.username)?.slice(1) ?? [spec.domain ?? '', credentials.username];
|
|
2273
|
+
const [domain, username] = /^([^\\]+)\\(.+)$/.exec(credentials.username)?.slice(1) ?? [endpoint.domain ?? spec.domain ?? '', credentials.username];
|
|
1919
2274
|
session.status(spec.id === desktops_1.OWN_DESKTOP ? `Connecting to ${target.label}…`
|
|
1920
2275
|
: target.direct ? `Connecting to ${spec.name}…` : `Connecting to ${spec.name} via ${target.label}…`);
|
|
1921
2276
|
const config = session.ui.configBuilder()
|
|
1922
2277
|
.withUsername(username)
|
|
1923
2278
|
.withPassword(credentials.password)
|
|
1924
|
-
|
|
2279
|
+
// The name the server is signed in to as: the SSH host's for its own desktop, and for its VMs' consoles.
|
|
2280
|
+
.withDestination(`${spec.id === desktops_1.OWN_DESKTOP || spec.hyperv ? target.hostname ?? target.label : endpoint.host}:${endpoint.port}`)
|
|
1925
2281
|
.withProxyAddress(session.proxy.url)
|
|
1926
2282
|
.withServerDomain(domain)
|
|
1927
2283
|
.withAuthToken(session.proxy.token)
|
|
@@ -1931,9 +2287,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1931
2287
|
// H.264 in the graphics pipeline, decoded by the browser (hardware-accelerated where it can be). A decoder
|
|
1932
2288
|
// failure ends the connection, which then reconnects without H.264 (see below). Not for xrdp: it encodes
|
|
1933
2289
|
// H.264 only in some builds, and without it bitmaps do better there than the pipeline.
|
|
2290
|
+
// A Hyper-V VM's console: the VM's id goes ahead of everything, and the sign-in (the host's) before the RDP
|
|
2291
|
+
// handshake. Its basic console is a picture, keyboard and mouse: no channels for sound, files or resizing.
|
|
2292
|
+
const basic = endpoint.hyperv === 'basic';
|
|
2293
|
+
if (spec.hyperv) {
|
|
2294
|
+
if (typeof rdp.vmConnect !== 'function') {
|
|
2295
|
+
throw new Error('this build of IronRDP has no Hyper-V console support');
|
|
2296
|
+
}
|
|
2297
|
+
config.withExtension(rdp.vmConnect(spec.hyperv, endpoint.hyperv ?? 'basic'));
|
|
2298
|
+
}
|
|
1934
2299
|
session.h264?.close();
|
|
1935
2300
|
session.h264 = null;
|
|
1936
|
-
if (spec.kind !== 'xrdp' && typeof rdp.graphicsPipeline === 'function' && await this.useH264(rdp, session, settings)) {
|
|
2301
|
+
if (spec.kind !== 'xrdp' && !basic && typeof rdp.graphicsPipeline === 'function' && await this.useH264(rdp, session, settings)) {
|
|
1937
2302
|
session.h264 = new rdp.WebCodecsH264Decoder({ onFailure: (reason) => session.log.push(`h264: failed: ${reason}`) });
|
|
1938
2303
|
config.withExtension(rdp.h264Decoder(session.h264));
|
|
1939
2304
|
}
|
|
@@ -1949,19 +2314,29 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1949
2314
|
session.graphics = 'automatic graphics';
|
|
1950
2315
|
}
|
|
1951
2316
|
session.log.push(`graphics: ${session.graphics}`);
|
|
1952
|
-
if (settings.sound) {
|
|
2317
|
+
if (settings.sound && !basic) {
|
|
1953
2318
|
session.audio?.close();
|
|
1954
2319
|
session.audio = new audio_1.AudioPlayer();
|
|
1955
2320
|
config.withExtension(rdp.audioPlayback(session.audio.callback));
|
|
1956
2321
|
}
|
|
1957
2322
|
session.mic?.close();
|
|
1958
2323
|
session.mic = null;
|
|
1959
|
-
if (settings.microphone && typeof rdp.audioInput === 'function') {
|
|
2324
|
+
if (settings.microphone && !basic && typeof rdp.audioInput === 'function') {
|
|
1960
2325
|
const ui = session.ui;
|
|
1961
2326
|
const mic = new microphone_1.Microphone(pcm => ui.invokeExtension(rdp.audioInputData(pcm)), () => session.overlay.classList.toggle('trd-mic-on', mic.capturing), m => session.log.push(m), m => this.zone.run(() => this.notifications.error(m)));
|
|
1962
2327
|
session.mic = mic;
|
|
1963
2328
|
config.withExtension(rdp.audioInput(mic.callback));
|
|
1964
2329
|
}
|
|
2330
|
+
// Shared folders as drives (\\tsclient\<name>). GNOME Remote Desktop doesn't serve drives; Windows and xrdp
|
|
2331
|
+
// do. Also skipped with no folder shared: the server then sees no drive device at all.
|
|
2332
|
+
session.drives?.dispose();
|
|
2333
|
+
session.drives = null;
|
|
2334
|
+
const folders = this.sharedFolders();
|
|
2335
|
+
if (folders.length && spec.kind !== 'gnome' && !basic && typeof rdp.driveRedirection === 'function') {
|
|
2336
|
+
session.drives = new drives_1.SharedDrives(folders, m => session.log.push(m));
|
|
2337
|
+
config.withExtension(rdp.driveRedirection(session.drives));
|
|
2338
|
+
session.log.push(`drives: ${folders.map(f => `${f.name}${f.readOnly ? ' (read-only)' : ''}`).join(', ')}`);
|
|
2339
|
+
}
|
|
1965
2340
|
const built = config.build();
|
|
1966
2341
|
let info;
|
|
1967
2342
|
try {
|
|
@@ -1972,6 +2347,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1972
2347
|
if (session.certificateProblem) {
|
|
1973
2348
|
return { connected: false, certificate: session.certificateProblem };
|
|
1974
2349
|
}
|
|
2350
|
+
// The proxy stopped at a server that would get the password as it is (see checkWithoutNla).
|
|
2351
|
+
if (session.withoutNla) {
|
|
2352
|
+
return { connected: false, withoutNla: true };
|
|
2353
|
+
}
|
|
2354
|
+
// Its gateway refused the sign-in, before anything went to the desktop.
|
|
2355
|
+
if (session.gatewayRefused) {
|
|
2356
|
+
session.log.push(`gateway: ${session.gatewayRefused}`);
|
|
2357
|
+
return { connected: false, signInFailed: session.gatewayRefused, gatewayRefused: true };
|
|
2358
|
+
}
|
|
1975
2359
|
// IronErrorKind: 1 WrongPassword, 2 LogonFailure.
|
|
1976
2360
|
const kind = typeof e?.kind === 'function' ? e.kind() : undefined;
|
|
1977
2361
|
const detail = typeof e?.backtrace === 'function' ? e.backtrace() : (e?.message ?? String(e));
|
|
@@ -1985,8 +2369,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1985
2369
|
return { connected: false };
|
|
1986
2370
|
}
|
|
1987
2371
|
session.ui.setVisibility(true);
|
|
2372
|
+
this.applyKeyboardMode(session);
|
|
1988
2373
|
session.remoteSize = { width, height, scale: 100 };
|
|
1989
|
-
if (size.scale !== 100 || spec.kind === 'windows') {
|
|
2374
|
+
if (!basic && (size.scale !== 100 || spec.kind === 'windows')) {
|
|
1990
2375
|
// Retina: the connection starts unscaled; ask for the matching remote scale right away. Windows also
|
|
1991
2376
|
// keeps a signed-in session's scale from the last connection: set it either way.
|
|
1992
2377
|
this.applySize(session, size);
|
|
@@ -2000,12 +2385,19 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2000
2385
|
this.syncIndicator(session);
|
|
2001
2386
|
this.changed$.next();
|
|
2002
2387
|
this.reconnects.delete(pane); // connected again: a later drop starts over
|
|
2003
|
-
|
|
2004
|
-
|
|
2388
|
+
// Under a saved account only while that account is still there with the same sign-in name: the prompt may
|
|
2389
|
+
// have been open while it was removed or changed in Settings.
|
|
2390
|
+
const accountNow = endpoint.saveKey ? this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === endpoint.saveKey) : undefined;
|
|
2391
|
+
if (endpoint.remember && endpoint.saveKey && (!accountNow || (0, accounts_1.signInName)(accountNow) !== endpoint.saveFor)) {
|
|
2392
|
+
session.log.push(`sign-in: not saved: the account ${accountNow ? 'changed' : 'was removed'} meanwhile`);
|
|
2393
|
+
}
|
|
2394
|
+
else if (endpoint.remember) {
|
|
2395
|
+
(0, signin_1.saveCredentials)(endpoint.saveKey ?? session.authKey, credentials, endpoint.saveLabel).then(() => session.log.push(`sign-in: saved ${endpoint.saveKey ? 'as the account\'s password ' : ''}in ${(0, signin_1.storeName)()}`), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
2005
2396
|
}
|
|
2006
2397
|
if (session.visible) {
|
|
2007
2398
|
session.focusDesktop();
|
|
2008
2399
|
}
|
|
2400
|
+
const connectedAt = Date.now();
|
|
2009
2401
|
let end;
|
|
2010
2402
|
try {
|
|
2011
2403
|
end = await info.run();
|
|
@@ -2020,11 +2412,19 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2020
2412
|
this.h264Failed.add(session.key);
|
|
2021
2413
|
return { connected: true, error: `H.264 decoding failed (${session.h264.failed}); continuing without it` };
|
|
2022
2414
|
}
|
|
2023
|
-
|
|
2415
|
+
const error = typeof e?.backtrace === 'function' ? e.backtrace().split('\n')[0] : (e?.message ?? String(e));
|
|
2416
|
+
// A Hyper-V host signs anyone in that it knows, and ends the connection at once when that account may not
|
|
2417
|
+
// open the VM's console: a refused sign-in in effect, so the form comes back, saying so.
|
|
2418
|
+
if (spec.hyperv && Date.now() - connectedAt < 5000 && /disconnect provider ultimatum/i.test(error)) {
|
|
2419
|
+
session.log.push(`hyper-v: disconnected at once (${error}): the account may not open this VM's console`);
|
|
2420
|
+
return { connected: false, signInFailed: `${target.label} ended the connection at once: this account may not open ${spec.name}'s console. Use an administrator of the host, a member of its Hyper-V Administrators, or an account given access with Grant-VMConnectAccess.` };
|
|
2421
|
+
}
|
|
2422
|
+
return { connected: true, error };
|
|
2024
2423
|
}
|
|
2025
2424
|
finally {
|
|
2026
|
-
// The server can't close the microphone once the connection is gone.
|
|
2425
|
+
// The server can't close the microphone, or its files, once the connection is gone.
|
|
2027
2426
|
session.mic?.close();
|
|
2427
|
+
session.drives?.dispose();
|
|
2028
2428
|
}
|
|
2029
2429
|
return { connected: true, reason: end?.reason?.() };
|
|
2030
2430
|
}
|