tabby-rdp 0.4.1 → 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 +90 -0
- package/README.md +93 -4
- package/dist/desktop.service.js +404 -43
- 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 +60 -6
- package/dist/settingsPage.js.map +1 -1
- package/dist/signin.js +26 -18
- 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);
|
|
@@ -438,13 +481,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
438
481
|
if (known && Date.now() - known.at < 60000) {
|
|
439
482
|
return Promise.resolve();
|
|
440
483
|
}
|
|
441
|
-
const
|
|
442
|
-
|
|
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 });
|
|
443
487
|
this.changed$.next();
|
|
444
488
|
}, () => {
|
|
445
|
-
|
|
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 });
|
|
446
491
|
});
|
|
447
|
-
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 });
|
|
448
493
|
return scan;
|
|
449
494
|
}
|
|
450
495
|
/** Keeps a found VM open in the pane among the host's desktops (`remoteDesktop.desktops`), under its name. */
|
|
@@ -454,7 +499,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
454
499
|
if (!session?.remote || !spec?.found) {
|
|
455
500
|
return;
|
|
456
501
|
}
|
|
457
|
-
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 } : {} };
|
|
458
504
|
this.config.store.remoteDesktop.desktops = [...this.configuredDesktops(), entry];
|
|
459
505
|
this.config.save();
|
|
460
506
|
this.changed$.next();
|
|
@@ -491,15 +537,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
491
537
|
return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
|
|
492
538
|
}
|
|
493
539
|
/**
|
|
494
|
-
* The desktops that sign in with a saved account
|
|
495
|
-
* 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.
|
|
496
543
|
*/
|
|
497
544
|
accountUses(id) {
|
|
498
|
-
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 }] : []);
|
|
499
546
|
const groups = this.config.store.groups ?? [];
|
|
500
547
|
const inherited = (p) => p.options?.account === undefined && groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account === id;
|
|
501
548
|
const profiles = (this.config.store.profiles ?? [])
|
|
502
|
-
.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)))
|
|
503
550
|
.map((p) => ({ name: String(p.name ?? p.options?.host ?? ''), profileId: p.id ? String(p.id) : undefined }));
|
|
504
551
|
return [...desktops, ...profiles];
|
|
505
552
|
}
|
|
@@ -584,12 +631,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
584
631
|
async removeAccount(id) {
|
|
585
632
|
const store = this.config.store.remoteDesktop;
|
|
586
633
|
store.accounts = this.accounts().filter(a => a.id !== id);
|
|
587
|
-
if (this.configuredDesktops().some(d => d.account === id)) {
|
|
634
|
+
if (this.configuredDesktops().some(d => d.account === id || d.gatewayAccount === id)) {
|
|
588
635
|
store.desktops = this.configuredDesktops().map(d => {
|
|
589
|
-
|
|
590
|
-
|
|
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;
|
|
591
643
|
}
|
|
592
|
-
const { account: _account, ...rest } = d;
|
|
593
644
|
return rest;
|
|
594
645
|
});
|
|
595
646
|
}
|
|
@@ -597,6 +648,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
597
648
|
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.account === id) {
|
|
598
649
|
profile.options.account = '';
|
|
599
650
|
}
|
|
651
|
+
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.gatewayAccount === id) {
|
|
652
|
+
profile.options.gatewayAccount = '';
|
|
653
|
+
}
|
|
600
654
|
}
|
|
601
655
|
// A profile group's default for remote desktop profiles, too.
|
|
602
656
|
for (const group of this.config.store.groups ?? []) {
|
|
@@ -728,7 +782,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
728
782
|
}
|
|
729
783
|
const moved = (key) => direct ? key === `${desktops_1.DIRECT_KEY}#${fromId}` : key.endsWith(`#${fromId}`) && !key.startsWith(`${desktops_1.DIRECT_KEY}#`);
|
|
730
784
|
const store = this.config.store.remoteDesktop;
|
|
731
|
-
for (const list of ['desktopSharpness', 'trustedCertificates']) {
|
|
785
|
+
for (const list of ['desktopSharpness', 'trustedCertificates', 'withoutNla']) {
|
|
732
786
|
if (Array.isArray(store[list])) {
|
|
733
787
|
store[list] = store[list].map((e) => typeof e?.desktop === 'string' && moved(e.desktop)
|
|
734
788
|
? { ...e, desktop: `${e.desktop.slice(0, -fromId.length)}${toId}` }
|
|
@@ -784,23 +838,53 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
784
838
|
return null;
|
|
785
839
|
}
|
|
786
840
|
const existing = (this.config.store.profiles ?? []).find((p) => p?.type === targets_1.RDP_PROFILE_TYPE && !p.options?.via &&
|
|
787
|
-
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 ?? ''));
|
|
788
843
|
if (existing) {
|
|
789
844
|
this.notifications.info(`Opening "${existing.name}", which is already a profile for ${fileName}`);
|
|
790
845
|
await this.profiles.openNewTabForProfile(existing);
|
|
791
846
|
return existing;
|
|
792
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
|
+
}
|
|
793
865
|
const profile = {
|
|
794
866
|
type: targets_1.RDP_PROFILE_TYPE,
|
|
795
|
-
name:
|
|
867
|
+
name: base.replace(/\.rdp$/i, '') || parsed.host,
|
|
796
868
|
icon: 'fas fa-desktop',
|
|
797
869
|
group: await this.remoteDesktopGroup(),
|
|
798
|
-
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: '' },
|
|
799
871
|
};
|
|
800
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
|
+
}
|
|
801
879
|
this.config.save();
|
|
802
880
|
this.notifications.notice(`Added the remote desktop profile "${profile.name}" (Settings › Profiles & connections)`);
|
|
803
|
-
|
|
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
|
+
}
|
|
804
888
|
return profile;
|
|
805
889
|
}
|
|
806
890
|
/** Asks, then removes a configured desktop (see removeDesktop). The menus and the settings page use this. */
|
|
@@ -1119,6 +1203,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1119
1203
|
sharpness: store.sharpness === 'retina' ? 'retina' : 'standard',
|
|
1120
1204
|
desk: store.desk === true,
|
|
1121
1205
|
macShortcuts: store.macShortcuts !== false,
|
|
1206
|
+
unicodeKeys: store.unicodeKeys === true,
|
|
1122
1207
|
sound: store.sound !== false,
|
|
1123
1208
|
h264: store.h264 !== false,
|
|
1124
1209
|
zoom: store.zoom === 'actual' ? 'actual' : 'fit',
|
|
@@ -1130,6 +1215,16 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1130
1215
|
shutDownIdle: [5, 15, 60].includes(Number(store.shutDownIdle)) ? Number(store.shutDownIdle) : 0,
|
|
1131
1216
|
};
|
|
1132
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
|
+
}
|
|
1133
1228
|
/** The sharpness chosen for the pane's desktop in particular (`remoteDesktop.desktopSharpness`), or null. */
|
|
1134
1229
|
ownSharpness(pane) {
|
|
1135
1230
|
const key = this.sessions.get(pane)?.key;
|
|
@@ -1176,10 +1271,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1176
1271
|
this.config.save();
|
|
1177
1272
|
for (const [pane, session] of this.sessions) {
|
|
1178
1273
|
this.applyZoom(session);
|
|
1274
|
+
this.applyKeyboardMode(session);
|
|
1179
1275
|
this.syncIndicator(session);
|
|
1180
1276
|
this.followPane(pane, session);
|
|
1181
1277
|
}
|
|
1182
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
|
+
}
|
|
1183
1286
|
/** IronRDP's ScreenScale for the picture: Real (1:1, scrolling) only with a fixed resolution and zoom 'actual'. */
|
|
1184
1287
|
screenScale() {
|
|
1185
1288
|
const settings = this.settings();
|
|
@@ -1290,6 +1393,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1290
1393
|
setViewOnly(pane, viewOnly) {
|
|
1291
1394
|
const session = this.sessions.get(pane);
|
|
1292
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
|
+
}
|
|
1293
1404
|
session.setViewOnly(viewOnly);
|
|
1294
1405
|
this.changed$.next();
|
|
1295
1406
|
}
|
|
@@ -1427,9 +1538,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1427
1538
|
forgetCertificate(key) {
|
|
1428
1539
|
const store = this.config.store.remoteDesktop;
|
|
1429
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);
|
|
1430
1543
|
this.config.save();
|
|
1431
1544
|
this.changed$.next();
|
|
1432
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
|
+
}
|
|
1433
1550
|
/** The split tab holding the pane, if it has more than one pane. */
|
|
1434
1551
|
splitOf(pane) {
|
|
1435
1552
|
const split = this.app.tabs.find(t => t instanceof tabby_core_1.SplitTabComponent && t.getAllTabs().includes(pane));
|
|
@@ -1456,6 +1573,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1456
1573
|
if (!split) {
|
|
1457
1574
|
return;
|
|
1458
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
|
+
}
|
|
1459
1580
|
on ? this.broadcast.add(split) : this.broadcast.delete(split);
|
|
1460
1581
|
for (const p of split.getAllTabs()) {
|
|
1461
1582
|
const session = this.sessions.get(p);
|
|
@@ -1464,14 +1585,19 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1464
1585
|
}
|
|
1465
1586
|
this.changed$.next();
|
|
1466
1587
|
}
|
|
1467
|
-
/**
|
|
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
|
+
*/
|
|
1468
1592
|
broadcastKey(pane, type, init) {
|
|
1593
|
+
this.sessions.get(pane)?.noteKey(type, init);
|
|
1469
1594
|
if (!this.isBroadcast(pane)) {
|
|
1470
1595
|
return;
|
|
1471
1596
|
}
|
|
1472
1597
|
for (const { session } of this.othersInTab(pane)) {
|
|
1473
1598
|
try {
|
|
1474
1599
|
session.ui.sendKeyboardEvent(new KeyboardEvent(type, { ...init, cancelable: true }));
|
|
1600
|
+
session.noteKey(type, init);
|
|
1475
1601
|
}
|
|
1476
1602
|
catch (e) {
|
|
1477
1603
|
session.log.push(`typing into all desktops: ${e?.message ?? e}`);
|
|
@@ -1601,6 +1727,20 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1601
1727
|
spec.port = endpoint.port;
|
|
1602
1728
|
spec.username ??= endpoint.username;
|
|
1603
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
|
+
}
|
|
1604
1744
|
// A saved account (Settings): its user name, and its password from the keychain. One that has none yet, or that
|
|
1605
1745
|
// this desktop just refused, is asked for here and saved for every desktop that uses the account.
|
|
1606
1746
|
const account = spec.account ? this.accounts().find(a => a.id === spec.account) : undefined;
|
|
@@ -1609,7 +1749,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1609
1749
|
}
|
|
1610
1750
|
const again = this.askAgain.delete(pane);
|
|
1611
1751
|
// A desktop whose account is gone asks, rather than falling back to a login of its own saved earlier.
|
|
1612
|
-
let saved = retryError || again || spec.account && !account ? null : await (0, signin_1.loadCredentials)(account ? (0, accounts_1.accountKey)(account.id) : session.
|
|
1752
|
+
let saved = retryError || again || spec.account && !account ? null : await (0, signin_1.loadCredentials)(account ? (0, accounts_1.accountKey)(account.id) : session.authKey);
|
|
1613
1753
|
if (saved && account && saved.username !== (0, accounts_1.signInName)(account)) {
|
|
1614
1754
|
// Kept for an earlier user name of the account (its change didn't reach the store): not this one's.
|
|
1615
1755
|
session.log.push(`sign-in: the account's saved password is for ${saved.username}, not ${(0, accounts_1.signInName)(account)}; asking`);
|
|
@@ -1619,12 +1759,13 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1619
1759
|
if (account) {
|
|
1620
1760
|
session.log.push(`sign-in: the saved account "${account.name}"`);
|
|
1621
1761
|
}
|
|
1622
|
-
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 };
|
|
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 };
|
|
1623
1763
|
}
|
|
1624
1764
|
session.status('');
|
|
1625
1765
|
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
1626
1766
|
// The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via".
|
|
1627
|
-
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})`,
|
|
1628
1769
|
username: account ? (0, accounts_1.signInName)(account) : spec.username,
|
|
1629
1770
|
error: retryError,
|
|
1630
1771
|
canRemember: true,
|
|
@@ -1637,6 +1778,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1637
1778
|
return entered && {
|
|
1638
1779
|
host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
|
|
1639
1780
|
saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
|
|
1781
|
+
hyperv,
|
|
1640
1782
|
};
|
|
1641
1783
|
}
|
|
1642
1784
|
async connect(pane, target, spec, session) {
|
|
@@ -1648,7 +1790,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1648
1790
|
let woken = false;
|
|
1649
1791
|
let startupRetries = 0;
|
|
1650
1792
|
let endpoint = null;
|
|
1793
|
+
let gatewayError;
|
|
1651
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
|
+
}
|
|
1652
1799
|
if (spec.wake) {
|
|
1653
1800
|
const woke = await this.wakeIfDown(pane, target, spec, session, automatic);
|
|
1654
1801
|
if (woke === null) {
|
|
@@ -1679,7 +1826,23 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1679
1826
|
endpoint = asked;
|
|
1680
1827
|
session.remote = target;
|
|
1681
1828
|
session.pinnedCertificate = asked.certificate ?? null;
|
|
1682
|
-
|
|
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) });
|
|
1683
1846
|
const outcome = await this.run(pane, target, spec, session, rdp, asked);
|
|
1684
1847
|
if (outcome.certificate && alive()) {
|
|
1685
1848
|
// A changed certificate, now trusted: the same account again, without asking for it again.
|
|
@@ -1688,10 +1851,23 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1688
1851
|
}
|
|
1689
1852
|
return;
|
|
1690
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
|
+
}
|
|
1691
1867
|
if (outcome.signInFailed && this.signsIn(spec) && attempt < 5 && alive()) {
|
|
1692
1868
|
// The desktop's own saved account goes. A shared one stays until a new password is entered: this
|
|
1693
1869
|
// desktop refusing it doesn't make it wrong for the others.
|
|
1694
|
-
await (0, signin_1.forgetCredentials)(session.
|
|
1870
|
+
await (0, signin_1.forgetCredentials)(session.authKey);
|
|
1695
1871
|
retryError = outcome.signInFailed;
|
|
1696
1872
|
endpoint = null;
|
|
1697
1873
|
continue;
|
|
@@ -1728,6 +1904,147 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1728
1904
|
}
|
|
1729
1905
|
}
|
|
1730
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
|
+
}
|
|
1731
2048
|
/**
|
|
1732
2049
|
* The proxy's check of the server's certificate, before any credentials go out. The host's own desktop must show
|
|
1733
2050
|
* the certificate its setup reported. Others (behind a host, a Windows host's own, a remote desktop tab's) are
|
|
@@ -1747,9 +2064,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1747
2064
|
session.log.push(`certificate: SHA-256 ${fingerprint}, as set up`);
|
|
1748
2065
|
return;
|
|
1749
2066
|
}
|
|
1750
|
-
const known = this.trustedCertificate(session.
|
|
2067
|
+
const known = this.trustedCertificate(session.authKey);
|
|
1751
2068
|
if (!known) {
|
|
1752
|
-
this.trustCertificate(session.
|
|
2069
|
+
this.trustCertificate(session.authKey, fingerprint);
|
|
1753
2070
|
session.log.push(`certificate: SHA-256 ${fingerprint}, remembered (first connection)`);
|
|
1754
2071
|
return;
|
|
1755
2072
|
}
|
|
@@ -1822,13 +2139,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1822
2139
|
`Something other than GNOME Remote Desktop may be listening on port ${endpoint.port}.`);
|
|
1823
2140
|
return false;
|
|
1824
2141
|
}
|
|
2142
|
+
// The desktop's, or its gateway's.
|
|
2143
|
+
const whose = problem.gateway ? `${spec.name}'s gateway ${problem.gateway.address}` : spec.name;
|
|
1825
2144
|
if (!session.visible) {
|
|
1826
|
-
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.`);
|
|
1827
2146
|
}
|
|
1828
2147
|
const trusted = await new Promise(resolve => {
|
|
1829
|
-
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` +
|
|
1830
2149
|
`${fingerprints}\n\nReinstalling the machine or renewing its certificate changes it. If neither happened, ` +
|
|
1831
|
-
`something else may be answering at ${spec.host}:${spec.port}.`, [
|
|
2150
|
+
`something else may be answering at ${problem.gateway?.address ?? `${spec.host}:${spec.port}`}.`, [
|
|
1832
2151
|
{ label: 'Trust the new certificate', run: () => resolve(true) },
|
|
1833
2152
|
{ label: 'Cancel', run: () => resolve(false) },
|
|
1834
2153
|
], true);
|
|
@@ -1841,8 +2160,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1841
2160
|
stop('Not connected: the new certificate was not trusted.');
|
|
1842
2161
|
return false;
|
|
1843
2162
|
}
|
|
1844
|
-
this.trustCertificate(session.
|
|
1845
|
-
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}`);
|
|
1846
2165
|
return true;
|
|
1847
2166
|
}
|
|
1848
2167
|
/** The certificate remembered for a desktop behind a host (`remoteDesktop.trustedCertificates`), if any. */
|
|
@@ -1866,6 +2185,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1866
2185
|
const list = Array.isArray(store.trustedCertificates) ? store.trustedCertificates : [];
|
|
1867
2186
|
const forget = (key) => direct ? key === `${desktops_1.DIRECT_KEY}#${desktopId}` : key.endsWith(`#${desktopId}`) && !key.startsWith(`${desktops_1.DIRECT_KEY}#`);
|
|
1868
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 ?? '')));
|
|
1869
2189
|
}
|
|
1870
2190
|
/**
|
|
1871
2191
|
* For a desktop with `wake`: when its RDP server doesn't answer (probed from the SSH host), starts it and waits
|
|
@@ -1875,7 +2195,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1875
2195
|
async wakeIfDown(pane, target, spec, session, automatic) {
|
|
1876
2196
|
const alive = () => this.sessions.get(pane) === session;
|
|
1877
2197
|
session.status(`Connecting to ${spec.name} via ${target.label}…`);
|
|
1878
|
-
if (await (0, wake_1.
|
|
2198
|
+
if (await (0, wake_1.desktopUp)(target, spec.host, spec.port, spec.wake)) {
|
|
1879
2199
|
return false;
|
|
1880
2200
|
}
|
|
1881
2201
|
if (!alive()) {
|
|
@@ -1900,7 +2220,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1900
2220
|
const started = Date.now();
|
|
1901
2221
|
const progress = () => session.status(`Starting ${spec.name}…\nWaiting for it to answer: ${Math.round((Date.now() - started) / 1000)} s`, [cancel]);
|
|
1902
2222
|
progress();
|
|
1903
|
-
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));
|
|
1904
2224
|
if (up) {
|
|
1905
2225
|
session.log.push(`wake: answered after ${Math.round((Date.now() - started) / 1000)} s`);
|
|
1906
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
|
|
@@ -1922,6 +2242,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1922
2242
|
const credentials = endpoint.credentials;
|
|
1923
2243
|
const alive = () => this.sessions.get(pane) === session;
|
|
1924
2244
|
session.certificateProblem = null;
|
|
2245
|
+
session.gatewayRefused = null;
|
|
2246
|
+
session.withoutNla = false;
|
|
1925
2247
|
const el = document.createElement('iron-remote-desktop');
|
|
1926
2248
|
try {
|
|
1927
2249
|
el.setAttribute('scale', this.screenScale() === 3 ? 'real' : 'fit');
|
|
@@ -1954,7 +2276,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1954
2276
|
const config = session.ui.configBuilder()
|
|
1955
2277
|
.withUsername(username)
|
|
1956
2278
|
.withPassword(credentials.password)
|
|
1957
|
-
|
|
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}`)
|
|
1958
2281
|
.withProxyAddress(session.proxy.url)
|
|
1959
2282
|
.withServerDomain(domain)
|
|
1960
2283
|
.withAuthToken(session.proxy.token)
|
|
@@ -1964,9 +2287,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1964
2287
|
// H.264 in the graphics pipeline, decoded by the browser (hardware-accelerated where it can be). A decoder
|
|
1965
2288
|
// failure ends the connection, which then reconnects without H.264 (see below). Not for xrdp: it encodes
|
|
1966
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
|
+
}
|
|
1967
2299
|
session.h264?.close();
|
|
1968
2300
|
session.h264 = null;
|
|
1969
|
-
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)) {
|
|
1970
2302
|
session.h264 = new rdp.WebCodecsH264Decoder({ onFailure: (reason) => session.log.push(`h264: failed: ${reason}`) });
|
|
1971
2303
|
config.withExtension(rdp.h264Decoder(session.h264));
|
|
1972
2304
|
}
|
|
@@ -1982,19 +2314,29 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1982
2314
|
session.graphics = 'automatic graphics';
|
|
1983
2315
|
}
|
|
1984
2316
|
session.log.push(`graphics: ${session.graphics}`);
|
|
1985
|
-
if (settings.sound) {
|
|
2317
|
+
if (settings.sound && !basic) {
|
|
1986
2318
|
session.audio?.close();
|
|
1987
2319
|
session.audio = new audio_1.AudioPlayer();
|
|
1988
2320
|
config.withExtension(rdp.audioPlayback(session.audio.callback));
|
|
1989
2321
|
}
|
|
1990
2322
|
session.mic?.close();
|
|
1991
2323
|
session.mic = null;
|
|
1992
|
-
if (settings.microphone && typeof rdp.audioInput === 'function') {
|
|
2324
|
+
if (settings.microphone && !basic && typeof rdp.audioInput === 'function') {
|
|
1993
2325
|
const ui = session.ui;
|
|
1994
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)));
|
|
1995
2327
|
session.mic = mic;
|
|
1996
2328
|
config.withExtension(rdp.audioInput(mic.callback));
|
|
1997
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
|
+
}
|
|
1998
2340
|
const built = config.build();
|
|
1999
2341
|
let info;
|
|
2000
2342
|
try {
|
|
@@ -2005,6 +2347,15 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2005
2347
|
if (session.certificateProblem) {
|
|
2006
2348
|
return { connected: false, certificate: session.certificateProblem };
|
|
2007
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
|
+
}
|
|
2008
2359
|
// IronErrorKind: 1 WrongPassword, 2 LogonFailure.
|
|
2009
2360
|
const kind = typeof e?.kind === 'function' ? e.kind() : undefined;
|
|
2010
2361
|
const detail = typeof e?.backtrace === 'function' ? e.backtrace() : (e?.message ?? String(e));
|
|
@@ -2018,8 +2369,9 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2018
2369
|
return { connected: false };
|
|
2019
2370
|
}
|
|
2020
2371
|
session.ui.setVisibility(true);
|
|
2372
|
+
this.applyKeyboardMode(session);
|
|
2021
2373
|
session.remoteSize = { width, height, scale: 100 };
|
|
2022
|
-
if (size.scale !== 100 || spec.kind === 'windows') {
|
|
2374
|
+
if (!basic && (size.scale !== 100 || spec.kind === 'windows')) {
|
|
2023
2375
|
// Retina: the connection starts unscaled; ask for the matching remote scale right away. Windows also
|
|
2024
2376
|
// keeps a signed-in session's scale from the last connection: set it either way.
|
|
2025
2377
|
this.applySize(session, size);
|
|
@@ -2040,11 +2392,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2040
2392
|
session.log.push(`sign-in: not saved: the account ${accountNow ? 'changed' : 'was removed'} meanwhile`);
|
|
2041
2393
|
}
|
|
2042
2394
|
else if (endpoint.remember) {
|
|
2043
|
-
(0, signin_1.saveCredentials)(endpoint.saveKey ?? session.
|
|
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}`));
|
|
2044
2396
|
}
|
|
2045
2397
|
if (session.visible) {
|
|
2046
2398
|
session.focusDesktop();
|
|
2047
2399
|
}
|
|
2400
|
+
const connectedAt = Date.now();
|
|
2048
2401
|
let end;
|
|
2049
2402
|
try {
|
|
2050
2403
|
end = await info.run();
|
|
@@ -2059,11 +2412,19 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2059
2412
|
this.h264Failed.add(session.key);
|
|
2060
2413
|
return { connected: true, error: `H.264 decoding failed (${session.h264.failed}); continuing without it` };
|
|
2061
2414
|
}
|
|
2062
|
-
|
|
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 };
|
|
2063
2423
|
}
|
|
2064
2424
|
finally {
|
|
2065
|
-
// 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.
|
|
2066
2426
|
session.mic?.close();
|
|
2427
|
+
session.drives?.dispose();
|
|
2067
2428
|
}
|
|
2068
2429
|
return { connected: true, reason: end?.reason?.() };
|
|
2069
2430
|
}
|