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.
- package/CHANGELOG.md +490 -0
- package/README.md +177 -48
- package/dist/accountForm.js +0 -1
- package/dist/accounts.js +0 -1
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1442 -244
- package/dist/desktopForm.js +51 -17
- package/dist/desktops.js +232 -24
- package/dist/drives.js +271 -30
- package/dist/fileTransfer.js +958 -83
- package/dist/gateway.js +216 -57
- package/dist/header.js +76 -5
- package/dist/help.js +34 -13
- package/dist/hyperv.js +65 -15
- package/dist/index.js +2 -2
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +0 -1
- package/dist/osd.js +56 -15
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +319 -44
- package/dist/rdpFile.js +22 -7
- package/dist/rdpProfile.js +39 -21
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +65 -29
- package/dist/signin.js +407 -40
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +40 -6
- package/dist/targets.js +185 -39
- package/dist/ui.js +88 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +37 -2
- package/dist/vms.js +17 -3
- package/dist/wake.js +15 -8
- package/package.json +4 -3
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/drives.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/gateway.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/hyperv.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/ntlm.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/rdpProfile.js
CHANGED
|
@@ -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
|
|
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
|
|
262
|
-
// behind a host shares them with that host's
|
|
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
|
-
(
|
|
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
|
-
|
|
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)(
|
|
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
|
-
...
|
|
332
|
-
...o.via ? [(this.config.store.profiles ?? []).find((p) => p.id === o.via)?.name
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/remoteSetup.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
147
|
-
# Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
424
|
+
return { kind: 'windows', port: 3389, username: (0, desktops_1.keyUser)(target.key), password: '', certificate: '' };
|
|
347
425
|
}
|
|
348
|
-
|
|
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
|
-
|
|
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;
|
package/dist/settingsPage.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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)}` :
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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)}` :
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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)(
|
|
620
|
-
|
|
621
|
-
|
|
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 & 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
|
|
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
|
-
|
|
964
|
-
//
|
|
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
|
|
971
|
-
: 'None yet.
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|