tabby-rdp 0.5.0 → 0.5.2

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. package/dist/wake.js.map +0 -1
@@ -5,11 +5,16 @@ exports.formatAddress = formatAddress;
5
5
  exports.askDesktop = askDesktop;
6
6
  const accounts_1 = require("./accounts");
7
7
  const accountForm_1 = require("./accountForm");
8
+ const clipboard_1 = require("./clipboard");
9
+ const desktops_1 = require("./desktops");
8
10
  const gateway_1 = require("./gateway");
11
+ const osd_1 = require("./osd");
9
12
  const wake_1 = require("./wake");
13
+ const hostCompat_1 = require("./hostCompat");
10
14
  /** host:port, [v6]:port, or a bare host (RDP's 3389); null if it doesn't look like an address. */
11
15
  function parseAddress(text) {
12
- const m = /^\[([^\]]+)\](?::(\d+))?$|^([^:\s]+)(?::(\d+))?$/.exec(text.trim());
16
+ // No '#' in a host: session and trust keys are split on it (see keyParts).
17
+ const m = /^\[([^\]#]+)\](?::(\d+))?$|^([^:\s#]+)(?::(\d+))?$/.exec(text.trim());
13
18
  const host = m?.[1] ?? m?.[3];
14
19
  const port = Number(m?.[2] ?? m?.[4] ?? 3389);
15
20
  return host && Number.isInteger(port) && port >= 1 && port <= 65535 ? { host, port } : null;
@@ -20,7 +25,7 @@ function formatAddress(host, port) {
20
25
  }
21
26
  /** An entry's `wake` as the form's field shows it: the VM's name, or the MAC address. */
22
27
  function wakeTextOf(entry) {
23
- return entry.wake?.vm ?? entry.wake?.mac ?? '';
28
+ return (0, desktops_1.configText)(entry.wake?.vm) || (0, desktops_1.configText)(entry.wake?.mac);
24
29
  }
25
30
  /**
26
31
  * "Add a desktop behind <host>" and "Edit": a form over the pane (plain DOM, like the sign-in form; uses its styles).
@@ -51,6 +56,7 @@ function askDesktop(pane, options) {
51
56
  <input class="form-control" name="gateway" placeholder="RD Gateway (optional), as seen from the host, e.g. rdgw.example.com" title="A Remote Desktop Gateway to reach the desktop through (HTTPS, port 443 unless given); the address above is then as the gateway sees it" spellcheck="false">
52
57
  <select class="form-control" name="gatewayAccount" title="A gateway that takes another account than the desktop: a saved account for it"></select>
53
58
  <input class="form-control" name="wake" placeholder="Start it when off (optional): libvirt VM name, or MAC address" title="A libvirt VM on the host, started with virsh; or a MAC address to wake over the network from the host" spellcheck="false">
59
+ <select class="form-control" name="clipboard" title="Which ways text, pictures and files copied on either side go between this computer and the desktop. Applies on the next connection"></select>
54
60
  <div class="trd-signin-buttons">
55
61
  <button type="button" class="btn btn-secondary" name="cancel">Cancel</button>
56
62
  <button type="submit" class="btn btn-primary"></button>
@@ -63,24 +69,36 @@ function askDesktop(pane, options) {
63
69
  form.querySelector('button[type=submit]').textContent = options.action;
64
70
  const error = form.querySelector('.trd-signin-error');
65
71
  const initial = options.entry;
66
- field('name').value = initial.name ?? '';
72
+ // Each field as text whatever the entry holds (see configText): a field makes text of what it is given, and a value
73
+ // that can't be made text (a hand edit's, config sync's) would keep the form from opening, and the entry from being
74
+ // put right here.
75
+ field('name').value = (0, desktops_1.configText)(initial.name);
67
76
  const via = field('via');
68
- via.value = initial.via ?? '';
77
+ via.value = (0, desktops_1.configText)(initial.via);
69
78
  via.hidden = !options.hosts;
70
79
  form.querySelector('datalist').append(...(options.hosts ?? []).map(h => new Option(h)));
71
- field('address').value = formatAddress(initial.host ?? '127.0.0.1', Number(initial.port ?? 3389));
80
+ field('address').value = formatAddress((0, desktops_1.configText)(initial.host ?? '127.0.0.1'), (0, osd_1.configNumber)(initial.port ?? 3389));
72
81
  field('kind').value = initial.kind === 'gnome' || initial.kind === 'xrdp' ? initial.kind : 'windows';
73
- field('username').value = initial.username ?? '';
74
- field('domain').value = initial.domain ?? '';
82
+ // A VM saved from the host's list is xrdp on the host's word (see xrdpFromHost), which is a choice of its own here:
83
+ // kept as long as it is chosen, so that the desktop still asks before signing in without NLA. Any kind picked is
84
+ // the user's, xrdp included.
85
+ if ((0, desktops_1.xrdpFromHost)(initial)) {
86
+ const kind = field('kind');
87
+ kind.prepend(new Option(`xrdp, as ${(0, desktops_1.configText)(initial.via) || 'the host'} reported (asks before signing in without NLA)`, ''));
88
+ kind.value = '';
89
+ }
90
+ field('username').value = (0, desktops_1.configText)(initial.username);
91
+ field('domain').value = (0, desktops_1.configText)(initial.domain);
75
92
  field('wake').value = wakeTextOf(initial);
76
93
  // A saved account in place of a user name and domain of its own. Without any saved, the choice isn't shown.
77
94
  const account = field('account');
78
95
  const accounts = options.accounts ?? [];
79
96
  account.append(new Option('Ask for the account when connecting', ''), ...accounts.map(a => new Option(`Sign in with ${a.name} (${(0, accounts_1.signInName)(a)})`, a.id)));
80
- if (initial.account && !accounts.some(a => a.id === initial.account)) {
81
- account.append(new Option('(a saved account that no longer exists)', initial.account));
97
+ const initialAccount = (0, desktops_1.configText)(initial.account);
98
+ if (initialAccount && !accounts.some(a => a.id === initialAccount)) {
99
+ account.append(new Option('(a saved account that no longer exists)', initialAccount));
82
100
  }
83
- account.value = initial.account ?? '';
101
+ account.value = initialAccount;
84
102
  const ownAccount = () => {
85
103
  field('username').hidden = field('domain').hidden = !!account.value && account.value !== '__new__';
86
104
  };
@@ -95,23 +113,29 @@ function askDesktop(pane, options) {
95
113
  // The gateway's own account: offered once there is a gateway and a saved account to choose.
96
114
  const gateway = field('gateway');
97
115
  const gatewayAccount = field('gatewayAccount');
98
- gateway.value = initial.gateway ?? '';
116
+ gateway.value = (0, desktops_1.configText)(initial.gateway);
99
117
  gatewayAccount.append(new Option('Gateway account: the same as the desktop\'s', ''), ...accounts.map(a => new Option(`Gateway account: ${a.name} (${(0, accounts_1.signInName)(a)})`, a.id)));
100
- if (initial.gatewayAccount && !accounts.some(a => a.id === initial.gatewayAccount)) {
101
- gatewayAccount.append(new Option('(a saved account that no longer exists)', initial.gatewayAccount));
118
+ const initialGatewayAccount = (0, desktops_1.configText)(initial.gatewayAccount);
119
+ if (initialGatewayAccount && !accounts.some(a => a.id === initialGatewayAccount)) {
120
+ gatewayAccount.append(new Option('(a saved account that no longer exists)', initialGatewayAccount));
102
121
  }
103
- gatewayAccount.value = initial.gatewayAccount ?? '';
122
+ gatewayAccount.value = initialGatewayAccount;
104
123
  const gatewayChanged = () => {
105
124
  gatewayAccount.hidden = !gateway.value.trim() || gatewayAccount.options.length < 2;
106
125
  };
107
126
  gateway.addEventListener('input', gatewayChanged);
108
127
  gatewayChanged();
128
+ // Its own clipboard sharing, or the setting's (named, as it is now).
129
+ const clipboard = field('clipboard');
130
+ const lower = (mode) => clipboard_1.CLIPBOARD_LABELS[mode].toLowerCase();
131
+ clipboard.append(new Option(`Clipboard: as in Settings (${lower(options.clipboard)})`, ''), ...Object.keys(clipboard_1.CLIPBOARD_LABELS).map(mode => new Option(`Clipboard: ${lower(mode)}`, mode)));
132
+ clipboard.value = (0, clipboard_1.ownClipboard)(initial.clipboard) ?? '';
109
133
  if (getComputedStyle(pane).position === 'static') {
110
134
  pane.style.position = 'relative';
111
135
  }
112
136
  pane.appendChild(overlay);
113
137
  // Same as a desktop layer: the terminal underneath can't take the keyboard while the form shows.
114
- const inputs = Array.from(pane.querySelectorAll('textarea.xterm-helper-textarea')).filter(t => !t.disabled);
138
+ const inputs = (0, hostCompat_1.terminalInputs)(pane).filter(t => !t.disabled);
115
139
  inputs.forEach(t => { t.disabled = true; });
116
140
  const focusForm = () => {
117
141
  if (!form.contains(document.activeElement)) {
@@ -160,6 +184,8 @@ function askDesktop(pane, options) {
160
184
  const username = field('username').value.trim();
161
185
  const domain = field('domain').value.trim();
162
186
  const kind = field('kind').value;
187
+ // '': the kind the host reported, still its word (see above).
188
+ const picked = kind !== '';
163
189
  const wakeText = field('wake').value.trim();
164
190
  // An unchanged wake keeps what the field doesn't show (a Wake-on-LAN broadcast address and port).
165
191
  const wake = wakeText && wakeText === wakeTextOf(initial) ? initial.wake : (0, wake_1.wakeFromText)(wakeText);
@@ -170,19 +196,29 @@ function askDesktop(pane, options) {
170
196
  name,
171
197
  host: address.host,
172
198
  port: address.port,
173
- kind: kind === 'gnome' || kind === 'xrdp' ? kind : 'windows',
199
+ kind: !picked ? initial.kind : kind === 'gnome' || kind === 'xrdp' ? kind : 'windows',
174
200
  username: username || undefined,
175
201
  domain: domain || undefined,
176
202
  account: account.value && account.value !== '__new__' ? account.value : undefined,
177
203
  wake,
178
204
  gateway: gatewayText || undefined,
179
205
  gatewayAccount: gatewayText && gatewayAccount.value || undefined,
206
+ clipboard: clipboard.value || undefined,
180
207
  };
181
- for (const key of ['username', 'domain', 'account', 'wake', 'gateway', 'gatewayAccount']) {
208
+ for (const key of ['username', 'domain', 'account', 'wake', 'gateway', 'gatewayAccount', 'clipboard']) {
182
209
  if (entry[key] === undefined) {
183
210
  delete entry[key];
184
211
  }
185
212
  }
213
+ // A kind left as the host reported it stays marked so; one picked is the user's, and is marked so only where
214
+ // the entry would otherwise be taken for a VM saved before the mark existed (see xrdpFromHost).
215
+ delete entry.kindFromHost;
216
+ if (!picked) {
217
+ entry.kindFromHost = true;
218
+ }
219
+ else if ((0, desktops_1.xrdpFromHost)(entry)) {
220
+ entry.kindFromHost = false;
221
+ }
186
222
  const refused = options.check?.(entry);
187
223
  if (refused) {
188
224
  error.textContent = refused;
@@ -199,4 +235,3 @@ function askDesktop(pane, options) {
199
235
  });
200
236
  });
201
237
  }
202
- //# sourceMappingURL=desktopForm.js.map
package/dist/desktops.js CHANGED
@@ -2,51 +2,104 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DIRECT_KEY = exports.OWN_DESKTOP = void 0;
4
4
  exports.viaMatches = viaMatches;
5
+ exports.configText = configText;
5
6
  exports.specOf = specOf;
7
+ exports.xrdpFromHost = xrdpFromHost;
6
8
  exports.hyperVSpec = hyperVSpec;
7
9
  exports.desktopsFor = desktopsFor;
8
10
  exports.desktopIdOf = desktopIdOf;
11
+ exports.isDesktopEntry = isDesktopEntry;
12
+ exports.entryText = entryText;
9
13
  exports.sessionKey = sessionKey;
14
+ exports.xrdpUnasked = xrdpUnasked;
15
+ exports.keyFromConfig = keyFromConfig;
16
+ exports.isTargetKey = isTargetKey;
17
+ exports.legacyKeyFromConfig = legacyKeyFromConfig;
18
+ exports.keyFields = keyFields;
19
+ exports.nestedKey = nestedKey;
20
+ exports.shownKey = shownKey;
21
+ exports.keyUser = keyUser;
22
+ exports.keyParts = keyParts;
23
+ exports.rekeyed = rekeyed;
24
+ const clipboard_1 = require("./clipboard");
10
25
  const hyperv_1 = require("./hyperv");
26
+ const osd_1 = require("./osd");
11
27
  const wake_1 = require("./wake");
12
28
  exports.OWN_DESKTOP = 'own';
13
- /** Whether a `via` value names this SSH target: its alias (label), hostname, user@hostname, or full key. */
29
+ /**
30
+ * Whether a `via` value names this SSH target: its alias (label), hostname, user@hostname, or user@hostname:port, as ssh
31
+ * resolved them (or the key itself). Never a machine reached with ssh typed in a host's console: its name, its user and
32
+ * its hostname there are that host's word, which could name any host the user configured desktops behind (and with them
33
+ * their saved accounts). Only its own desktop opens through that host.
34
+ */
14
35
  function viaMatches(via, target) {
15
- const v = via.trim().toLowerCase();
16
- const key = target.key.toLowerCase(); // user@hostname:port
17
- const userHost = key.replace(/:\d+$/, '');
18
- const hostname = userHost.replace(/^[^@]*@/, '');
19
- return !!v && [key, userHost, hostname, target.label.toLowerCase()].includes(v);
36
+ const v = configText(via).trim().toLowerCase();
37
+ if (!v || target.via !== undefined) {
38
+ return false;
39
+ }
40
+ const { user, host, port } = keyFields(target.key);
41
+ // Also what the add form wrote up to 0.5.0 for a user name with an `@` (alice@corp.example): all after the first.
42
+ const written = `${user}@${host}`.replace(/^[^@]*@/, '');
43
+ return [target.key, `${user}@${host}:${port}`, `${user}@${host}`, host, written, target.label].some(name => name.toLowerCase() === v);
44
+ }
45
+ /**
46
+ * A config value as text: a string as it is, a number written out. Anything else is none: String() of an object (a
47
+ * hand edit, config sync) runs its own conversions, which can throw, and every desktop listed with it would fail, as
48
+ * would every menu or list that shows it.
49
+ */
50
+ function configText(value) {
51
+ return typeof value === 'string' ? value : typeof value === 'number' && Number.isFinite(value) ? String(value) : '';
20
52
  }
21
53
  /** The desktop an entry describes (a `remoteDesktop.desktops` entry, or an RDP profile's options), or null if unusable. */
22
54
  function specOf(extra) {
55
+ // Its own clipboard sharing, or none (the setting's).
56
+ const clipboard = (0, clipboard_1.ownClipboard)(extra?.clipboard);
57
+ const own = clipboard ? { clipboard } : {};
23
58
  const hyperv = (0, hyperv_1.vmId)(extra?.hyperv);
24
59
  if (hyperv) {
25
- return hyperVSpec(hyperv, extra.name ? String(extra.name) : hyperv, {
26
- username: extra.username ? String(extra.username) : undefined,
27
- domain: extra.domain ? String(extra.domain) : undefined,
28
- account: extra.account ? String(extra.account) : undefined,
60
+ return hyperVSpec(hyperv, configText(extra.name) || hyperv, {
61
+ username: configText(extra.username) || undefined,
62
+ domain: configText(extra.domain) || undefined,
63
+ account: configText(extra.account) || undefined,
64
+ ...own,
29
65
  });
30
66
  }
31
- const port = Number(extra?.port || 3389);
67
+ // From a number or text only: Number() of an object from the config runs its conversions, and can throw.
68
+ const port = (0, osd_1.configNumber)(extra?.port || 3389);
32
69
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
33
70
  return null;
34
71
  }
35
- const host = String(extra.host || '127.0.0.1');
72
+ const host = extra.host ? configText(extra.host) : '127.0.0.1';
73
+ // No '#' in a host: it is part of the session and trust keys, which are split on '#' (see keyParts).
74
+ if (!host || host.includes('#')) {
75
+ return null;
76
+ }
36
77
  const gateway = typeof extra.gateway === 'string' ? extra.gateway.trim() : '';
37
78
  return {
38
79
  id: `${host}:${port}`,
39
- name: extra.name ? String(extra.name) : `${host}:${port}`,
80
+ name: configText(extra.name) || `${host}:${port}`,
40
81
  kind: extra.kind === 'gnome' || extra.kind === 'xrdp' ? extra.kind : 'windows',
82
+ kindTrusted: !xrdpFromHost(extra),
41
83
  host,
42
84
  port,
43
- username: extra.username ? String(extra.username) : undefined,
44
- domain: extra.domain ? String(extra.domain) : undefined,
45
- account: extra.account ? String(extra.account) : undefined,
85
+ username: configText(extra.username) || undefined,
86
+ domain: configText(extra.domain) || undefined,
87
+ account: configText(extra.account) || undefined,
46
88
  wake: (0, wake_1.parseWake)(extra.wake),
47
- ...gateway ? { gateway, gatewayAccount: extra.gatewayAccount ? String(extra.gatewayAccount) : undefined } : {},
89
+ ...gateway ? { gateway, gatewayAccount: configText(extra.gatewayAccount) || undefined } : {},
90
+ ...own,
48
91
  };
49
92
  }
93
+ /**
94
+ * Whether an entry is of the xrdp kind on a host's word rather than the user's (see DesktopSpec.kindTrusted): a VM
95
+ * saved from the host's list as the host reported it (`kindFromHost`). Up to 0.5.0 those were saved without the mark,
96
+ * and one then looks like this: xrdp, started by its name when it is off (`wake.vm`, which the save always set). Such
97
+ * an entry is taken for the host's word too, unless the user has picked its kind in the form since (`kindFromHost:
98
+ * false`); a desktop of the user's that looks the same asks once before signing in without NLA.
99
+ */
100
+ function xrdpFromHost(extra) {
101
+ return extra.kind === 'xrdp' && (extra.kindFromHost === true || extra.kindFromHost === undefined && !!extra.wake?.vm);
102
+ }
50
103
  /** A Hyper-V VM's console as a desktop of its host: the host's VMConnect port, the VM's id, started when it's off. */
51
104
  function hyperVSpec(id, name, more = {}) {
52
105
  return { id: `hyperv:${id}`, name, kind: 'windows', host: '127.0.0.1', port: hyperv_1.HYPERV_PORT, hyperv: id, wake: { hyperv: id }, ...more };
@@ -56,21 +109,33 @@ function hyperVSpec(id, name, more = {}) {
56
109
  * host). `found`: what the setup found there the last time (none yet: GNOME, as the setup will try first). A host
57
110
  * with only xrdp has it as its own desktop; one with both GNOME and xrdp also offers xrdp, as a desktop at its
58
111
  * loopback port. A Windows host's own desktop is its RDP server, signed in to with the Windows account (the SSH user,
59
- * to start with).
112
+ * to start with). `clipboard`: the setting, which desktops without their own follow.
60
113
  */
61
- function desktopsFor(target, extras, found, more = []) {
114
+ function desktopsFor(target, extras, found, more = [], clipboard = 'both') {
62
115
  const own = found?.kind === 'xrdp'
63
116
  ? { id: exports.OWN_DESKTOP, name: `${target.label} desktop`, kind: 'xrdp', host: '127.0.0.1', port: found.xrdpPort ?? 3389, username: found.xrdpUser }
64
117
  : found?.kind === 'windows'
65
- ? { id: exports.OWN_DESKTOP, name: `${target.label} desktop`, kind: 'windows', host: '127.0.0.1', port: 3389, username: target.key.replace(/@[^@]*$/, '') || undefined }
118
+ ? { id: exports.OWN_DESKTOP, name: `${target.label} desktop`, kind: 'windows', host: '127.0.0.1', port: 3389, username: keyUser(target.key) || undefined }
66
119
  : { id: exports.OWN_DESKTOP, name: `${target.label} desktop`, kind: 'gnome', host: '127.0.0.1', port: 0 };
120
+ // What the setup found, the own desktop's kind and the xrdp besides GNOME below, is the host's word about itself,
121
+ // never trusted (see DesktopSpec.kindTrusted).
67
122
  const specs = [own];
68
123
  const behind = (Array.isArray(extras) ? extras : []).filter(extra => extra?.via && viaMatches(extra.via, target));
69
124
  for (const extra of [...behind, ...more]) {
70
125
  const spec = specOf(extra);
71
- if (spec && !specs.some(s => s.id === spec.id)) {
126
+ const kept = spec && specs.find(s => s.id === spec.id);
127
+ if (spec && !kept) {
72
128
  specs.push(spec);
73
129
  }
130
+ else if (spec && kept) {
131
+ // The same desktop configured twice (an entry and an RDP profile through this host, say): the first is the
132
+ // one that opens, and its clipboard goes no further than the other's would. Left following the setting
133
+ // where the other lets as much through.
134
+ const narrower = (0, clipboard_1.narrowest)(kept.clipboard ?? clipboard, spec.clipboard ?? clipboard);
135
+ if (narrower !== (kept.clipboard ?? clipboard)) {
136
+ kept.clipboard = narrower;
137
+ }
138
+ }
74
139
  }
75
140
  // Configured entries come first: one for the same port keeps its name.
76
141
  if (found?.kind === 'gnome' && found.xrdpPort && !specs.some(s => s.id === `127.0.0.1:${found.xrdpPort}`)) {
@@ -87,9 +152,29 @@ function desktopsFor(target, extras, found, more = []) {
87
152
  }
88
153
  /** The target key of direct connections (remote desktop tabs); their session keys are `rdp#<address>`. */
89
154
  exports.DIRECT_KEY = 'rdp';
90
- /** The id of a configured desktop (`host:port`), as its DesktopSpec and session keys have it. */
155
+ /**
156
+ * The id of a configured desktop (`host:port`), as its DesktopSpec and session keys have it (see specOf), whatever the
157
+ * entry holds: a host or port that is neither text nor a number makes an id that no desktop has, and nothing is kept
158
+ * under it.
159
+ */
91
160
  function desktopIdOf(extra) {
92
- return (0, hyperv_1.vmId)(extra.hyperv) ? `hyperv:${(0, hyperv_1.vmId)(extra.hyperv)}` : `${extra.host || '127.0.0.1'}:${extra.port || 3389}`;
161
+ const hyperv = (0, hyperv_1.vmId)(extra?.hyperv);
162
+ return hyperv ? `hyperv:${hyperv}` : `${extra?.host ? configText(extra.host) : '127.0.0.1'}:${(0, osd_1.configNumber)(extra?.port || 3389)}`;
163
+ }
164
+ /** Whether an item of `remoteDesktop.desktops` is an entry (a map) at all: anything else describes no desktop. */
165
+ function isDesktopEntry(value) {
166
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
167
+ }
168
+ /**
169
+ * A configured desktop's entry as menus and lists show it: its name, else its address (see desktopIdOf), and the host
170
+ * it is behind ('' for none), each as text whatever the entry holds. A hand edit or config sync can put anything there,
171
+ * and an entry that can't be shown must not take with it the menus and lists that show the others. `hyperv`: whether
172
+ * it is a Hyper-V VM, told as specOf tells one, by its VM's id: not by its address, which a desktop at a host named
173
+ * hyperv starts the same way.
174
+ */
175
+ function entryText(extra) {
176
+ const address = desktopIdOf(extra);
177
+ return { name: configText(extra?.name) || address, address, via: configText(extra?.via), hyperv: !!(0, hyperv_1.vmId)(extra?.hyperv) };
93
178
  }
94
179
  /**
95
180
  * One desktop per key: the remote account for its own desktop, plus the RDP server for one behind it (`rdp#<address>`
@@ -98,4 +183,127 @@ function desktopIdOf(extra) {
98
183
  function sessionKey(target, spec) {
99
184
  return spec.id === exports.OWN_DESKTOP ? target.key : `${target.key}#${spec.id}`;
100
185
  }
101
- //# sourceMappingURL=desktops.js.map
186
+ /**
187
+ * Whether a desktop signs in without Network Level Authentication without being asked about it first, as xrdp does by
188
+ * design: an xrdp desktop whose kind is trusted (see DesktopSpec.kindTrusted). Every other desktop stops before the
189
+ * password goes as it is to a server that doesn't use it.
190
+ */
191
+ function xrdpUnasked(spec) {
192
+ return spec.kind === 'xrdp' && spec.kindTrusted === true;
193
+ }
194
+ /** What keyField writes as `%` and two hex digits, in a key's user, host name and port. */
195
+ const ESCAPED_USER = /[%#>\x00-\x1f\x7f]/g;
196
+ const ESCAPED_HOST = /[%#>@\x00-\x1f\x7f]/g;
197
+ const ESCAPED_PORT = /[%#>@:\x00-\x1f\x7f]/g;
198
+ /**
199
+ * A field of `ssh -G` output, fit for a key, so that whatever ssh replies a key keeps its shape and no two targets get
200
+ * the same one: `%` (the escape itself), `#` (it joins a target's key and a desktop's address, see sessionKey), `>` (an
201
+ * SSH tab's key and a machine reached from there, see nestedKey) and control characters are written as `%25`, `%23`,
202
+ * `%3E`, …; so are an `@` in a host name and an `@` or `:` in a port, which would move where the user or the host name
203
+ * ends (see keyFields). A user's own `@` (alice@corp.example), spaces and an IPv6 address's colons stay as they are.
204
+ */
205
+ function keyField(value, escaped) {
206
+ return value.replace(escaped, c => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`);
207
+ }
208
+ /** A field of `ssh -G` output, from the line that names it. */
209
+ function configField(out, name) {
210
+ return new RegExp(`^${name} (.+)$`, 'm').exec(out)?.[1]?.trim();
211
+ }
212
+ /** A target's key: `user@hostname:port` from `ssh -G` output (config aliases and defaults resolved; see keyOf). */
213
+ function keyFromConfig(out, destination) {
214
+ return keyOf({ user: configField(out, 'user') ?? '', host: configField(out, 'hostname') ?? destination, port: configField(out, 'port') ?? '22' });
215
+ }
216
+ /** The key of a target with this user, host name and port (see keyField; keyFields takes it apart again). */
217
+ function keyOf(fields) {
218
+ return `${keyField(fields.user, ESCAPED_USER)}@${keyField(fields.host.toLowerCase(), ESCAPED_HOST)}:${keyField(fields.port, ESCAPED_PORT)}`;
219
+ }
220
+ /**
221
+ * Whether some target could have this key now, reached directly or with ssh typed in a tab's console (see nestedKey):
222
+ * it reads the same taken apart and put back together. A key as the plugin made it up to 0.5.0 (see
223
+ * legacyKeyFromConfig) that does could be another target's now (a user name `alice%23ops` then, `alice#ops`'s now).
224
+ */
225
+ function isTargetKey(key) {
226
+ const parts = key.split('>');
227
+ return parts.length <= 2 && parts.every(part => keyOf(keyFields(part)) === part);
228
+ }
229
+ /**
230
+ * A target's key as the plugin made it up to 0.5.0: the fields as ssh printed them. It reads as keyFromConfig's does
231
+ * unless a field has a character keyField now writes as `%xx`; what was kept for the target before is under this one
232
+ * (see RemoteTargets.keyChanged, which moves it).
233
+ */
234
+ function legacyKeyFromConfig(out, destination) {
235
+ return `${configField(out, 'user') ?? ''}@${(configField(out, 'hostname') ?? destination).toLowerCase()}:${configField(out, 'port') ?? '22'}`;
236
+ }
237
+ /**
238
+ * The user, host name and port in a target's key (for a machine reached through a host, see nestedKey, that machine's)
239
+ * as ssh resolved them: the user is what comes before the last `@` and the port what comes after the last `:`, since
240
+ * keyField leaves neither in a host name or a port, and every `%` in a field is one keyField wrote.
241
+ */
242
+ function keyFields(key) {
243
+ const fields = key.slice(key.lastIndexOf('>') + 1);
244
+ const at = fields.lastIndexOf('@');
245
+ const colon = fields.lastIndexOf(':');
246
+ const end = colon > at ? colon : fields.length;
247
+ const unescape = (field) => field.replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
248
+ return { user: unescape(fields.slice(0, Math.max(at, 0))), host: unescape(fields.slice(at + 1, end)), port: unescape(fields.slice(end + 1)) };
249
+ }
250
+ /**
251
+ * The key of a machine reached with `ssh` typed in an SSH tab's console: the tab's key, `>`, and the machine's own as
252
+ * the host there resolves it (`ssh -G`, run there). Everything about that machine comes from that host, which can make
253
+ * it out to be any machine: under the host's own key, what it names is one of its own desktops, never the key (and with
254
+ * it the saved account, the remembered certificate, or the permission to sign in without NLA) of a desktop reached
255
+ * another way.
256
+ */
257
+ function nestedKey(outerKey, configOut, destination) {
258
+ return `${outerKey}>${keyFromConfig(configOut, destination)}`;
259
+ }
260
+ /**
261
+ * A target's key as it reads, for a list of what is kept by it (Settings › Remote Desktop › Certificates): `%`, `#`,
262
+ * `>`, `@` and `:` as they are rather than as keyField writes them (control characters stay written so), and a machine
263
+ * reached through a host (see nestedKey) named first, through that host.
264
+ */
265
+ function shownKey(key) {
266
+ const fields = (part) => part.replace(/%(25|23|3E|40|3A)/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
267
+ const [outer, inner] = key.split('>');
268
+ return inner === undefined ? fields(outer) : `${fields(inner)}, through ${fields(outer)}`;
269
+ }
270
+ /**
271
+ * The user in a target's key, as ssh resolved it (not as the key writes it, see keyField): for a machine reached through
272
+ * a host (see nestedKey), that machine's.
273
+ */
274
+ function keyUser(key) {
275
+ return keyFields(key).user;
276
+ }
277
+ /**
278
+ * A session, trust or credential key split into the target it belongs to and the desktop id after it, so matching is
279
+ * anchored on whole parts rather than an `endsWith('#'+id)` that a different key could also satisfy. A key is
280
+ * `<target>#<id>`: the target is `rdp` for a direct desktop, `gateway` for a gateway certificate, or `user@host:port`
281
+ * for a desktop behind an SSH host (`<tab's key>><user@host:port>` for a machine reached through one, see nestedKey;
282
+ * no target's key has a `#`, see keyField); the own desktop's key is the bare target, with no id. A credential key
283
+ * may carry a gateway suffix (`…@gateway#<host:port>`), which names where a password is sent rather than the desktop,
284
+ * and is returned separately so the id still compares equal.
285
+ */
286
+ function keyParts(key) {
287
+ const hash = key.indexOf('#');
288
+ if (hash < 0) {
289
+ return { target: key, desktopId: '', gateway: '' };
290
+ }
291
+ const rest = key.slice(hash + 1);
292
+ const at = rest.indexOf('@gateway#');
293
+ return at < 0
294
+ ? { target: key.slice(0, hash), desktopId: rest, gateway: '' }
295
+ : { target: key.slice(0, hash), desktopId: rest.slice(0, at), gateway: rest.slice(at) };
296
+ }
297
+ /**
298
+ * A session, trust or credential key of the target `from`, as the same key of `to` (see RemoteTargets.keyChanged,
299
+ * which moves what was kept under a target's former key): `from` itself (its own desktop), or `from#<id>`, with a
300
+ * gateway suffix on a saved password's key. Null for any other key. Desktop ids have no `#` (see specOf), so a key that
301
+ * has more of them isn't one of `from`'s desktops, whatever it starts with.
302
+ */
303
+ function rekeyed(key, from, to) {
304
+ if (key === from) {
305
+ return to;
306
+ }
307
+ const rest = key.startsWith(`${from}#`) ? key.slice(from.length + 1) : null;
308
+ return rest !== null && /^[^#]*(@gateway#[^#]*)?$/.test(rest) ? `${to}#${rest}` : null;
309
+ }