tabby-rdp 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +40 -11
- package/TROUBLESHOOTING.md +77 -0
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +2 -1
- package/dist/desktop.service.js +59 -27
- package/dist/desktopForm.js +2 -1
- package/dist/diskStorage.js +212 -0
- package/dist/fileTransfer.js +6 -53
- package/dist/help.js +10 -62
- package/dist/hostCompat.js +86 -0
- package/dist/index.js +5 -2
- package/dist/packageChangeDialog.js +186 -0
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +408 -0
- package/dist/settingsPage.js +91 -101
- package/dist/signin.js +73 -10
- package/dist/targets.js +9 -1
- package/dist/updates.js +77 -89
- package/docs/desk.md +157 -0
- package/package.json +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
- **Updates settings** include stable and preview channels (beta releases and release candidates), automatic and
|
|
7
|
+
manual checks, compatible version selection, rollback and uninstall controls. Changes require confirmation and
|
|
8
|
+
take effect after restarting Tabby. Update preferences and installation history stay on this computer.
|
|
9
|
+
- **Installation progress stays in the confirmation dialog**, with elapsed time and a verified success or failure
|
|
10
|
+
result. The dialog remains visible if Settings closes. The installed version is checked before reporting success.
|
|
11
|
+
|
|
12
|
+
**Fixed**
|
|
13
|
+
- **Commands through the system SSH report SSH's own failure** when it exits before reading their input,
|
|
14
|
+
instead of letting a broken-pipe error escape unhandled. Commands that ignore their input still return their output.
|
|
15
|
+
- **Plugin startup waits for Tabby's configuration** before migrating update preferences, avoiding an error that
|
|
16
|
+
could disable third-party plugins for the session.
|
|
17
|
+
|
|
18
|
+
**Changed**
|
|
19
|
+
- **Troubleshooting opens the Markdown guide** from the links at the top of Remote Desktop settings. The former
|
|
20
|
+
Troubleshooting tab is now Updates; connection logs are under Desktops. Empty lists use concise status messages.
|
|
21
|
+
- **Settings descriptions are shorter**, and help icons appear consistently after their descriptions.
|
|
22
|
+
- **Troubleshooting and desk cleanup guides are included in the npm package.**
|
|
23
|
+
- **Host compatibility checks share one layer**, with host and runtime versions, the host's declared xterm dependency,
|
|
24
|
+
and observed capabilities included in copied connection logs. Modal drag fallbacks and terminal input handling use
|
|
25
|
+
shared adapters that check the host's actual features, including on forks and nightly builds.
|
|
26
|
+
- **CI and the release workflow check out the code with actions/checkout 7.0.1** (#56), pinned to its commit as before,
|
|
27
|
+
in place of 4.4.0. Nothing in the package changes.
|
|
28
|
+
|
|
3
29
|
## 0.5.1
|
|
4
30
|
|
|
5
31
|
Security fixes from an audit of 0.5.0: saved passwords and certificates, what a host says about its desktops, `desk`,
|
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ Linux and Windows, side by side, through the SSH connections you already have.
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
tabby-rdp is a plugin for the [Tabby](https://tabby.sh) terminal. Press a key in an SSH tab and the machine's desktop
|
|
23
|
-
appears in its place; press it again and you're back at the shell.
|
|
24
|
-
|
|
23
|
+
appears in its place; press it again and you're back at the shell. SSH desktops use the connection you already have;
|
|
24
|
+
remote desktop profiles can also connect directly or through an RD Gateway.
|
|
25
25
|
|
|
26
26
|
- **Linux desktops without a monitor or a login.** The plugin starts a headless GNOME session for your SSH user with
|
|
27
27
|
GNOME Remote Desktop, on the fly, without root. A server in a rack works as well as a workstation.
|
|
@@ -40,8 +40,8 @@ nothing is opened on the network, and nothing has to be installed on the remote
|
|
|
40
40
|
- **`desk`:** type `desk` in the console and the same shell, with its history and running programs, moves into a
|
|
41
41
|
terminal on the desktop.
|
|
42
42
|
|
|
43
|
-
Everything is in **Settings › Remote Desktop
|
|
44
|
-
|
|
43
|
+
Everything is in **Settings › Remote Desktop**: Getting started, Settings, Overlay, Desktops, Accounts and Updates.
|
|
44
|
+
The **Troubleshooting** link at the top opens the [troubleshooting guide](TROUBLESHOOTING.md).
|
|
45
45
|
|
|
46
46
|
The remote desktop itself is [IronRDP](https://github.com/Devolutions/IronRDP)'s web client, running inside Tabby.
|
|
47
47
|
Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([below](#built-on-ironrdp)).
|
|
@@ -49,6 +49,7 @@ Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([
|
|
|
49
49
|
## Contents
|
|
50
50
|
|
|
51
51
|
- [Install](#install)
|
|
52
|
+
- [Versions and recovery](#versions-and-recovery)
|
|
52
53
|
- [Use](#use)
|
|
53
54
|
- [Features](#features)
|
|
54
55
|
- [Windows and other desktops behind a host](#windows-and-other-desktops-behind-a-host)
|
|
@@ -78,6 +79,24 @@ Or from a terminal, into Tabby's plugin folder (on macOS `~/Library/Application
|
|
|
78
79
|
npm install tabby-rdp
|
|
79
80
|
```
|
|
80
81
|
|
|
82
|
+
## Versions and recovery
|
|
83
|
+
|
|
84
|
+
Open **Settings › Remote Desktop › Updates** to manage the installed version:
|
|
85
|
+
|
|
86
|
+
- **Stable** is the default. **Preview** also offers beta releases and release candidates when available.
|
|
87
|
+
- Automatic checks look for updates once a day. **Check now** checks manually; installation is always your choice.
|
|
88
|
+
- Choose an exact compatible version, **Return to stable**, or **Restore previous version** after an installation
|
|
89
|
+
from this page. Version changes currently support **0.5.1 or newer**.
|
|
90
|
+
- **Uninstall plugin…** removes the package. **Manage in Tabby…** opens Tabby's plugin controls to disable it.
|
|
91
|
+
Saved connections, settings, passwords and remote setup are kept.
|
|
92
|
+
|
|
93
|
+
The confirmation dialog stays open during installation or removal and shows progress, elapsed time and the verified
|
|
94
|
+
result. After success, close remote desktops and restart Tabby. Until then, Updates shows the running version and
|
|
95
|
+
the installed version waiting for restart. Preferences and version history stay on this computer.
|
|
96
|
+
|
|
97
|
+
See [troubleshooting and recovery](TROUBLESHOOTING.md#recovery) if the plugin cannot load or you want to clean up saved
|
|
98
|
+
data or remote helpers.
|
|
99
|
+
|
|
81
100
|
## Use
|
|
82
101
|
|
|
83
102
|
Open an SSH tab to a Linux machine with GNOME, and:
|
|
@@ -371,7 +390,7 @@ the open desktops that follow it, takes back the files offered to them, and stop
|
|
|
371
390
|
several files that is under way before its next step. Narrowing applies in full only once they reconnect: reconnect a
|
|
372
391
|
desktop you don't trust that far. **Paste to all desktops** leaves out the desktops that don't take this computer's
|
|
373
392
|
clipboard and says which. Shared folders are a setting of their own, and screenshots and **Copy log** (Settings ›
|
|
374
|
-
Remote Desktop ›
|
|
393
|
+
Remote Desktop › Desktops › Open desktops) still go to your clipboard: they are your own actions.
|
|
375
394
|
|
|
376
395
|
## Shared folders
|
|
377
396
|
|
|
@@ -495,7 +514,8 @@ desktop of a machine you don't trust with `desk` off, which also has Tabby forge
|
|
|
495
514
|
## Settings
|
|
496
515
|
|
|
497
516
|
Open **Settings › Remote Desktop** in Tabby, right-click the desktop button in Tabby's header, or open **Settings** in the Remote Desktop section of a terminal's or tab's menu.
|
|
498
|
-
|
|
517
|
+
Desktop settings are stored in Tabby's config under `remoteDesktop`; update preferences and installation history
|
|
518
|
+
are local to this computer:
|
|
499
519
|
|
|
500
520
|
| Setting | Default | |
|
|
501
521
|
|---|---|---|
|
|
@@ -517,12 +537,12 @@ They are stored in Tabby's config under `remoteDesktop`:
|
|
|
517
537
|
| Session backend (`sessionBackend`) | `native` | For `desk`: `native` (trd-pty) or `tmux`. Config file only. |
|
|
518
538
|
| Find virtual machines on SSH hosts (`discoverVMs`) | On | Lists a host's libvirt VMs with a desktop in its menu ([VMs on a host](#vms-on-a-host)). |
|
|
519
539
|
| Shut down VMs it started (`shutDownIdle`) | Never | 5, 15 or 60: minutes without a desktop open after which a VM the plugin started is shut down again ([VMs on a host](#vms-on-a-host)). |
|
|
520
|
-
|
|
|
540
|
+
| Check for updates automatically (Updates tab) | On, Stable | Checks npm once a day; choose Stable or Preview, check manually, and install a compatible version explicitly. [Versions and recovery](#versions-and-recovery). |
|
|
521
541
|
| Desktop name overlay (`osd`) | When it helps | `show` (`auto`, `always`, `off`), `font`, `size`, `position`, `color` (empty: white; else a color name, `#hex`, or a color function of numbers such as `rgb()`) and `seconds`; the settings page previews it, at a resolution you pick. |
|
|
522
542
|
|
|
523
543
|
## Requirements
|
|
524
544
|
|
|
525
|
-
- **Tabby 1.0.236 or newer**, on macOS, Windows or Linux
|
|
545
|
+
- **Tabby 1.0.236 or newer**, on macOS, Windows or Linux. On a fresh Windows, Tabby
|
|
526
546
|
itself needs the [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist)
|
|
527
547
|
for SSH.
|
|
528
548
|
- **Linux desktops:** GNOME Shell and GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), a systemd user
|
|
@@ -536,7 +556,13 @@ They are stored in Tabby's config under `remoteDesktop`:
|
|
|
536
556
|
host or running OpenSSH Server itself.
|
|
537
557
|
|
|
538
558
|
Tested with Ubuntu 24.04 (GNOME Remote Desktop 46.3) and Windows 11 (Pro and Enterprise), from Tabby on macOS,
|
|
539
|
-
Windows 11 and Ubuntu 24.04 (arm64).
|
|
559
|
+
Windows 11 and Ubuntu 24.04 (arm64). Recent host compatibility tests cover official Tabby **1.0.238** on macOS
|
|
560
|
+
with xterm 5, and upstream xterm 6 builds on macOS and Linux. The new package installation dialog is tested on
|
|
561
|
+
macOS with Tabby 1.0.238; Windows qualification of these changes remains pending. See the
|
|
562
|
+
[host test matrix](test/README.md#host-qualification) for the exact builds and coverage.
|
|
563
|
+
|
|
564
|
+
The plugin checks the host's available features through a shared compatibility layer. Connection logs include the
|
|
565
|
+
host and runtime versions, declared xterm dependency and observed capabilities to help diagnose forks and nightlies.
|
|
540
566
|
|
|
541
567
|
## Built on IronRDP
|
|
542
568
|
|
|
@@ -573,8 +599,10 @@ it, not just this plugin.
|
|
|
573
599
|
## Development
|
|
574
600
|
|
|
575
601
|
```sh
|
|
576
|
-
npm
|
|
602
|
+
npm ci --ignore-scripts
|
|
577
603
|
npm run build # TypeScript to dist/
|
|
604
|
+
npm run typecheck # plugin and test TypeScript
|
|
605
|
+
npm run test:unit # standard unit suite; no remote machine needed
|
|
578
606
|
scripts/sandbox.sh # a separate Tabby with this plugin linked in (macOS), DevTools on port 9334
|
|
579
607
|
npm test # end-to-end suites against a test machine: see test/README.md
|
|
580
608
|
npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches: see ironrdp/README.md
|
|
@@ -626,7 +654,8 @@ npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches:
|
|
|
626
654
|
|
|
627
655
|
Questions ("how do I…", "is this expected?") go to [Discussions › Q&A](https://github.com/meanaverage/tabby-rdp/discussions/categories/q-a);
|
|
628
656
|
bugs and feature requests to the [issues](https://github.com/meanaverage/tabby-rdp/issues). A bug report is most useful
|
|
629
|
-
with the connection log: **Settings › Remote Desktop ›
|
|
657
|
+
with the connection log: **Settings › Remote Desktop › Desktops › Open desktops › Copy log**.
|
|
658
|
+
Start with the [troubleshooting guide](TROUBLESHOOTING.md) for common connection and installation problems.
|
|
630
659
|
|
|
631
660
|
## License
|
|
632
661
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
For connection logs, open **Settings › Remote Desktop › Desktops › Open desktops**, then choose **Copy log**. In version 0.5.1, the list is in **Troubleshooting**. Review logs before sharing them.
|
|
4
|
+
|
|
5
|
+
<a id="certificate"></a>
|
|
6
|
+
|
|
7
|
+
## A certificate this computer can't verify, or one that has changed
|
|
8
|
+
|
|
9
|
+
A desktop you connect to directly (a remote desktop profile, an imported file) and an RD Gateway connect without asking when their certificate is valid for their name by this computer's certificate authorities, as an organisation's own desktops and gateways often are. Any other is shown the first time, with why it can't be verified (most often: it is the server's own, self-signed) and its fingerprint, before anything of your sign-in is sent: check the fingerprint with whoever runs it, some other way than this connection (it is also in the desktop's connection log), then choose **Trust the certificate**. The question also says whom the certificate says it was issued to and by, and when it is valid: its own word, which nothing has checked, unless its chain leads to an authority this computer trusts (another machine's certificate then shows that machine's name) and the TLS library didn't refuse it for a reason of its own (a limit set on the authority, say). An issuer you wouldn't expect there (one that inspects your network's connections, say) is worth asking about before you trust it, but the one you would expect proves nothing: anyone can make a certificate that names it. Only the fingerprint, checked some other way, does. A desktop behind an SSH host remembers its certificate on first use without asking, since the way there runs inside SSH. When a remembered certificate differs later, the plugin stops before signing in, so nothing of your sign-in has gone to that server; only a certificate an authority vouched for is replaced without a question, by another it vouches for too. Reinstalling the machine or renewing its certificate changes it: then choose **Trust the new certificate**. If neither happened, something else is answering at that address. A GNOME desktop only ever accepts the certificate the plugin made for it; a different one there means another program listens on its port. Remembered certificates are in Settings › Remote Desktop › Desktops.
|
|
10
|
+
|
|
11
|
+
<a id="sign-in"></a>
|
|
12
|
+
|
|
13
|
+
## Wrong user name or password
|
|
14
|
+
|
|
15
|
+
Windows and xrdp desktops sign in with an account on that machine (`DOMAIN\user` works). A saved password that no longer works is asked for again; **Sign in again…** in the tab's menu replaces it on purpose. xrdp doesn't refuse a wrong password: it shows its own login window instead. GNOME desktops never ask: the plugin manages their sign-in.
|
|
16
|
+
|
|
17
|
+
<a id="no-desktop"></a>
|
|
18
|
+
|
|
19
|
+
## No desktop found on a Linux host
|
|
20
|
+
|
|
21
|
+
The plugin uses GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), with no root and no login screen. For KDE, XFCE, MATE and others, install xrdp and a desktop for its sessions: on Debian and Ubuntu, `sudo apt install xrdp xfce4`. A GNOME session already shared from the machine's own screen (GNOME's Desktop Sharing) has to be turned off first.
|
|
22
|
+
|
|
23
|
+
<a id="unreachable"></a>
|
|
24
|
+
|
|
25
|
+
## Can't reach a Windows desktop
|
|
26
|
+
|
|
27
|
+
Windows needs Remote Desktop turned on (Settings › System › Remote Desktop; Pro, Enterprise or Server). Behind an SSH host, the address is as that host sees it: from there, `nc -z <address> 3389` should succeed. For a VM or machine that may be off, give the desktop a VM name or MAC address in its edit form and it is started when opened.
|
|
28
|
+
|
|
29
|
+
<a id="reconnecting"></a>
|
|
30
|
+
|
|
31
|
+
## It keeps reconnecting
|
|
32
|
+
|
|
33
|
+
After sleep or a network change the desktop reconnects by itself, with growing pauses, and waits while the SSH connection is down (**Reconnect SSH** hurries that along). It stops after a few tries or on **Stop**. If it drops again right after connecting, check the host's free memory and that the desktop's session is still running there.
|
|
34
|
+
|
|
35
|
+
<a id="picture"></a>
|
|
36
|
+
|
|
37
|
+
## The picture is blurry or slow
|
|
38
|
+
|
|
39
|
+
**Sharpness › Retina** renders at your screen's device pixels, with the remote's scale set to match. Video (H.264) is used when the remote sends it: Windows does, GNOME only with a hardware encoder (VA-API or NVENC). **Show connection status** shows the frame rate, the round trip and how the picture comes.
|
|
40
|
+
|
|
41
|
+
<a id="sound"></a>
|
|
42
|
+
|
|
43
|
+
## No sound, or the microphone isn't heard
|
|
44
|
+
|
|
45
|
+
Sound and microphone changes apply on the next connection. xrdp needs `pipewire-module-xrdp` or `pulseaudio-module-xrdp`. The microphone needs the system's permission here (macOS: System Settings › Privacy & Security › Microphone, then restart Tabby). On GNOME, apps record from "Remoteaudio Source"; on Windows, the machine must allow audio recording redirection. A desktop whose microphone was stopped (**Stop** in the microphone's menu in Tabby's header, or **Send the microphone** unchecked in the desktop's menu) gets none, its reconnects included, until **Send the microphone** is checked again in its menu or that Tabby window is closed.
|
|
46
|
+
|
|
47
|
+
<a id="nested-ssh"></a>
|
|
48
|
+
|
|
49
|
+
## I typed ssh to another machine in a terminal
|
|
50
|
+
|
|
51
|
+
In an SSH tab (or a split pane) where you typed `ssh` on to another machine, **Desktop** opens that machine's desktop, going through the first one, and `desk` works there too. Desktop asks which desktop you mean, since which `ssh` runs there is the first machine's to say; pick the other machine's "from now on" to have it open at once the next times. Desktops you set up behind a host open in a tab connected to that host, not through another one. The first machine has to log in to the other by itself for this: with a key there, or agent forwarding, as `ssh -o BatchMode=yes` would. Tabby's **Reconnect** belongs to the tab: it reconnects the first machine and ends the ssh typed in it. To have both desktops side by side, open the first machine's desktop in one pane, and ssh on in the other.
|
|
52
|
+
|
|
53
|
+
<a id="extra-monitor"></a>
|
|
54
|
+
|
|
55
|
+
## The desktop is open somewhere else
|
|
56
|
+
|
|
57
|
+
GNOME Remote Desktop's headless mode gives each connection a screen of its own: a second computer, or another Tabby window, connected to the same account gets an empty extra screen rather than the same one. So when the desktop is open somewhere else already, you choose. **Take it over** disconnects the other connection, as Windows does; the session and its apps carry on, on this screen. **Open a second screen** keeps both, each with a screen of its own. A desktop taken over from here doesn't take itself back: it says so, and offers the same two choices.
|
|
58
|
+
|
|
59
|
+
<a id="recovery"></a>
|
|
60
|
+
|
|
61
|
+
## Recover a version, disable the plugin, or remove it
|
|
62
|
+
|
|
63
|
+
In **0.5.2 or newer**, open **Settings › Remote Desktop › Updates**. **Stable** is the default; **Preview** includes beta releases and release candidates when available. Choose an exact published version and confirm installation. **Return to stable** can install an older version. **Restore previous version** uses the version saved after the last successful installation from this page. On older releases, manage the package in **Tabby › Settings › Plugins**.
|
|
64
|
+
|
|
65
|
+
The confirmation dialog stays open through progress and the verified result. If installation fails, the dialog shows the cause. After success, close remote desktops and restart Tabby; Updates shows the running version and the installed version waiting for restart until then.
|
|
66
|
+
|
|
67
|
+
Use **0.5.1 or newer** with current account and certificate settings, and respect any higher minimum documented by a later data migration.
|
|
68
|
+
|
|
69
|
+
If the plugin cannot load:
|
|
70
|
+
|
|
71
|
+
1. Open **Tabby › Settings › Plugins**. Disable **tabby-rdp** to stop it loading, or uninstall and reinstall it there. Restart Tabby after changing plugins. Tabby’s [configuration locations](https://github.com/Eugeny/tabby/wiki/Config-file) help locate your profile when the application cannot open.
|
|
72
|
+
2. For a specific known compatible version, open a terminal in **Tabby's plugin folder** (the folder containing the plugin installation's `package.json`, with `node_modules/tabby-rdp` below it). Run `npm install tabby-rdp@0.5.1`, replacing `0.5.1` with your known compatible exact version. Do not run this in the tabby-rdp source checkout, the application bundle, or inside `node_modules/tabby-rdp` itself. Fully quit and reopen Tabby afterwards.
|
|
73
|
+
3. If a later version has migrated saved data, respect that version's documented minimum. Reinstall a compatible stable version or the last working preview; do not force an older version across a migration boundary.
|
|
74
|
+
|
|
75
|
+
Disabling or uninstalling keeps desktops, profiles, settings, remembered certificates and saved credentials. It also leaves any remote helpers and remote applications in place. To remove saved entries, use **Remote Desktop › Desktops** and **Accounts**, and **Profiles & connections** for profiles, before uninstalling. Unlock the credential store first if required; check any reported failure rather than assuming a credential was deleted. Data synced to other computers may need attention there too.
|
|
76
|
+
|
|
77
|
+
For the optional `desk` remote helper, turn **Settings › Remote Desktop › Settings › desk: bring the console along** off and reconnect to each host while the plugin is still installed. The next connection removes the helper's own files and login hook; an offline host cannot be cleaned this way until it returns. Read the [desk cleanup details](docs/desk.md) for manual removal and read-only startup files. End remote applications you no longer need separately. Uninstalling this plugin does not shut down remote hosts or erase their files.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AccountRevisions = exports.ACCOUNT_REMEMBER_UNAVAILABLE = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
/** No secrets: invalidates old sign-in forms in other windows before an account edit touches the store. */
|
|
6
|
+
const PREFIX = 'tabby-rdp.account-revision.';
|
|
7
|
+
exports.ACCOUNT_REMEMBER_UNAVAILABLE = 'To remember this password, save the account again in Settings and reopen this sign-in form.';
|
|
8
|
+
class AccountRevisions {
|
|
9
|
+
constructor(storage = () => window.localStorage, coordinated = () => !!globalThis.navigator?.locks) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
this.coordinated = coordinated;
|
|
12
|
+
}
|
|
13
|
+
/** A prompt may remember only when this window's account agrees with the shared revision. */
|
|
14
|
+
capture(account) {
|
|
15
|
+
const revision = account.credentialRevision ?? '';
|
|
16
|
+
return this.current(account.id, revision) ? revision : undefined;
|
|
17
|
+
}
|
|
18
|
+
current(id, revision) {
|
|
19
|
+
if (revision === undefined || !this.coordinated()) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const shared = this.storage().getItem(PREFIX + id);
|
|
24
|
+
return shared === null || shared === revision;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Synchronous publication: older writers fail their next check, including one already waiting on a store. */
|
|
31
|
+
change(id) {
|
|
32
|
+
if (!this.coordinated()) {
|
|
33
|
+
throw new Error('Saved account changes are unavailable. Restart or update Tabby and try again.');
|
|
34
|
+
}
|
|
35
|
+
const revision = (0, crypto_1.randomUUID)();
|
|
36
|
+
// A failed write must stop the edit: a revision held only in this window cannot invalidate another's form.
|
|
37
|
+
try {
|
|
38
|
+
this.storage().setItem(PREFIX + id, revision);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error('Saved account changes are unavailable. Restart Tabby and try again.');
|
|
42
|
+
}
|
|
43
|
+
return revision;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.AccountRevisions = AccountRevisions;
|
package/dist/accounts.js
CHANGED
|
@@ -16,7 +16,8 @@ function accountsOf(store) {
|
|
|
16
16
|
}
|
|
17
17
|
seen.add(id);
|
|
18
18
|
const domain = typeof a.domain === 'string' && a.domain.trim() ? a.domain.trim() : undefined;
|
|
19
|
-
return [{ id, name: typeof a.name === 'string' && a.name.trim() ? a.name.trim() : signInName({ username, domain }), username,
|
|
19
|
+
return [{ id, name: typeof a.name === 'string' && a.name.trim() ? a.name.trim() : signInName({ username, domain }), username,
|
|
20
|
+
...domain ? { domain } : {}, ...typeof a.credentialRevision === 'string' ? { credentialRevision: a.credentialRevision } : {} }];
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
23
|
/** A new account's id: nothing in it but that it is unused. */
|
package/dist/desktop.service.js
CHANGED
|
@@ -29,6 +29,7 @@ const microphone_1 = require("./microphone");
|
|
|
29
29
|
const drives_1 = require("./drives");
|
|
30
30
|
const desktopForm_1 = require("./desktopForm");
|
|
31
31
|
const accounts_1 = require("./accounts");
|
|
32
|
+
const accountRevisions_1 = require("./accountRevisions");
|
|
32
33
|
const accountForm_1 = require("./accountForm");
|
|
33
34
|
const fileTransfer_1 = require("./fileTransfer");
|
|
34
35
|
const desktops_1 = require("./desktops");
|
|
@@ -45,6 +46,7 @@ const gateway_1 = require("./gateway");
|
|
|
45
46
|
const authorities_1 = require("./authorities");
|
|
46
47
|
const vms_1 = require("./vms");
|
|
47
48
|
const sessionLog_1 = require("./sessionLog");
|
|
49
|
+
const hostCompat_1 = require("./hostCompat");
|
|
48
50
|
const unshowable_1 = require("./unshowable");
|
|
49
51
|
/**
|
|
50
52
|
* The keychain entry that marks the passwords 0.5.0 saved for desktops reached through a gateway forgotten from this
|
|
@@ -348,7 +350,7 @@ class DesktopSession {
|
|
|
348
350
|
setVisible(visible) {
|
|
349
351
|
this.visible = visible;
|
|
350
352
|
this.overlay.style.display = visible ? '' : 'none';
|
|
351
|
-
this.container
|
|
353
|
+
(0, hostCompat_1.terminalInputs)(this.container).forEach(input => {
|
|
352
354
|
input.disabled = visible;
|
|
353
355
|
});
|
|
354
356
|
if (visible) {
|
|
@@ -478,6 +480,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
478
480
|
/** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops; and what the scans learnt of the host. */
|
|
479
481
|
this.vms = new Map();
|
|
480
482
|
this.lastUsed = new Map();
|
|
483
|
+
this.accountRevisions = new accountRevisions_1.AccountRevisions();
|
|
481
484
|
/** Each pane's `desk` requests, one at a time (see DeskGate). */
|
|
482
485
|
this.deskGates = new WeakMap();
|
|
483
486
|
/**
|
|
@@ -833,6 +836,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
833
836
|
accounts() {
|
|
834
837
|
return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
|
|
835
838
|
}
|
|
839
|
+
accountCurrent(key, name, revision) {
|
|
840
|
+
const account = this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === key);
|
|
841
|
+
return !!account && (0, accounts_1.signInName)(account) === name && (account.credentialRevision ?? '') === revision && this.accountRevisions.current(account.id, revision);
|
|
842
|
+
}
|
|
836
843
|
/**
|
|
837
844
|
* The desktops that sign in with a saved account, or sign in to their RD Gateway with it: configured ones behind
|
|
838
845
|
* SSH hosts, and RDP profiles (their own choice, or their profile group's default when they make none). Each comes
|
|
@@ -863,14 +870,31 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
863
870
|
username: account.username.trim(),
|
|
864
871
|
...account.domain?.trim() ? { domain: account.domain.trim() } : {},
|
|
865
872
|
};
|
|
873
|
+
let revision;
|
|
874
|
+
try {
|
|
875
|
+
revision = this.accountRevisions.change(entry.id);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
return { id: entry.id, keychainError: error?.message ?? String(error) };
|
|
879
|
+
}
|
|
880
|
+
entry.credentialRevision = revision;
|
|
881
|
+
// Invalidate before any store await. Keep the old sign-in name until changing its password succeeds.
|
|
882
|
+
if (old) {
|
|
883
|
+
this.config.store.remoteDesktop.accounts = list.map(a => a.id === old.id ? { ...a, credentialRevision: revision } : a);
|
|
884
|
+
await this.config.save();
|
|
885
|
+
}
|
|
886
|
+
const current = () => this.accountRevisions.current(entry.id, revision) && (!old || this.accounts().some(a => a.id === entry.id && a.credentialRevision === revision));
|
|
866
887
|
// The password first: a new user name must not be written while the old password (for the old name) stays, nor
|
|
867
888
|
// a new password reported as saved when the store refused it.
|
|
868
889
|
let keychainError;
|
|
869
890
|
if (password !== undefined && password !== '') {
|
|
870
|
-
await (0, signin_1.
|
|
891
|
+
await (0, signin_1.saveCredentialsIf)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }, current, `saved account ${entry.name}`).catch(e => { keychainError = String(e?.message ?? e); });
|
|
871
892
|
}
|
|
872
893
|
else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
|
|
873
|
-
await (0, signin_1.
|
|
894
|
+
await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(entry.id), current).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
|
|
895
|
+
}
|
|
896
|
+
if (!current()) {
|
|
897
|
+
return { id: entry.id, keychainError: 'The account changed again while this edit was being saved. The newer change was kept.' };
|
|
874
898
|
}
|
|
875
899
|
if (keychainError && !old) {
|
|
876
900
|
// Not added at all: a retry would otherwise make a second account.
|
|
@@ -886,7 +910,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
886
910
|
delete entry.domain;
|
|
887
911
|
}
|
|
888
912
|
}
|
|
889
|
-
|
|
913
|
+
const now = this.accounts();
|
|
914
|
+
this.config.store.remoteDesktop.accounts = old ? now.map(a => a.id === old.id ? entry : a) : [...now, entry];
|
|
890
915
|
await this.config.save();
|
|
891
916
|
this.changed$.next();
|
|
892
917
|
return { id: entry.id, keychainError };
|
|
@@ -923,11 +948,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
923
948
|
if (response !== 0) {
|
|
924
949
|
return false;
|
|
925
950
|
}
|
|
926
|
-
|
|
927
|
-
|
|
951
|
+
try {
|
|
952
|
+
await this.removeAccount(id);
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
this.notifications.error(`The account could not be fully removed: ${error?.message ?? error}`);
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
928
959
|
}
|
|
929
960
|
/** Removes a saved account and its password; desktops that used it go back to asking. */
|
|
930
961
|
async removeAccount(id) {
|
|
962
|
+
const revision = this.accountRevisions.change(id);
|
|
931
963
|
const store = this.config.store.remoteDesktop;
|
|
932
964
|
store.accounts = this.accounts().filter(a => a.id !== id);
|
|
933
965
|
if (this.configuredDesktops().some(d => (0, desktops_1.isDesktopEntry)(d) && (d.account === id || d.gatewayAccount === id))) {
|
|
@@ -964,7 +996,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
964
996
|
}
|
|
965
997
|
await this.config.save();
|
|
966
998
|
this.changed$.next();
|
|
967
|
-
await (0, signin_1.
|
|
999
|
+
await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(id), () => this.accountRevisions.current(id, revision)).catch(e => this.notifications.error(`The account is removed, but its password could not be: ${e?.message ?? e}`));
|
|
968
1000
|
}
|
|
969
1001
|
/** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
|
|
970
1002
|
configuredDesktops() {
|
|
@@ -2721,6 +2753,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2721
2753
|
}
|
|
2722
2754
|
session.status('');
|
|
2723
2755
|
const mark = (0, signin_1.forgetMark)();
|
|
2756
|
+
const accountRevision = account && this.accountRevisions.capture(account);
|
|
2724
2757
|
// The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via". A machine reached
|
|
2725
2758
|
// with ssh typed in a host's console says which host: that host is what leads there.
|
|
2726
2759
|
const hostOwn = spec.id === desktops_1.OWN_DESKTOP || spec.kind === 'xrdp' && spec.host === '127.0.0.1';
|
|
@@ -2729,8 +2762,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2729
2762
|
title: spec.hyperv ? `Sign in to ${target.label}${target.via ? ` (via ${target.via})` : ''}, to open ${spec.name}`
|
|
2730
2763
|
: hostOwn && !target.via || target.direct ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${via})`,
|
|
2731
2764
|
username: account ? (0, accounts_1.signInName)(account) : spec.username,
|
|
2732
|
-
error: retryError,
|
|
2733
|
-
canRemember:
|
|
2765
|
+
error: [retryError, account && accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
|
|
2766
|
+
canRemember: !account || accountRevision !== undefined,
|
|
2734
2767
|
account: account?.name,
|
|
2735
2768
|
}, session.disposed);
|
|
2736
2769
|
// Only in the pane in front, as when connected (see run()): the form gets the keyboard when its pane does.
|
|
@@ -2741,7 +2774,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2741
2774
|
return entered && {
|
|
2742
2775
|
host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
|
|
2743
2776
|
saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
|
|
2744
|
-
hyperv, mark,
|
|
2777
|
+
hyperv, mark, accountRevision,
|
|
2745
2778
|
};
|
|
2746
2779
|
}
|
|
2747
2780
|
async connect(pane, target, spec, session) {
|
|
@@ -2895,7 +2928,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2895
2928
|
session.log.push('gateway: its saved account no longer exists; signing in with the desktop\'s');
|
|
2896
2929
|
return undefined;
|
|
2897
2930
|
}
|
|
2898
|
-
const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account) };
|
|
2931
|
+
const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account), accountRevision: this.accountRevisions.capture(account) };
|
|
2899
2932
|
const saved = retryError ? null : await (0, signin_1.loadCredentials)(signIn.saveKey);
|
|
2900
2933
|
if (saved && saved.username === (0, accounts_1.signInName)(account)) {
|
|
2901
2934
|
session.log.push(`gateway: the saved account "${account.name}"`);
|
|
@@ -2905,8 +2938,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2905
2938
|
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
2906
2939
|
title: `Sign in to the gateway ${gateway.host}, to reach ${spec.name}`,
|
|
2907
2940
|
username: (0, accounts_1.signInName)(account),
|
|
2908
|
-
error: retryError,
|
|
2909
|
-
canRemember:
|
|
2941
|
+
error: [retryError, signIn.accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
|
|
2942
|
+
canRemember: signIn.accountRevision !== undefined,
|
|
2910
2943
|
account: account.name,
|
|
2911
2944
|
}, session.disposed);
|
|
2912
2945
|
// Only in the pane in front (see endpointFor).
|
|
@@ -2930,10 +2963,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
2930
2963
|
// Entered a moment ago and taken by the gateway: kept, while the account is still what it was signed in as.
|
|
2931
2964
|
if (own?.remember) {
|
|
2932
2965
|
own.remember = false;
|
|
2933
|
-
|
|
2934
|
-
if (accountNow && (0, accounts_1.signInName)(accountNow) === own.saveFor) {
|
|
2935
|
-
(0, signin_1.saveCredentials)(own.saveKey, own.credentials, own.saveLabel).then(() => session.log.push(`gateway: saved as the account's password in ${(0, signin_1.storeName)()}`), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
2936
|
-
}
|
|
2966
|
+
(0, signin_1.saveCredentialsIf)(own.saveKey, own.credentials, () => this.accountCurrent(own.saveKey, own.saveFor, own.accountRevision), own.saveLabel).then(saved => session.log.push(saved ? `gateway: saved as the account's password in ${(0, signin_1.storeName)()}` : 'gateway: not saved: the account changed meanwhile'), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
2937
2967
|
}
|
|
2938
2968
|
return stream;
|
|
2939
2969
|
}
|
|
@@ -3564,11 +3594,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
3564
3594
|
this.changed$.next();
|
|
3565
3595
|
// Under a saved account only while that account is still there with the same sign-in name: the prompt may
|
|
3566
3596
|
// have been open while it was removed or changed in Settings.
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
session.log.push(`sign-in:
|
|
3597
|
+
if (endpoint.remember && endpoint.saveKey) {
|
|
3598
|
+
const key = endpoint.saveKey;
|
|
3599
|
+
(0, signin_1.saveCredentialsIf)(key, credentials, () => this.accountCurrent(key, endpoint.saveFor, endpoint.accountRevision), endpoint.saveLabel).then(saved => session.log.push(saved ? `sign-in: saved as the account's password in ${(0, signin_1.storeName)()}` : 'sign-in: not saved: the account changed meanwhile'), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
3570
3600
|
}
|
|
3571
|
-
else if (endpoint.remember &&
|
|
3601
|
+
else if (endpoint.remember && endpoint.mark !== undefined && (0, signin_1.forgottenSince)(session.credentialKey, endpoint.mark)) {
|
|
3572
3602
|
// Edited (its user name, gateway or address) or removed while the server delayed the sign-in: what was
|
|
3573
3603
|
// forgotten then stays forgotten, and the desktop asks the next time.
|
|
3574
3604
|
session.log.push('sign-in: not saved: the desktop was changed or removed meanwhile');
|
|
@@ -3647,7 +3677,7 @@ exports.RemoteDesktopService = RemoteDesktopService = RemoteDesktopService_1 = _
|
|
|
3647
3677
|
tabby_core_1.PlatformService])
|
|
3648
3678
|
], RemoteDesktopService);
|
|
3649
3679
|
let styleInstalled = false;
|
|
3650
|
-
function installStyle() {
|
|
3680
|
+
function installStyle(compat) {
|
|
3651
3681
|
if (styleInstalled) {
|
|
3652
3682
|
return;
|
|
3653
3683
|
}
|
|
@@ -3655,7 +3685,7 @@ function installStyle() {
|
|
|
3655
3685
|
const style = document.createElement('style');
|
|
3656
3686
|
style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE + DRAGBAR_STYLE + accountForm_1.STYLE;
|
|
3657
3687
|
document.head.appendChild(style);
|
|
3658
|
-
keepWindowDraggable();
|
|
3688
|
+
keepWindowDraggable(compat);
|
|
3659
3689
|
}
|
|
3660
3690
|
/** A strip the height of the tab bar that stays a window drag region over a dialog (the plugin's, and Tabby's). */
|
|
3661
3691
|
const DRAGBAR_STYLE = `
|
|
@@ -3663,12 +3693,14 @@ const DRAGBAR_STYLE = `
|
|
|
3663
3693
|
.modal > .trd-form-dragbar { display: block; position: fixed; left: 0; right: 0; top: 0; height: var(--tabs-height, 38px); -webkit-app-region: drag; }
|
|
3664
3694
|
`;
|
|
3665
3695
|
/**
|
|
3666
|
-
* Tabby
|
|
3667
|
-
*
|
|
3668
|
-
* is below it. The plugin's own dialogs do the same (see settingsPage.ts).
|
|
3696
|
+
* Older Tabby dialogs cover the tab bar without a drag region. Supply their fallback strip, deferring to the host's
|
|
3697
|
+
* modal pseudo-element when it provides one. The plugin's own dialogs still need their strips (see settingsPage.ts).
|
|
3669
3698
|
*/
|
|
3670
|
-
function keepWindowDraggable() {
|
|
3699
|
+
function keepWindowDraggable(compat) {
|
|
3671
3700
|
const strip = (modal) => {
|
|
3701
|
+
if (compat.hasNativeModalDragRegion(modal)) {
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3672
3704
|
if (!modal.querySelector(':scope > .trd-form-dragbar')) {
|
|
3673
3705
|
const bar = document.createElement('div');
|
|
3674
3706
|
bar.className = 'trd-form-dragbar';
|
package/dist/desktopForm.js
CHANGED
|
@@ -10,6 +10,7 @@ const desktops_1 = require("./desktops");
|
|
|
10
10
|
const gateway_1 = require("./gateway");
|
|
11
11
|
const osd_1 = require("./osd");
|
|
12
12
|
const wake_1 = require("./wake");
|
|
13
|
+
const hostCompat_1 = require("./hostCompat");
|
|
13
14
|
/** host:port, [v6]:port, or a bare host (RDP's 3389); null if it doesn't look like an address. */
|
|
14
15
|
function parseAddress(text) {
|
|
15
16
|
// No '#' in a host: session and trust keys are split on it (see keyParts).
|
|
@@ -134,7 +135,7 @@ function askDesktop(pane, options) {
|
|
|
134
135
|
}
|
|
135
136
|
pane.appendChild(overlay);
|
|
136
137
|
// Same as a desktop layer: the terminal underneath can't take the keyboard while the form shows.
|
|
137
|
-
const inputs =
|
|
138
|
+
const inputs = (0, hostCompat_1.terminalInputs)(pane).filter(t => !t.disabled);
|
|
138
139
|
inputs.forEach(t => { t.disabled = true; });
|
|
139
140
|
const focusForm = () => {
|
|
140
141
|
if (!form.contains(document.activeElement)) {
|