tabby-rdp 0.5.0 → 0.5.1

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 (79) hide show
  1. package/CHANGELOG.md +490 -0
  2. package/README.md +177 -48
  3. package/dist/accountForm.js +0 -1
  4. package/dist/accounts.js +0 -1
  5. package/dist/audio.js +0 -1
  6. package/dist/authorities.js +331 -0
  7. package/dist/byteQueue.js +102 -0
  8. package/dist/clipboard.js +106 -0
  9. package/dist/connectionStatus.js +0 -1
  10. package/dist/desk.js +16 -47
  11. package/dist/deskScript.js +196 -6
  12. package/dist/desktop.service.js +1442 -244
  13. package/dist/desktopForm.js +51 -17
  14. package/dist/desktops.js +232 -24
  15. package/dist/drives.js +271 -30
  16. package/dist/fileTransfer.js +958 -83
  17. package/dist/gateway.js +216 -57
  18. package/dist/header.js +76 -5
  19. package/dist/help.js +34 -13
  20. package/dist/hyperv.js +65 -15
  21. package/dist/index.js +2 -2
  22. package/dist/keyboard.js +0 -1
  23. package/dist/microphone.js +57 -13
  24. package/dist/ntlm.js +0 -1
  25. package/dist/osd.js +56 -15
  26. package/dist/probe.js +15 -3
  27. package/dist/rdcleanpath.js +319 -44
  28. package/dist/rdpFile.js +22 -7
  29. package/dist/rdpProfile.js +39 -21
  30. package/dist/remoteSetup.js +103 -23
  31. package/dist/sessionLog.js +53 -0
  32. package/dist/settingsPage.js +65 -29
  33. package/dist/signin.js +407 -40
  34. package/dist/ssh.js +99 -18
  35. package/dist/sshChannelStream.js +40 -6
  36. package/dist/targets.js +185 -39
  37. package/dist/ui.js +88 -20
  38. package/dist/unshowable.js +41 -0
  39. package/dist/updates.js +37 -2
  40. package/dist/vms.js +17 -3
  41. package/dist/wake.js +15 -8
  42. package/package.json +4 -3
  43. package/remote/trd-pty.py +74 -13
  44. package/vendor/SHA256SUMS +26 -0
  45. package/vendor/ironrdp_web_bg.wasm +0 -0
  46. package/dist/accountForm.js.map +0 -1
  47. package/dist/accounts.js.map +0 -1
  48. package/dist/audio.js.map +0 -1
  49. package/dist/connectionStatus.js.map +0 -1
  50. package/dist/desk.js.map +0 -1
  51. package/dist/deskScript.js.map +0 -1
  52. package/dist/desktop.service.js.map +0 -1
  53. package/dist/desktopForm.js.map +0 -1
  54. package/dist/desktops.js.map +0 -1
  55. package/dist/drives.js.map +0 -1
  56. package/dist/fileTransfer.js.map +0 -1
  57. package/dist/gateway.js.map +0 -1
  58. package/dist/header.js.map +0 -1
  59. package/dist/help.js.map +0 -1
  60. package/dist/hyperv.js.map +0 -1
  61. package/dist/index.js.map +0 -1
  62. package/dist/keyboard.js.map +0 -1
  63. package/dist/microphone.js.map +0 -1
  64. package/dist/ntlm.js.map +0 -1
  65. package/dist/osd.js.map +0 -1
  66. package/dist/probe.js.map +0 -1
  67. package/dist/rdcleanpath.js.map +0 -1
  68. package/dist/rdpFile.js.map +0 -1
  69. package/dist/rdpProfile.js.map +0 -1
  70. package/dist/remoteSetup.js.map +0 -1
  71. package/dist/settingsPage.js.map +0 -1
  72. package/dist/signin.js.map +0 -1
  73. package/dist/ssh.js.map +0 -1
  74. package/dist/sshChannelStream.js.map +0 -1
  75. package/dist/targets.js.map +0 -1
  76. package/dist/ui.js.map +0 -1
  77. package/dist/updates.js.map +0 -1
  78. package/dist/vms.js.map +0 -1
  79. package/dist/wake.js.map +0 -1
@@ -18,9 +18,10 @@ const desktopForm_1 = require("./desktopForm");
18
18
  const help_1 = require("./help");
19
19
  const accounts_1 = require("./accounts");
20
20
  const accountForm_1 = require("./accountForm");
21
+ const clipboard_1 = require("./clipboard");
21
22
  const desktops_1 = require("./desktops");
22
23
  const gateway_1 = require("./gateway");
23
- const signin_1 = require("./signin");
24
+ const osd_1 = require("./osd");
24
25
  const ssh_1 = require("./ssh");
25
26
  const targets_1 = require("./targets");
26
27
  /** Input of an SSH tab opened for an RDP profile with `via`: the desktop to show once SSH is connected. */
@@ -134,7 +135,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
134
135
  const o = this.profile.options;
135
136
  const account = () => `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}`;
136
137
  // No id for a new profile (no address yet): it has nothing saved to take along.
137
- this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '' };
138
+ this.before = { id: o.host ? (0, desktops_1.desktopIdOf)(o) : '', account: account(), via: o.via ?? '', gateway: o.gateway ?? '' };
138
139
  const root = this.element.nativeElement;
139
140
  root.innerHTML = `
140
141
  <div class="mb-3">
@@ -179,6 +180,12 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
179
180
  <label>Gateway account</label>
180
181
  <select class="form-control" name="gatewayAccount"></select>
181
182
  <div class="text-muted small">A gateway that takes another account than the desktop: a saved account for it.</div>
183
+ </div>
184
+ <div class="mb-3">
185
+ <label>Clipboard</label>
186
+ <select class="form-control" name="clipboard"></select>
187
+ <div class="text-muted small">Which ways text, pictures and files copied on either side go between this computer and the desktop.
188
+ Applies on the next connection.</div>
182
189
  </div>`;
183
190
  const field = (name) => root.querySelector(`[name="${name}"]`);
184
191
  field('address').value = o.host ? (0, desktopForm_1.formatAddress)(o.host, o.port || 3389) : '';
@@ -240,6 +247,11 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
240
247
  });
241
248
  gatewayAccount.addEventListener('change', () => { o.gatewayAccount = gatewayAccount.value; });
242
249
  gatewayChanged();
250
+ // Its own clipboard sharing, or the setting's (named, as it is now).
251
+ const clipboard = field('clipboard');
252
+ clipboard.append(new Option(`As in Settings › Remote Desktop (${clipboard_1.CLIPBOARD_LABELS[desktop.settings().clipboard].toLowerCase()})`, ''), ...Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, text]) => new Option(text, mode)));
253
+ clipboard.value = (0, clipboard_1.ownClipboard)(o.clipboard) ?? '';
254
+ clipboard.addEventListener('change', () => { o.clipboard = clipboard.value; });
243
255
  const via = field('via');
244
256
  via.addEventListener('change', () => { o.via = via.value; });
245
257
  this.injector.get(tabby_core_1.ProfilesService).getProfiles().then(profiles => {
@@ -253,21 +265,22 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
253
265
  }
254
266
  save() {
255
267
  const o = this.profile.options;
256
- const { id, account, via } = this.before;
268
+ const { id, account, via, gateway } = this.before;
257
269
  if (!id) {
258
270
  return;
259
271
  }
260
272
  if ((o.via ?? '') !== via) {
261
- // Reached another way, it is another desktop. A direct one's saved account and certificate were its own; one
262
- // behind a host shares them with that host's desktop at the same address, so they stay.
273
+ // Reached another way, it is another desktop. A direct one's saved passwords (through any gateway, the ones
274
+ // it had before included) and certificate were its own; one behind a host shares them with that host's
275
+ // desktop at the same address, so they stay.
263
276
  if (!via) {
264
- (0, signin_1.forgetCredentials)(`${desktops_1.DIRECT_KEY}#${id}`);
265
- this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
277
+ this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop(id, true, `"${this.profile.name}"`).catch(() => null);
266
278
  }
267
279
  return;
268
280
  }
281
+ // Its saved password goes with its way there (see desktopEdited): another gateway is asked for again.
269
282
  const accountChanged = `${o.username ?? ''}\n${o.domain ?? ''}\n${o.account ?? ''}` !== account;
270
- this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via).catch(() => null);
283
+ this.injector.get(desktop_service_1.RemoteDesktopService).desktopEdited(id, (0, desktops_1.desktopIdOf)(o), accountChanged, !via, { from: gateway, to: o.gateway }).catch(() => null);
271
284
  }
272
285
  };
273
286
  exports.RDPProfileSettingsComponent = RDPProfileSettingsComponent;
@@ -289,11 +302,14 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
289
302
  this.name = 'Remote desktop (RDP)';
290
303
  this.settingsComponent = RDPProfileSettingsComponent;
291
304
  this.configDefaults = {
292
- options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '' },
305
+ options: { host: '', port: 3389, kind: 'windows', username: '', domain: '', account: '', via: '', gateway: '', gatewayAccount: '', clipboard: '' },
293
306
  clearServiceMessagesOnConnect: false,
294
307
  };
295
308
  }
296
309
  async getBuiltinProfiles() {
310
+ // No clipboard here: a new profile is made from a copy of this, and a value saved with it (even '', the
311
+ // setting's) would hide the clipboard its group's or the type's defaults give it, which may well be narrower.
312
+ // Left out, the profile follows those defaults (configDefaults has the key) until it is given one of its own.
297
313
  return [{
298
314
  id: `${targets_1.RDP_PROFILE_TYPE}:template`,
299
315
  type: targets_1.RDP_PROFILE_TYPE,
@@ -323,13 +339,17 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
323
339
  }
324
340
  getDescription(profile) {
325
341
  const o = profile.options ?? {};
326
- if (!o.host) {
342
+ // Each field as text whatever the config holds (see configText): Tabby asks this of every profile as it lists
343
+ // them in its profile selector, and one that throws keeps the selector from opening at all.
344
+ const host = (0, desktops_1.configText)(o.host);
345
+ if (!host) {
327
346
  return '';
328
347
  }
329
- const address = (0, desktopForm_1.formatAddress)(o.host, o.port || 3389);
348
+ const address = (0, desktopForm_1.formatAddress)(host, (0, osd_1.configNumber)(o.port || 3389));
349
+ const gateway = (0, desktops_1.configText)(o.gateway);
330
350
  const via = [
331
- ...o.gateway ? [`the gateway ${o.gateway}`] : [],
332
- ...o.via ? [(this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name ?? 'an SSH profile'] : [],
351
+ ...gateway ? [`the gateway ${gateway}`] : [],
352
+ ...o.via ? [(0, desktops_1.configText)((this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name) || 'an SSH profile'] : [],
333
353
  ];
334
354
  return via.length ? `${address} via ${via.join(', via ')}` : address;
335
355
  }
@@ -348,16 +368,15 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
348
368
  }
349
369
  intoQuickConnectString(profile) {
350
370
  const o = profile.options;
351
- return o.via ? null : `${o.username ? `${o.username}@` : ''}${(0, desktopForm_1.formatAddress)(o.host, o.port || 3389)}`;
371
+ // As text, as getDescription reads them: the profile selector asks this of every profile too.
372
+ const username = (0, desktops_1.configText)(o.username);
373
+ return o.via ? null : `${username ? `${username}@` : ''}${(0, desktopForm_1.formatAddress)((0, desktops_1.configText)(o.host), (0, osd_1.configNumber)(o.port || 3389))}`;
352
374
  }
353
375
  deleteProfile(profile) {
354
- // A direct desktop's saved account and certificate are its own; one behind a host shares them with that host's
355
- // desktop at the same address.
376
+ // A direct desktop's saved passwords (through any gateway: one it went through before too) and certificate are
377
+ // its own; one behind a host shares them with that host's desktop at the same address.
356
378
  if (!profile.options.via) {
357
- const id = (0, desktops_1.desktopIdOf)(profile.options);
358
- (0, signin_1.forgetCredentials)(`${desktops_1.DIRECT_KEY}#${id}`);
359
- this.injector.get(desktop_service_1.RemoteDesktopService).forgetCertificatesFor(id, true);
360
- this.config.save();
379
+ this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop((0, desktops_1.desktopIdOf)(profile.options), true, `"${profile.name}"`).catch(() => null);
361
380
  }
362
381
  }
363
382
  };
@@ -427,4 +446,3 @@ exports.RDPProfileOpener = RDPProfileOpener = __decorate([
427
446
  (0, core_1.Injectable)(),
428
447
  __metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
429
448
  ], RDPProfileOpener);
430
- //# sourceMappingURL=rdpProfile.js.map
@@ -3,14 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.prepareRemoteDesktop = prepareRemoteDesktop;
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
+ const desktops_1 = require("./desktops");
6
7
  const rdcleanpath_1 = require("./rdcleanpath");
7
8
  // Runs as the SSH user; needs no root. Prints exactly one `RD_OK port=N user=U pass=P cert=SHA256`,
8
9
  // `RD_XRDP port=N user=U` (no GNOME, but xrdp), `RD_WINDOWS` (a Windows host's POSIX sh) or `RD_ERR <reason>` line;
9
- // with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already, and
10
- // `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over.
10
+ // with RD_OK, also an RD_XRDP line when xrdp runs besides GNOME, `RD_CLIENTS N`, the clients connected already,
11
+ // `RD_TAKEN_OVER <client> <seconds ago>` after a recent take-over, `RD_DESK <key>` with 'desk' on, and an `RD_NOTE <text>`
12
+ // for each rc file it couldn't change.
11
13
  // The RDP password is generated once per remote user and kept in a 0600 file; grd reads credentials only
12
14
  // at startup, so grd is restarted only when its configuration actually changes (that drops live sessions).
13
- // (grdctl takes the credentials as arguments, so they are briefly visible in the remote's process list.)
15
+ // Secrets never go on a command line, which any user there can see in the process list, except where grdctl can't
16
+ // read the password from its input: then it is an argument once, when the credentials are set.
14
17
  const SETUP_SCRIPT = String.raw `
15
18
  say () { printf '%s\n' "$*"; }
16
19
  fail () { say "RD_ERR $*"; exit 0; }
@@ -59,12 +62,49 @@ OLD="$HOME/.local/share/tabby-remote-desktop"
59
62
  if [ -d "$OLD" ] && [ ! -e "$D" ]; then
60
63
  mv "$OLD" "$D" || fail "could not move $OLD to $D"
61
64
  fi
62
- for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
63
- if [ -f "$rc" ] && grep -qF '# tabby-remote-desktop' "$rc"; then
64
- sed -i.trd-bak '/# tabby-remote-desktop$/d' "$rc" && rm -f "$rc.trd-bak"
65
+ mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
66
+ # The login hook ('desk'): this one line in ~/.bashrc and ~/.zshrc, added and removed exactly, so that a line of the
67
+ # user's own that only looks like it stays.
68
+ HOOK='[ -f "$HOME/.local/share/tabby-rdp/login.sh" ] && . "$HOME/.local/share/tabby-rdp/login.sh" # tabby-rdp'
69
+ # Takes the lines that are exactly $2 out of rc file $1 and changes nothing else. grep reads it byte by byte, as text
70
+ # (LC_ALL=C, -a): otherwise a NUL byte, or a byte the host's locale can't read, has it leave lines out or print "Binary
71
+ # file … matches" in their place. What it leaves must be the file less those lines, to the byte, and is written into
72
+ # the file itself, so that a symbolic link to it, another name for it (a hard link), its mode and its owner all stay.
73
+ # A file that can't be written (read-only) or doesn't add up is left as it is, and the setup says so (RD_NOTE).
74
+ unhook () {
75
+ [ -f "$1" ] || return 0
76
+ LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null
77
+ case $? in 0) ;; 1) return 0 ;; *) say "RD_NOTE desk's line may be in $1, which can't be read"; return 0 ;; esac
78
+ uh_tmp=$(mktemp "$D/rc.XXXXXX") || { say "RD_NOTE desk's line stays in $1: no temporary file could be made in $D"; return 0; }
79
+ LC_ALL=C grep -a -v -xF -- "$2" "$1" > "$uh_tmp"
80
+ # 1: no line left, the file held only the hook. Each line taken out goes with its newline, and grep ends a last line
81
+ # that has none with one: so what is left is shorter by exactly that, or one byte longer than that. A step that fails
82
+ # counts as a mismatch (rather than ending the script, as a failed sum would).
83
+ if [ $? -le 1 ] && uh_was=$(wc -c < "$1") && uh_lines=$(LC_ALL=C grep -a -c -xF -- "$2" "$1") && uh_now=$(wc -c < "$uh_tmp"); then
84
+ uh_more=$(( $uh_now - ($uh_was - $uh_lines * (${"$"}{#2} + 1)) ))
85
+ else
86
+ uh_more=-1
65
87
  fi
88
+ if [ "$uh_more" != 0 ] && [ "$uh_more" != 1 ]; then
89
+ say "RD_NOTE desk's line stays in $1: the rest of it didn't copy exactly"
90
+ elif ! cat "$uh_tmp" 2>/dev/null > "$1"; then
91
+ # Not opened (read-only), the file is as it was, its line in it. Otherwise writing it failed on the way (a full
92
+ # disk): what it is to hold stays where the user can find it.
93
+ if ! LC_ALL=C grep -a -q -xF -- "$2" "$1" 2>/dev/null; then
94
+ say "RD_NOTE writing $1 failed; what it should hold (without desk's line) is in $uh_tmp"
95
+ return 0
96
+ fi
97
+ say "RD_NOTE desk's line stays in $1: it can't be written (read-only?)"
98
+ fi
99
+ rm -f "$uh_tmp"
100
+ }
101
+ # The hook from before 0.2, as those versions wrote it, and what it sources, where the old folder is still there (it
102
+ # couldn't be moved over): so that a copy of the line that was changed since, and stays, does nothing either.
103
+ OLD_HOOK='[ -f "$HOME/.local/share/tabby-remote-desktop/login.sh" ] && . "$HOME/.local/share/tabby-remote-desktop/login.sh" # tabby-remote-desktop'
104
+ for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
105
+ unhook "$rc" "$OLD_HOOK"
66
106
  done
67
- mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
107
+ rm -f "$OLD/login.sh"
68
108
  NEW_CERT=0
69
109
  if [ ! -s "$D/tls.key" ]; then
70
110
  openssl req -new -newkey rsa:3072 -days 3650 -nodes -x509 -subj "/CN=$(hostname)" \
@@ -77,8 +117,29 @@ CERT=$(openssl x509 -in "$D/tls.crt" -noout -fingerprint -sha256 2>/dev/null | s
77
117
  # 'desk' support (setting, off by default): interactive SSH logins start inside a shareable session (trd-pty,
78
118
  # or tmux), so 'desk' can attach this exact console to a terminal on the desktop. Rewritten on every connect;
79
119
  # opt out per login with TABBY_NO_SESSION=1. When the setting is off, the hook and helpers are removed.
120
+ DESK_KEY=
80
121
  if [ "${"$"}{TRD_DESK:-0}" = 1 ]; then
81
122
  mkdir -p "$D/bin"
123
+ # 'desk' sends this key with each request, and the plugin (told it below) ignores requests without it: anything shown
124
+ # in a terminal could print the rest. Only this user can read it. Made once, like the RDP password: a key is a file of
125
+ # this user's own (not a link), private, holding 128 bits in hex.
126
+ desk_key () {
127
+ DESK_KEY=
128
+ [ -f "$D/desk-key" ] && [ ! -L "$D/desk-key" ] && [ -O "$D/desk-key" ] || return 1
129
+ DESK_KEY=$(cat "$D/desk-key")
130
+ case $DESK_KEY in *[!0-9a-f]*) DESK_KEY= ;; esac
131
+ [ ${"$"}{#DESK_KEY} = 32 ] || { DESK_KEY=; return 1; }
132
+ chmod 600 "$D/desk-key"
133
+ }
134
+ if ! desk_key; then
135
+ # Put in place in one step, where there is none: of two setups making one at once (two panes, or two computers),
136
+ # the first one's stays, and both report it.
137
+ T=$(mktemp "$D/desk-key.XXXXXX") && od -An -N16 -tx1 /dev/urandom | tr -d ' \n' > "$T" || fail "could not create a key for desk"
138
+ { [ -e "$D/desk-key" ] || [ -L "$D/desk-key" ]; } && ! desk_key && rm -f "$D/desk-key"
139
+ ln "$T" "$D/desk-key" 2>/dev/null || desk_key || mv -f "$T" "$D/desk-key"
140
+ rm -f "$T"
141
+ desk_key || fail "could not create a key for desk"
142
+ fi
82
143
  cat > "$D/login.sh" <<'TRD_EOF'
83
144
  # tabby-rdp ('desk' setting): interactive SSH logins run inside a shareable session, so 'desk' in Tabby can
84
145
  # bring this console to the remote desktop. Backend ("native" trd-pty or "tmux") is in $TRD_HOME/backend.
@@ -128,7 +189,9 @@ fi
128
189
  # Which machine this is: a shared home folder puts this script on others too, and the tab may be connected to another
129
190
  # machine than the one this runs on (ssh typed in its console).
130
191
  MACHINE=$(cat /etc/machine-id 2>/dev/null || hostname)
131
- SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")" "$(b64 "$MACHINE")" "$(b64 "$(hostname)")")
192
+ # What Tabby knows a real request by: other output can't read this.
193
+ KEY=$(cat "$HOME/.local/share/tabby-rdp/desk-key" 2>/dev/null)
194
+ SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")" "$(b64 "$MACHINE")" "$(b64 "$(hostname)")" "$(b64 "$KEY")")
132
195
  if [ "$KIND" = tmux ]; then
133
196
  printf '\033Ptmux;\033%s\033\\' "$SEQ" # tmux only lets it through as passthrough
134
197
  else
@@ -143,19 +206,18 @@ chmod 755 "$D/bin/trd-pty"
143
206
  printf '%s\n' "${"$"}{TRD_BACKEND:-native}" > "$D/backend"
144
207
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
145
208
  [ -f "$rc" ] || continue
146
- grep -qF '# tabby-rdp' "$rc" && continue
147
- # Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
148
- [ -z "$(tail -c1 "$rc")" ] || echo >> "$rc"
149
- printf '[ -f "$HOME/.local/share/tabby-rdp/login.sh" ] && . "$HOME/.local/share/tabby-rdp/login.sh" # tabby-rdp\n' >> "$rc"
209
+ LC_ALL=C grep -a -q -xF -- "$HOOK" "$rc" && continue
210
+ # Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was. A line of its own:
211
+ # a newline first, unless the file is empty or its last byte is one (counted, as a NUL there reads as nothing).
212
+ { [ ! -s "$rc" ] || [ $(( $(tail -c1 "$rc" | wc -l) )) = 1 ] || echo; printf '%s\n' "$HOOK"; } 2>/dev/null >> "$rc" \
213
+ || say "RD_NOTE desk's line couldn't be added to $rc (read-only?): logins there don't start in a shared session"
150
214
  done
151
215
  else
152
216
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
153
- if [ -f "$rc" ] && grep -qF '# tabby-rdp' "$rc"; then
154
- sed -i.trd-bak '/# tabby-rdp$/d' "$rc" && rm -f "$rc.trd-bak"
155
- fi
217
+ unhook "$rc" "$HOOK"
156
218
  done
157
219
  # Sessions already running keep going; new logins are plain again.
158
- rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty"
220
+ rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty" "$D/desk-key"
159
221
  fi
160
222
  if [ ! -s "$D/rdp-password" ]; then
161
223
  ( umask 077; od -An -N18 -tx1 /dev/urandom | tr -d ' \n' > "$D/rdp-password" ) || fail "could not create credentials"
@@ -164,7 +226,11 @@ RD_USER=tabby
164
226
  RD_PASS=$(cat "$D/rdp-password")
165
227
 
166
228
  STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
167
- has () { printf '%s\n' "$STATUS" | grep -qxF "$(printf '\t%s' "$1")"; }
229
+ TAB=$(printf '\t') NL='
230
+ '
231
+ # Whether grd's status has this line. Compared in the shell: as a command's argument, the password would show in the
232
+ # process list on every connect.
233
+ has () { case "$NL$STATUS$NL" in *"$NL$TAB$1$NL"*) return 0 ;; esac; return 1; }
168
234
  g () { grdctl --headless rdp "$@" >/dev/null 2>&1; }
169
235
  RESTART=0
170
236
  # grd reads the certificate at startup: a new one at the same path needs a restart too, or grd would keep serving
@@ -176,7 +242,18 @@ fi
176
242
  if ! has "View-only: no"; then g disable-view-only || fail "grdctl could not disable view-only"; RESTART=1; fi
177
243
  if ! has "Status: enabled"; then g enable || fail "grdctl could not enable RDP"; RESTART=1; fi
178
244
  if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
179
- g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
245
+ # Left off the command line, the password stays out of the process list: grdctl versions that ask for it then read
246
+ # it from their input. Where that didn't take, it goes as an argument. Without timeout(1) on the host, grdctl runs
247
+ # as it is: its input is a pipe that ends after the password, so it doesn't wait for more.
248
+ if command -v timeout >/dev/null 2>&1; then
249
+ printf '%s\n' "$RD_PASS" | timeout 10 grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
250
+ else
251
+ printf '%s\n' "$RD_PASS" | grdctl --headless rdp set-credentials "$RD_USER" >/dev/null 2>&1
252
+ fi
253
+ STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
254
+ if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
255
+ g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
256
+ fi
180
257
  RESTART=1
181
258
  fi
182
259
 
@@ -309,6 +386,7 @@ if [ -s "$D/taken-over" ]; then
309
386
  fi
310
387
  # xrdp besides GNOME (on a port of its own; on grd's, one of them couldn't listen): offered as another desktop.
311
388
  [ -z "$XRDP_PORT" ] || [ "$XRDP_PORT" = "$PORT" ] || say "RD_XRDP port=$XRDP_PORT user=$(id -un)"
389
+ [ -z "$DESK_KEY" ] || say "RD_DESK $DESK_KEY"
312
390
  say "RD_OK port=$PORT user=$RD_USER pass=$RD_PASS cert=$CERT"
313
391
  `;
314
392
  let trdPtySource = null;
@@ -343,9 +421,10 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
343
421
  // it worth asking, so a Linux host costs nothing extra.
344
422
  if (/^RD_WINDOWS$/m.test(out) || !/^RD_(OK|XRDP|ERR) /m.test(out) && await isWindows(target)) {
345
423
  // Its RDP server makes its own certificate: trusted on first use, like a desktop behind the host.
346
- return { kind: 'windows', port: 3389, username: target.key.replace(/@[^@]*$/, ''), password: '', certificate: '' };
424
+ return { kind: 'windows', port: 3389, username: (0, desktops_1.keyUser)(target.key), password: '', certificate: '' };
347
425
  }
348
- const err = /^RD_ERR (.*)$/m.exec(out);
426
+ // The host's reason, as much of it as a status line shows: the host decides how long the line is.
427
+ const err = /^RD_ERR (.{0,1000})/m.exec(out);
349
428
  if (err) {
350
429
  throw new Error(err[1]);
351
430
  }
@@ -354,13 +433,15 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
354
433
  if (ok) {
355
434
  const certificate = (0, rdcleanpath_1.normalizeFingerprint)(ok[4]);
356
435
  if (!certificate) {
357
- throw new Error(`Remote setup reported no usable certificate fingerprint (${ok[4]})`);
436
+ // What the host put there, as long as it likes: as much of it as a fingerprint takes, for a status line.
437
+ throw new Error(`Remote setup reported no usable certificate fingerprint (${ok[4].slice(0, 100)})`);
358
438
  }
359
439
  const clients = Number(/^RD_CLIENTS (\d+)$/m.exec(out)?.[1] ?? 0);
360
440
  const taken = /^RD_TAKEN_OVER (\S+) (\d+)$/m.exec(out);
361
441
  return {
362
442
  kind: 'gnome', port: Number(ok[1]), username: ok[2], password: ok[3], certificate, xrdpPort: xrdp ? Number(xrdp[1]) : undefined, xrdpUser: xrdp?.[2],
363
- clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined,
443
+ clients, takenOver: taken && taken[1] !== client ? Number(taken[2]) : undefined, deskKey: /^RD_DESK ([0-9a-f]{32})$/m.exec(out)?.[1],
444
+ notes: [...out.matchAll(/^RD_NOTE (.+)$/gm)].map(m => m[1]),
364
445
  };
365
446
  }
366
447
  if (xrdp) {
@@ -369,4 +450,3 @@ async function prepareRemoteDesktop(target, desk, backend = 'native', client = '
369
450
  }
370
451
  throw new Error('Remote setup gave no result');
371
452
  }
372
- //# sourceMappingURL=remoteSetup.js.map
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionLog = void 0;
4
+ /**
5
+ * A line as a string of its own. V8 keeps a part cut out of a string (a slice, a pattern's match; 13 characters or
6
+ * more) as a view of the string it came from, and joining strings as a list of them: a short line made from a host's
7
+ * answer of megabytes would keep all of it in memory for as long as the line is kept. Joined to a character and cut
8
+ * from it again: V8 copies a joined string's characters into one string before it cuts a part out of it, so the line
9
+ * is a part of that copy, every character as it was, unpaired surrogates included. A copy through UTF-16 and back
10
+ * does the same at several times the cost, which every line logged pays.
11
+ */
12
+ function copied(line) {
13
+ return (' ' + line).slice(1);
14
+ }
15
+ /**
16
+ * A desktop's connection log (DesktopSession.log): what the settings page's Copy log hands out. Much of it is of the
17
+ * remote's making (an RD Gateway's administrator's messages, a server's), which decides how many lines there are and
18
+ * when, for as long as the desktop stays open: kept whole, they could fill Tabby's memory and crash its window. So the
19
+ * log keeps the first lines (how the connection was set up) and the latest ones, with a line between them saying how
20
+ * many were left out, and each line is cut at MAX_LINE characters and kept as a copy of its own (see copied), which
21
+ * the text it was made from doesn't stay in memory with.
22
+ */
23
+ class SessionLog extends Array {
24
+ constructor() {
25
+ super(...arguments);
26
+ /** How many lines were left out so far. */
27
+ this.dropped = 0;
28
+ }
29
+ // What is made from the log (filter, slice, map) is a plain array.
30
+ static get [Symbol.species]() {
31
+ return Array;
32
+ }
33
+ push(...lines) {
34
+ const { FIRST, LATEST, MAX_LINE } = SessionLog;
35
+ super.push(...lines.map(line => typeof line === 'string' ? copied(line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}…` : line) : line));
36
+ // Once the latest lines are twice as many as are kept, the older half of them goes in one step: each line then
37
+ // costs the same however many come.
38
+ const said = this.dropped ? 1 : 0;
39
+ const over = this.length - FIRST - said - LATEST;
40
+ if (over >= LATEST) {
41
+ this.dropped += over;
42
+ this.splice(FIRST, said + over, `(${this.dropped} lines left out here)`);
43
+ }
44
+ return this.length;
45
+ }
46
+ }
47
+ exports.SessionLog = SessionLog;
48
+ /** The first lines, kept whatever follows. */
49
+ SessionLog.FIRST = 200;
50
+ /** At least this many of the latest lines are kept (and at most twice as many, see push). */
51
+ SessionLog.LATEST = 2000;
52
+ /** The longest a line is kept. */
53
+ SessionLog.MAX_LINE = 4096;
@@ -15,6 +15,7 @@ const path_1 = require("path");
15
15
  const tabby_core_1 = require("tabby-core");
16
16
  const tabby_settings_1 = require("tabby-settings");
17
17
  const accounts_1 = require("./accounts");
18
+ const clipboard_1 = require("./clipboard");
18
19
  const desktop_service_1 = require("./desktop.service");
19
20
  const desktops_1 = require("./desktops");
20
21
  const drives_1 = require("./drives");
@@ -22,6 +23,7 @@ const osd_1 = require("./osd");
22
23
  const help_1 = require("./help");
23
24
  const updates_1 = require("./updates");
24
25
  const targets_1 = require("./targets");
26
+ const unshowable_1 = require("./unshowable");
25
27
  const REPO = 'https://github.com/meanaverage/tabby-rdp';
26
28
  const k = (s) => `<kbd>${(0, help_1.esc)(s)}</kbd>`;
27
29
  /** The page's tabs (as Tabby's "Profiles & connections" page has Profiles and Advanced), each holding sections by topic. */
@@ -59,7 +61,7 @@ const STYLE = `
59
61
  .trd-settings .trd-card { display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: 6px;
60
62
  border: 1px solid rgba(128, 128, 128, 0.25); }
61
63
  .trd-settings .trd-card .trd-card-title { font-weight: 600; display: flex; align-items: center; gap: 6px; }
62
- .trd-settings h4 .trd-info, .trd-settings .trd-lead .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
64
+ .trd-settings h4 .trd-info, .trd-settings .trd-lead .trd-info, .trd-settings .form-line .title .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
63
65
  .trd-settings .trd-info { position: relative; display: inline-flex; opacity: 0.55; cursor: help; outline: none; font-weight: normal; }
64
66
  .trd-settings .trd-info:hover, .trd-settings .trd-info:focus { opacity: 1; }
65
67
  .trd-settings .trd-info::after { content: attr(data-tip); position: absolute; left: 0; top: calc(100% + 6px); z-index: 20; width: 260px;
@@ -265,7 +267,20 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
265
267
 
266
268
  <h5>Sound</h5>
267
269
  ${this.toggleLine('sound', 'Sound', 'Play the remote desktop\'s sound here. Applies on the next connection.')}
268
- ${this.toggleLine('microphone', 'Microphone', 'Send your microphone while an app there records, with a red dot in the corner meanwhile. Applies on the next connection.')}
270
+ ${this.toggleLine('microphone', 'Microphone', 'Send your microphone to a remote desktop that asks for it, as one does while an app there records (a call, say). Meanwhile a red dot shows in the desktop\'s corner, and a red microphone in Tabby\'s header (in full screen, at the top of the window), also while the desktop is hidden. Its menu shows that desktop, or stops sending the microphone there until Send the microphone is turned back on in the desktop\'s menu, or Tabby quits. Turning the setting on applies on the next connection; off stops it at once.')}
271
+
272
+ <h5>Clipboard</h5>
273
+ <div class="form-line">
274
+ <div class="header">
275
+ <div class="title">Clipboard sharing ${this.info('A desktop that gets your clipboard sees what is on it whenever it has the keyboard: what you copy then, and what you copied elsewhere before clicking back into it, passwords included. One that sends its clipboard here can put anything on yours, such as a command for you to paste into a terminal. Off leaves the clipboard out of the connection altogether. Narrowed while a desktop is open, what you copy from then on stays here and the files offered there are taken back; the change applies in full once that desktop reconnects.')}</div>
276
+ <div class="description">Which ways text, pictures and files copied on either side go. A desktop can have its
277
+ own, in its profile or its edit form. Applies on the next connection; narrowing it also stops the
278
+ sending from here at once to open desktops that follow the setting.</div>
279
+ </div>
280
+ <select class="form-control" data-setting="clipboard">
281
+ ${Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, label]) => `<option value="${mode}">${(0, help_1.esc)(label)}</option>`).join('')}
282
+ </select>
283
+ </div>
269
284
 
270
285
  <h5>Shared folders</h5>
271
286
  <div class="trd-lead">Folders from this computer as drives on the remote desktop, under <code>\\\\tsclient</code> in
@@ -378,7 +393,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
378
393
  <section data-topic="desktops">
379
394
  <h4>Desktops</h4>
380
395
  <div class="trd-lead">Remote desktop profiles, and desktops added from SSH tabs.
381
- ${this.info('A desktop added from an SSH tab\'s menu belongs to that host. That is for hosts without a profile, such as an ssh typed in a terminal.')}</div>
396
+ ${this.info('A desktop added from an SSH tab\'s menu belongs to that host. That is for hosts without a profile, such as ssh run in a local terminal.')}</div>
382
397
  <div class="trd-list" data-list="desktops"></div>
383
398
  <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="desktop">Add a desktop…</button></div>
384
399
  </section>
@@ -589,37 +604,49 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
589
604
  // Remote desktop profiles first (Tabby's editor), then the desktops added from SSH tabs (the plugin's form).
590
605
  const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && p.options?.host);
591
606
  const groups = this.config.store.groups ?? [];
607
+ // Every field as text whatever the config holds (see configText): one that can't be shown would leave this list,
608
+ // and the lists after it on the page, unbuilt.
592
609
  const profileRows = profiles.map(p => {
593
610
  const o = p.options;
594
- const address = `${o.host}:${o.port || 3389}`;
595
- const via = o.via ? (this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name ?? 'an SSH profile' : null;
611
+ const address = `${(0, desktops_1.configText)(o.host)}:${(0, desktops_1.configText)(o.port || 3389)}`;
612
+ const via = o.via ? (0, desktops_1.configText)((this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name) || 'an SSH profile' : null;
596
613
  const inherited = o.account === undefined ? groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account : undefined;
597
614
  const account = accountOf(o.account || inherited);
615
+ // Its own clipboard, or its group's or the type's default: what it connects with, as Tabby resolves the profile.
616
+ const clipboard = (0, clipboard_1.ownClipboard)(this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options?.clipboard);
617
+ const [username, kind, gateway] = [(0, desktops_1.configText)(o.username), (0, desktops_1.configText)(o.kind), (0, desktops_1.configText)(o.gateway)];
598
618
  const details = [
599
- account ? `Signs in as ${(0, help_1.esc)(account.name)}` : o.username ? `Signs in as ${(0, help_1.esc)(o.username)}` : '',
600
- o.kind && o.kind !== 'windows' ? (0, help_1.esc)(o.kind) : '',
601
- o.gateway ? `Through the gateway ${(0, help_1.esc)(o.gateway)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
619
+ account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
620
+ kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
621
+ gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
622
+ clipboard ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[clipboard].toLowerCase())}` : '',
602
623
  ].filter(Boolean).join('<br>');
603
- return this.row((0, help_1.esc)(p.name ?? address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
624
+ return this.row((0, help_1.esc)((0, desktops_1.configText)(p.name) || address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
604
625
  { label: 'Edit…', run: () => { this.editProfile(p.id); } },
605
626
  { label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
606
627
  ]);
607
628
  });
608
629
  const desktops = this.desktop.configuredDesktops();
609
- const desktopRows = desktops.map((d, i) => {
630
+ // An item that is no entry describes no desktop, and isn't listed (see isDesktopEntry).
631
+ const desktopRows = desktops.flatMap((d, i) => {
632
+ if (!(0, desktops_1.isDesktopEntry)(d)) {
633
+ return [];
634
+ }
610
635
  // A Hyper-V VM has no address of its own: its console is its host's to give.
611
- const address = d.hyperv ? 'Hyper-V VM' : `${d.host ?? '127.0.0.1'}:${d.port ?? 3389}`;
636
+ const { name, address, via, hyperv } = (0, desktops_1.entryText)(d);
612
637
  const account = accountOf(d.account);
638
+ const [username, kind, gateway, vm, mac] = [(0, desktops_1.configText)(d.username), (0, desktops_1.configText)(d.kind), (0, desktops_1.configText)(d.gateway), (0, desktops_1.configText)(d.wake?.vm), (0, desktops_1.configText)(d.wake?.mac)];
613
639
  const details = [
614
- account ? `Signs in as ${(0, help_1.esc)(account.name)}` : d.username ? `Signs in as ${(0, help_1.esc)(d.username)}` : '',
615
- d.kind && d.kind !== 'windows' ? (0, help_1.esc)(d.kind) : '',
616
- d.gateway && !d.hyperv ? `Through the gateway ${(0, help_1.esc)(d.gateway)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
617
- d.wake?.vm ? `Starts VM ${(0, help_1.esc)(d.wake.vm)} when off` : d.wake?.mac ? `Wakes ${(0, help_1.esc)(d.wake.mac)} when off` : '',
640
+ account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
641
+ kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
642
+ gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
643
+ vm ? `Starts VM ${(0, help_1.esc)(vm)} when off` : mac ? `Wakes ${(0, help_1.esc)(mac)} when off` : '',
644
+ (0, clipboard_1.ownClipboard)(d.clipboard) ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[(0, clipboard_1.ownClipboard)(d.clipboard)].toLowerCase())}` : '',
618
645
  ].filter(Boolean).join('<br>');
619
- return this.row((0, help_1.esc)(d.name ?? address), `${(0, help_1.esc)(address)} ${d.hyperv ? 'on' : 'behind'} ${(0, help_1.esc)(d.via)}${details ? `<br>${details}` : ''}`, [
620
- { label: 'Edit…', run: () => { this.desktop.editDesktopIn(this.root, i, true); } },
621
- { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveDesktop(i); } },
622
- ]);
646
+ return [this.row((0, help_1.esc)(name), `${hyperv ? 'Hyper-V VM' : (0, help_1.esc)(address)}${via ? ` ${hyperv ? 'on' : 'behind'} ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
647
+ { label: 'Edit…', run: () => { this.desktop.editDesktopIn(this.root, i, true); } },
648
+ { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveDesktop(i); } },
649
+ ])];
623
650
  });
624
651
  list.replaceChildren(...profileRows, ...desktopRows, this.empty(profileRows.length + desktopRows.length
625
652
  ? 'Profiles are also in Profiles &amp; connections; a desktop behind a host also in that host\'s SSH tab: right-click › Settings.'
@@ -645,7 +672,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
645
672
  }));
646
673
  row.insertBefore(check, row.querySelector('button'));
647
674
  return row;
648
- }), ...folders.length ? [] : [this.empty('None shared. The clipboard still carries files either way: drop them on the desktop, or copy them there and save them here.')]);
675
+ }), ...folders.length ? [] : [this.empty('None shared. The clipboard carries files too, the ways it goes: drop them on the desktop, or copy them there and save them here.')]);
649
676
  }
650
677
  /** Asks for a folder and shares it under its own name (numbered when another share has it). */
651
678
  async shareFolder() {
@@ -958,17 +985,21 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
958
985
  return;
959
986
  }
960
987
  const entries = this.desktop.trustedCertificates();
961
- list.replaceChildren(...entries.map(({ desktop, sha256 }) => {
988
+ list.replaceChildren(...entries.map(({ desktop, sha256, authority }) => {
962
989
  const [who, address] = desktop.includes('#') ? desktop.split('#', 2) : [desktop, ''];
963
- const title = address ? who === 'gateway' ? `The gateway ${(0, help_1.esc)(address)}` : who === desktops_1.DIRECT_KEY ? `${(0, help_1.esc)(address)} (direct)` : `${(0, help_1.esc)(address)} behind ${(0, help_1.esc)(who)}` : (0, help_1.esc)(who);
964
- // What else was decided about that server is said here, and forgotten with the certificate.
990
+ // A host's key as it reads; a machine reached through one is named as that host's ssh resolved it, which is
991
+ // its word: what could reorder or hide part of it shows as U+FFFD.
992
+ const host = (0, help_1.esc)((0, unshowable_1.showable)((0, desktops_1.shownKey)(who)));
993
+ const title = address ? who === 'gateway' ? `The gateway ${(0, help_1.esc)(address)}` : who === desktops_1.DIRECT_KEY ? `${(0, help_1.esc)(address)} (direct)` : `${(0, help_1.esc)(address)} behind ${host}` : host;
994
+ // How it came to be trusted, where that is known, and what else was decided about that server: forgotten with it.
995
+ const how = authority === true ? '<br>Valid for its name by a certificate authority' : authority === false ? '<br>Trusted by you' : '';
965
996
  const plain = this.desktop.signsInWithoutNla(desktop) ? '<br>Signs in without Network Level Authentication' : '';
966
- return this.row(title, `SHA-256 ${(0, help_1.esc)(sha256.slice(0, 23))}…${plain}`, [
997
+ return this.row(title, `SHA-256 ${(0, help_1.esc)(sha256.slice(0, 23))}…${how}${plain}`, [
967
998
  { label: 'Forget', run: () => this.desktop.forgetCertificate(desktop) },
968
999
  ]);
969
1000
  }), this.empty(entries.length
970
- ? 'A forgotten certificate is remembered again, without asking, on the next connection.'
971
- : 'None yet. Windows and xrdp desktops\' certificates are remembered on first use and checked every time after; GNOME desktops only accept the one the plugin made.'));
1001
+ ? 'A forgotten certificate is asked about again on the next connection, unless a certificate authority vouches for it; one behind an SSH host is remembered again without asking.'
1002
+ : 'None yet. A desktop connected to directly, and a gateway, are asked about the first time unless a certificate authority vouches for them; a desktop behind an SSH host is remembered on first use. Each is checked every time after. GNOME desktops only accept the one the plugin made.'));
972
1003
  }
973
1004
  renderSessions() {
974
1005
  const list = this.root.querySelector('[data-list=sessions]');
@@ -1031,7 +1062,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
1031
1062
  return null;
1032
1063
  }
1033
1064
  }
1034
- /** "Remove…" on a remote desktop profile: asks, then deletes it as Tabby's Profiles page would (saved account and certificate go too). */
1065
+ /**
1066
+ * "Remove…" on a remote desktop profile: asks, then deletes it as Tabby's Profiles page would (a direct one's saved
1067
+ * passwords and certificate go too; see RDPProfilesService.deleteProfile).
1068
+ */
1035
1069
  async removeProfile(id) {
1036
1070
  const profiles = this.injector.get(tabby_core_1.ProfilesService);
1037
1071
  const profile = (await profiles.getProfiles()).find(p => p.id === id);
@@ -1041,7 +1075,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
1041
1075
  const { response } = await this.platform.showMessageBox({
1042
1076
  type: 'warning',
1043
1077
  message: `Remove the profile "${profile.name}"?`,
1044
- detail: 'Its saved account and remembered certificate are forgotten too.',
1078
+ // A saved account isn't the profile's: other desktops may sign in with it, and it stays.
1079
+ detail: profile.options?.via
1080
+ ? 'Its saved password and remembered certificate stay: they are those of the same desktop behind its SSH profile\'s host.'
1081
+ : 'Its saved password and remembered certificate are forgotten too.',
1045
1082
  buttons: ['Remove', 'Keep'],
1046
1083
  defaultId: 1,
1047
1084
  cancelId: 1,
@@ -1135,4 +1172,3 @@ exports.RemoteDesktopSettingsComponent = RemoteDesktopSettingsComponent = __deco
1135
1172
  core_1.NgZone,
1136
1173
  tabby_core_1.HotkeysService])
1137
1174
  ], RemoteDesktopSettingsComponent);
1138
- //# sourceMappingURL=settingsPage.js.map