tabby-rdp 0.4.1 → 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 +580 -0
- package/README.md +252 -34
- 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 +1763 -204
- package/dist/desktopForm.js +72 -13
- package/dist/desktops.js +243 -19
- package/dist/drives.js +525 -0
- package/dist/fileTransfer.js +1064 -72
- package/dist/gateway.js +651 -0
- package/dist/header.js +76 -5
- package/dist/help.js +34 -13
- package/dist/hyperv.js +129 -0
- package/dist/index.js +2 -2
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +225 -0
- package/dist/osd.js +56 -15
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +468 -52
- package/dist/rdpFile.js +53 -4
- package/dist/rdpProfile.js +75 -22
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +117 -27
- package/dist/signin.js +429 -54
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +50 -6
- package/dist/targets.js +185 -39
- package/dist/ui.js +98 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +37 -2
- package/dist/vms.js +36 -5
- package/dist/wake.js +31 -12
- package/package.json +4 -3
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/iron-remote-desktop-rdp.js +418 -390
- 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/fileTransfer.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.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/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/ui.js
CHANGED
|
@@ -17,9 +17,12 @@ exports.settingsMenu = settingsMenu;
|
|
|
17
17
|
const core_1 = require("@angular/core");
|
|
18
18
|
const tabby_core_1 = require("tabby-core");
|
|
19
19
|
const tabby_terminal_1 = require("tabby-terminal");
|
|
20
|
+
const clipboard_1 = require("./clipboard");
|
|
20
21
|
const desktop_service_1 = require("./desktop.service");
|
|
22
|
+
const desktops_1 = require("./desktops");
|
|
21
23
|
const help_1 = require("./help");
|
|
22
24
|
Object.defineProperty(exports, "TOGGLE_HOTKEY", { enumerable: true, get: function () { return help_1.TOGGLE_HOTKEY; } });
|
|
25
|
+
const hyperv_1 = require("./hyperv");
|
|
23
26
|
const keyboard_1 = require("./keyboard");
|
|
24
27
|
const updates_1 = require("./updates");
|
|
25
28
|
const ssh_1 = require("./ssh");
|
|
@@ -48,8 +51,13 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
48
51
|
sharpness: 'standard',
|
|
49
52
|
// `desk`: installs a login hook on remotes (applied the next time a desktop opens there).
|
|
50
53
|
desk: false,
|
|
54
|
+
// The keys `desk` sends on each host, as that host's setup reported them: [{ desktop: <host account>,
|
|
55
|
+
// sha256 }] (a digest, not the key). Requests without the host's key are ignored (see openConsole).
|
|
56
|
+
deskKeys: [],
|
|
51
57
|
// macOS: ⌘ acts as Ctrl on the remote desktop (false: ⌘ is the Windows/Super key).
|
|
52
58
|
macShortcuts: true,
|
|
59
|
+
// Send the characters typed (Unicode) rather than key positions: dead keys, layouts the remote lacks.
|
|
60
|
+
unicodeKeys: false,
|
|
53
61
|
// Play the remote desktop's sound (applies on the next connect).
|
|
54
62
|
sound: true,
|
|
55
63
|
// H.264 in the graphics pipeline, decoded by the browser, where it can (applies on the next connect).
|
|
@@ -58,8 +66,12 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
58
66
|
zoom: 'fit',
|
|
59
67
|
// A small indicator on the desktop: throughput, frames per second, SSH round trip, connection path.
|
|
60
68
|
connectionStatus: false,
|
|
61
|
-
// Send the microphone to
|
|
69
|
+
// Send the microphone to a remote desktop that asks for it, as one does while an app there records
|
|
70
|
+
// (applies on the next connect).
|
|
62
71
|
microphone: false,
|
|
72
|
+
// Which ways the clipboard (text, pictures, files) goes, unless a desktop has its own: 'both' | 'fromRemote'
|
|
73
|
+
// (only from the remote desktop to this computer) | 'off' (applies on the next connect; see clipboard.ts).
|
|
74
|
+
clipboard: 'both',
|
|
63
75
|
// More desktops behind SSH hosts (e.g. a Windows VM whose RDP port the host forwards); see desktops.ts.
|
|
64
76
|
desktops: [],
|
|
65
77
|
// Saved accounts desktops can sign in with: [{ id, name, username, domain }]; passwords are in the keychain
|
|
@@ -67,8 +79,19 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
67
79
|
accounts: [],
|
|
68
80
|
// Sharpness for particular desktops, overriding `sharpness`: [{ desktop: <session key>, sharpness }].
|
|
69
81
|
desktopSharpness: [],
|
|
70
|
-
//
|
|
82
|
+
// Folders shared with remote desktops as drives (\\tsclient\<name>): [{ path, name, readOnly }]; see drives.ts.
|
|
83
|
+
sharedFolders: [],
|
|
84
|
+
// Certificates remembered for desktops and RD Gateways: [{ desktop: <session key, or gateway#host:port>, sha256,
|
|
85
|
+
// authority }]; `authority` true where a certificate authority vouched for it, false where the user trusted
|
|
86
|
+
// it, none where it was trusted on first use (desktops behind hosts). See RemoteDesktopService.checkTrusted.
|
|
71
87
|
trustedCertificates: [],
|
|
88
|
+
// Desktops allowed to sign in without Network Level Authentication ("Send the password anyway"), each with
|
|
89
|
+
// the certificate it was allowed with: [{ desktop: <session key>, sha256 }]. Tabby saves only keys named
|
|
90
|
+
// here (test/unit/config-defaults.ts).
|
|
91
|
+
withoutNla: [],
|
|
92
|
+
// Machines reached with ssh typed in an SSH tab whose desktop was chosen there "from now on" (see remoteFor):
|
|
93
|
+
// [{ via: <that tab's key>, destination }]. Desktop opens such a one at once; others are asked about first.
|
|
94
|
+
nestedSSH: [],
|
|
72
95
|
// The on-screen display naming a desktop for a moment (see osd.ts).
|
|
73
96
|
osd: { show: 'auto', font: 'condensed', size: 'medium', position: 'top-right', color: '', seconds: 2.5 },
|
|
74
97
|
// Look for VMs with a desktop on SSH hosts (libvirt) and offer them in the menus (see vms.ts).
|
|
@@ -81,6 +104,10 @@ let RemoteDesktopConfig = class RemoteDesktopConfig extends tabby_core_1.ConfigP
|
|
|
81
104
|
updateNoted: '',
|
|
82
105
|
// The tip shown the first time a desktop connects (see help.ts) has been shown.
|
|
83
106
|
tipShown: false,
|
|
107
|
+
// The passwords 0.5.0 saved for desktops reached through an RD Gateway, under keys that are now those of the
|
|
108
|
+
// same addresses without one, have been forgotten from the Vault (see
|
|
109
|
+
// RemoteDesktopService.gatewayPasswords; each computer's keychain keeps a mark of its own).
|
|
110
|
+
gatewayPasswordsMigrated: false,
|
|
84
111
|
},
|
|
85
112
|
};
|
|
86
113
|
this.platformDefaults = {
|
|
@@ -110,30 +137,38 @@ function toggleLabel(desktop, pane) {
|
|
|
110
137
|
return desktop.isOpenElsewhere(pane) ? `Switch to ${what} (open in another tab)` : `Open ${what}`;
|
|
111
138
|
}
|
|
112
139
|
/**
|
|
113
|
-
* The host's other desktops (besides the one the toggle acts on): open one here,
|
|
114
|
-
* Then "Add a desktop behind <host>…".
|
|
140
|
+
* The host's other desktops (besides the one the toggle acts on, see RemoteDesktopService.choicesOf): open one here,
|
|
141
|
+
* replacing the current one, a machine's reached through the host included. Then "Add a desktop behind <host>…".
|
|
115
142
|
*/
|
|
116
143
|
function desktopChoices(desktop, pane, targetLabel) {
|
|
117
|
-
const { specs,
|
|
118
|
-
const items = specs.filter(spec => spec
|
|
144
|
+
const { specs, chosen } = desktop.choicesOf(pane);
|
|
145
|
+
const items = specs.filter(spec => !chosen(spec)).map(spec => ({
|
|
119
146
|
label: desktop.isOpenElsewhere(pane, spec) ? `Switch to ${spec.name} (open in another tab)`
|
|
120
147
|
// Found on the host (see vms.ts): say so, and that opening one that's off starts it.
|
|
121
148
|
: spec.found === 'off' ? `Start and open ${spec.name} (VM, shut off)` : spec.found ? `Open ${spec.name} (VM)` : `Open ${spec.name}`,
|
|
122
149
|
click: () => desktop.showDesktop(pane, spec.id),
|
|
123
150
|
}));
|
|
151
|
+
// A host with more VMs than a menu lists: the rest are left out, and that is said rather than hidden.
|
|
152
|
+
if (desktop.vmsCut(pane)) {
|
|
153
|
+
items.push({ label: `Only the first ${hyperv_1.MAX_VMS} VMs found are listed`, enabled: false });
|
|
154
|
+
}
|
|
124
155
|
// Not in a remote desktop tab: it isn't an SSH host.
|
|
125
156
|
if (targetLabel && desktop.hasConsole(pane)) {
|
|
126
157
|
items.push({ label: `Add a desktop behind ${targetLabel}…`, click: () => desktop.addDesktop(pane) });
|
|
127
158
|
}
|
|
128
159
|
return items;
|
|
129
160
|
}
|
|
130
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* What can be done with the pane's connected desktop: send keys, view only, its microphone, a screenshot. Shared by
|
|
163
|
+
* the menus.
|
|
164
|
+
*/
|
|
131
165
|
function desktopActions(desktop, keyboard, pane) {
|
|
132
166
|
const spec = desktop.desktopOf(pane);
|
|
133
167
|
if (!spec || !desktop.isConnected(pane)) {
|
|
134
168
|
return [];
|
|
135
169
|
}
|
|
136
170
|
const viewOnly = desktop.isViewOnly(pane);
|
|
171
|
+
const microphoneStopped = desktop.microphoneStoppedFor(pane);
|
|
137
172
|
return [
|
|
138
173
|
{
|
|
139
174
|
label: 'Send keys',
|
|
@@ -146,6 +181,14 @@ function desktopActions(desktop, keyboard, pane) {
|
|
|
146
181
|
checked: viewOnly,
|
|
147
182
|
click: () => desktop.setViewOnly(pane, !viewOnly),
|
|
148
183
|
},
|
|
184
|
+
// With the setting on, every desktop gets the microphone when it asks for it; this one can be left out,
|
|
185
|
+
// reconnects included, and taken back in (see RemoteDesktopService.stopMicrophone).
|
|
186
|
+
...desktop.settings().microphone ? [{
|
|
187
|
+
type: 'checkbox',
|
|
188
|
+
label: 'Send the microphone (when this desktop asks for it)',
|
|
189
|
+
checked: !microphoneStopped,
|
|
190
|
+
click: () => microphoneStopped ? desktop.resumeMicrophone(pane) : desktop.stopMicrophone(pane),
|
|
191
|
+
}] : [],
|
|
149
192
|
{ label: 'Save a screenshot (to Downloads and the clipboard)', click: () => desktop.saveScreenshot(pane) },
|
|
150
193
|
];
|
|
151
194
|
}
|
|
@@ -193,19 +236,23 @@ let RemoteDesktopContextMenu = class RemoteDesktopContextMenu extends tabby_core
|
|
|
193
236
|
if (this.desktop.isConnected(pane)) {
|
|
194
237
|
items.push({ label: 'Send files…', click: () => this.desktop.sendFiles(pane) });
|
|
195
238
|
items.push(...desktopActions(this.desktop, this.keyboard, pane));
|
|
196
|
-
// Several desktops in this tab: paste on all of them, or type into all of them.
|
|
239
|
+
// Several desktops showing in this tab: paste on all of them, or type into all of them. Typing into all, once
|
|
240
|
+
// on, stays here to be turned off while fewer of them show (those hidden get nothing: see
|
|
241
|
+
// RemoteDesktopService.takesAll).
|
|
197
242
|
const all = this.desktop.desktopsInTab(pane);
|
|
198
243
|
if (all > 1) {
|
|
199
244
|
items.push({ label: `Paste to all ${all} desktops in this tab`, click: () => this.desktop.pasteToAll(pane) });
|
|
245
|
+
}
|
|
246
|
+
if (all > 1 || this.desktop.isBroadcast(pane)) {
|
|
200
247
|
items.push({
|
|
201
248
|
type: 'checkbox',
|
|
202
|
-
label: `Type into all ${all} desktops in this tab
|
|
249
|
+
label: all > 1 ? `Type into all ${all} desktops in this tab` : 'Type into all desktops in this tab',
|
|
203
250
|
checked: this.desktop.isBroadcast(pane),
|
|
204
251
|
click: () => this.desktop.setBroadcast(pane, !this.desktop.isBroadcast(pane)),
|
|
205
252
|
});
|
|
206
253
|
}
|
|
207
254
|
}
|
|
208
|
-
if (this.desktop.
|
|
255
|
+
if (this.desktop.canSaveFoundDesktop(pane)) {
|
|
209
256
|
items.push({ label: `Save ${this.desktop.desktopOf(pane).name} to this host's desktops`, click: () => this.desktop.saveFoundDesktop(pane) });
|
|
210
257
|
}
|
|
211
258
|
if (this.desktop.canSignInAgain(pane)) {
|
|
@@ -252,6 +299,11 @@ function settingsMenu(desktop, pane, help) {
|
|
|
252
299
|
// The desktop open in this pane can have its own sharpness (for example, Retina for Windows only).
|
|
253
300
|
const spec = pane ? desktop.desktopOf(pane) : null;
|
|
254
301
|
const own = pane ? desktop.ownSharpness(pane) : null;
|
|
302
|
+
// Narrowed while it was open, by another way of opening it (see DesktopSession.clipboardCap): said only where that
|
|
303
|
+
// is narrower than what the desktop would have otherwise, its own setting or this one.
|
|
304
|
+
const capOf = pane ? desktop.clipboardCapOf(pane) : 'both';
|
|
305
|
+
const otherwise = spec?.clipboard ?? current.clipboard;
|
|
306
|
+
const cap = (0, clipboard_1.narrowest)(capOf, otherwise) !== otherwise ? (0, clipboard_1.narrowest)(capOf, otherwise) : 'both';
|
|
255
307
|
const ownRadio = (value, label) => ({
|
|
256
308
|
type: 'radio',
|
|
257
309
|
label,
|
|
@@ -264,6 +316,12 @@ function settingsMenu(desktop, pane, help) {
|
|
|
264
316
|
ownRadio('standard', 'Standard'),
|
|
265
317
|
ownRadio('retina', 'Retina'),
|
|
266
318
|
] : [];
|
|
319
|
+
// The configured desktops, each with its place in the list (what editing and removing take); an item that is no
|
|
320
|
+
// entry describes no desktop, and isn't listed. Shown as text whatever an entry holds (see entryText): this menu is
|
|
321
|
+
// part of every SSH tab's, which Tabby builds from every plugin's items at once, so one entry that can't be shown
|
|
322
|
+
// would take Tabby's own items too.
|
|
323
|
+
const entries = desktop.configuredDesktops().flatMap((d, i) => (0, desktops_1.isDesktopEntry)(d) ? [{ index: i, ...(0, desktops_1.entryText)(d) }] : []);
|
|
324
|
+
const entryLabel = (e) => `${e.name}${e.via ? ` (behind ${e.via})` : ''}…`;
|
|
267
325
|
return [
|
|
268
326
|
...help ? [
|
|
269
327
|
{ label: 'All settings, keys and help…', click: () => help.open() },
|
|
@@ -300,16 +358,37 @@ function settingsMenu(desktop, pane, help) {
|
|
|
300
358
|
},
|
|
301
359
|
{
|
|
302
360
|
type: 'checkbox',
|
|
303
|
-
label: 'Send the microphone
|
|
361
|
+
label: 'Send the microphone to remote desktops that ask for it (on applies on the next connect, off at once)',
|
|
304
362
|
checked: current.microphone,
|
|
305
363
|
click: () => desktop.updateSettings({ microphone: !current.microphone }),
|
|
306
364
|
},
|
|
365
|
+
{
|
|
366
|
+
// A desktop's own (its profile, or its edit form) takes the place of this: the one open here says so.
|
|
367
|
+
label: 'Clipboard (applies on the next connect)',
|
|
368
|
+
submenu: [
|
|
369
|
+
// Its own: from its profile (or that profile's group's or type's defaults), or its entry behind a host.
|
|
370
|
+
...spec?.clipboard ? [
|
|
371
|
+
{ label: `${spec.name} doesn't follow this: its profile or its own settings say ${clipboard_1.CLIPBOARD_LABELS[spec.clipboard].toLowerCase()}`, enabled: false },
|
|
372
|
+
] : [],
|
|
373
|
+
...cap !== 'both' ? [
|
|
374
|
+
{ label: `Clipboard sharing with ${spec?.name ?? 'this desktop'} is ${clipboard_1.CLIPBOARD_LABELS[cap].toLowerCase()} until it is closed, as another way of opening it asked`, enabled: false },
|
|
375
|
+
] : [],
|
|
376
|
+
...spec?.clipboard || cap !== 'both' ? [{ type: 'separator' }] : [],
|
|
377
|
+
...Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, label]) => radio('clipboard', mode, label)),
|
|
378
|
+
],
|
|
379
|
+
},
|
|
307
380
|
{
|
|
308
381
|
type: 'checkbox',
|
|
309
382
|
label: 'Show connection status on the desktop (throughput, frames per second, round trip)',
|
|
310
383
|
checked: current.connectionStatus,
|
|
311
384
|
click: () => desktop.updateSettings({ connectionStatus: !current.connectionStatus }),
|
|
312
385
|
},
|
|
386
|
+
{
|
|
387
|
+
type: 'checkbox',
|
|
388
|
+
label: 'Send text as typed (characters rather than key positions: dead keys, other layouts)',
|
|
389
|
+
checked: current.unicodeKeys,
|
|
390
|
+
click: () => desktop.updateSettings({ unicodeKeys: !current.unicodeKeys }),
|
|
391
|
+
},
|
|
313
392
|
...process.platform === 'darwin' ? [{
|
|
314
393
|
type: 'checkbox',
|
|
315
394
|
label: 'Mac shortcuts on the remote desktop (⌘C, ⌘V, ⌘Z… act as Ctrl+C, Ctrl+V…; off: ⌘ is the Windows key)',
|
|
@@ -321,18 +400,18 @@ function settingsMenu(desktop, pane, help) {
|
|
|
321
400
|
{
|
|
322
401
|
// The form shows over a console (a remote desktop tab edits its desktop in Tabby's profile settings).
|
|
323
402
|
label: 'Edit a desktop',
|
|
324
|
-
enabled: !!pane && desktop.hasConsole(pane) &&
|
|
325
|
-
submenu:
|
|
326
|
-
label:
|
|
327
|
-
click: () => pane && desktop.editDesktop(pane,
|
|
403
|
+
enabled: !!pane && desktop.hasConsole(pane) && entries.length > 0,
|
|
404
|
+
submenu: entries.map(e => ({
|
|
405
|
+
label: entryLabel(e),
|
|
406
|
+
click: () => pane && desktop.editDesktop(pane, e.index),
|
|
328
407
|
})),
|
|
329
408
|
},
|
|
330
409
|
{
|
|
331
410
|
label: 'Remove a desktop',
|
|
332
|
-
enabled:
|
|
333
|
-
submenu:
|
|
334
|
-
label:
|
|
335
|
-
click: () => desktop.confirmRemoveDesktop(
|
|
411
|
+
enabled: entries.length > 0,
|
|
412
|
+
submenu: entries.map(e => ({
|
|
413
|
+
label: entryLabel(e),
|
|
414
|
+
click: () => desktop.confirmRemoveDesktop(e.index),
|
|
336
415
|
})),
|
|
337
416
|
},
|
|
338
417
|
];
|
|
@@ -390,4 +469,3 @@ exports.RemoteDesktopToolbarButton = RemoteDesktopToolbarButton = __decorate([
|
|
|
390
469
|
(0, core_1.Injectable)(),
|
|
391
470
|
__metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
|
|
392
471
|
], RemoteDesktopToolbarButton);
|
|
393
|
-
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UNSHOWABLE = void 0;
|
|
4
|
+
exports.withoutWordJoiners = withoutWordJoiners;
|
|
5
|
+
exports.showable = showable;
|
|
6
|
+
/**
|
|
7
|
+
* A character that can change how the text around it reads without showing itself, in a name someone else chose (an
|
|
8
|
+
* .rdp file's, see RemoteDesktopService.importRdp; an `ssh` destination a host reports, see RemoteTargets.nestedSSH;
|
|
9
|
+
* what a server's certificate says of itself): control and format characters (bidi overrides and isolates, zero-width
|
|
10
|
+
* ones, tags), the other default-ignorable ones (variation selectors, the combining grapheme joiner, Hangul fillers),
|
|
11
|
+
* line and paragraph separators, private-use and unpaired surrogate code points, and blanks other than the space
|
|
12
|
+
* (no-break and wide spaces, the braille blank).
|
|
13
|
+
*/
|
|
14
|
+
exports.UNSHOWABLE = /[\p{Cc}\p{Cf}\p{Co}\p{Cs}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}⠀]|(?! )\p{Zs}/u;
|
|
15
|
+
/**
|
|
16
|
+
* A zero-width joiner or non-joiner between two letters of a script that writes words with them: Arabic (as Persian
|
|
17
|
+
* does) and the Indic scripts. There it can change how the letters join, and so show; between letters of other scripts
|
|
18
|
+
* (Latin, say) it shows nothing at all, and `ad\u200dmin` reads as `admin`.
|
|
19
|
+
*/
|
|
20
|
+
const WORD_JOINERS = new RegExp(['Arabic', 'Devanagari', 'Bengali', 'Gurmukhi', 'Gujarati', 'Oriya', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Sinhala']
|
|
21
|
+
.map(script => `(?<=[\\p{L}\\p{M}])(?<=\\p{scx=${script}})[\\u200c\\u200d](?=[\\p{L}\\p{M}])(?=\\p{scx=${script}})`).join('|'), 'gu');
|
|
22
|
+
/** `text` without the joiners its scripts write words with (see WORD_JOINERS): what UNSHOWABLE then finds doesn't show. */
|
|
23
|
+
function withoutWordJoiners(text) {
|
|
24
|
+
return text.replace(WORD_JOINERS, '');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Text someone else chose, as it is shown and logged: what could reorder or hide part of it (UNSHOWABLE) as U+FFFD, and
|
|
28
|
+
* at most `max` characters of it, an ellipsis saying where it was cut.
|
|
29
|
+
*/
|
|
30
|
+
function showable(text, max = Infinity) {
|
|
31
|
+
const shown = text.replace(new RegExp(exports.UNSHOWABLE.source, 'gu'), '�');
|
|
32
|
+
if (shown.length <= max) {
|
|
33
|
+
return shown;
|
|
34
|
+
}
|
|
35
|
+
// Not between the two halves of a character outside the BMP, which would leave half of one.
|
|
36
|
+
let end = Math.max(0, max - 1);
|
|
37
|
+
if (end > 0 && /[\ud800-\udbff]/.test(shown[end - 1])) {
|
|
38
|
+
end--;
|
|
39
|
+
}
|
|
40
|
+
return `${shown.slice(0, end)}…`;
|
|
41
|
+
}
|
package/dist/updates.js
CHANGED
|
@@ -10,6 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.UpdateCheck = void 0;
|
|
13
|
+
exports.latestVersion = latestVersion;
|
|
13
14
|
exports.installedVersion = installedVersion;
|
|
14
15
|
exports.newer = newer;
|
|
15
16
|
const core_1 = require("@angular/core");
|
|
@@ -20,6 +21,41 @@ const targets_1 = require("./targets");
|
|
|
20
21
|
const REGISTRY = 'https://registry.npmjs.org/tabby-rdp/latest';
|
|
21
22
|
const RELEASES = 'https://github.com/meanaverage/tabby-rdp/releases';
|
|
22
23
|
const DAY = 24 * 60 * 60 * 1000;
|
|
24
|
+
/**
|
|
25
|
+
* The latest version's number, as npm says, from an answer read for `timeoutMs` and up to `maxBytes` at most (it is
|
|
26
|
+
* that version's package.json, a few KB): whoever answers in npm's name (a proxy that inspects TLS, say) could
|
|
27
|
+
* otherwise keep the request waiting for good, or fill Tabby's memory.
|
|
28
|
+
*/
|
|
29
|
+
async function latestVersion(url = REGISTRY, timeoutMs = 15000, maxBytes = 256 * 1024) {
|
|
30
|
+
const abort = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, { cache: 'no-store', signal: abort.signal });
|
|
34
|
+
if (!response.body) {
|
|
35
|
+
throw new Error(`the registry answered ${response.status} with nothing`);
|
|
36
|
+
}
|
|
37
|
+
const reader = response.body.getReader();
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
size += value.byteLength;
|
|
46
|
+
if (size > maxBytes) {
|
|
47
|
+
throw new Error('the registry\'s answer is too large');
|
|
48
|
+
}
|
|
49
|
+
chunks.push(value);
|
|
50
|
+
}
|
|
51
|
+
return String(JSON.parse(Buffer.concat(chunks).toString('utf8'))?.version ?? '');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
// Ends the request, if it is still going (an answer too large).
|
|
56
|
+
abort.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
23
59
|
/** This package's version. */
|
|
24
60
|
function installedVersion() {
|
|
25
61
|
try {
|
|
@@ -74,7 +110,7 @@ let UpdateCheck = class UpdateCheck {
|
|
|
74
110
|
if (!this.enabled && latest === undefined) {
|
|
75
111
|
return null;
|
|
76
112
|
}
|
|
77
|
-
const version = latest ??
|
|
113
|
+
const version = latest ?? await latestVersion();
|
|
78
114
|
this.zone.run(() => {
|
|
79
115
|
this.available = newer(version, installedVersion()) ? version : null;
|
|
80
116
|
this.changed$.next();
|
|
@@ -129,4 +165,3 @@ exports.UpdateCheck = UpdateCheck = __decorate([
|
|
|
129
165
|
(0, core_1.Injectable)({ providedIn: 'root' }),
|
|
130
166
|
__metadata("design:paramtypes", [tabby_core_1.ConfigService, tabby_core_1.AppService, help_1.RemoteDesktopHelp, tabby_core_1.PlatformService, core_1.NgZone])
|
|
131
167
|
], UpdateCheck);
|
|
132
|
-
//# sourceMappingURL=updates.js.map
|
package/dist/vms.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.scanVMs = scanVMs;
|
|
4
4
|
exports.vmSpec = vmSpec;
|
|
5
|
+
const net_1 = require("net");
|
|
6
|
+
const desktops_1 = require("./desktops");
|
|
7
|
+
const hyperv_1 = require("./hyperv");
|
|
5
8
|
/**
|
|
6
9
|
* RDP security the server chose when offered TLS and NLA (RDP_NEG_RSP): Windows and GNOME Remote Desktop take NLA
|
|
7
10
|
* (2), xrdp only TLS (1). A Linux VM that takes NLA is GNOME's, whose sign-in the plugin manages over SSH to that VM:
|
|
@@ -14,6 +17,7 @@ const XRDP_LIKE = '1';
|
|
|
14
17
|
// wake.ts) within a second; the protocol the server chose, or 0 without a negotiation response ('' if none).
|
|
15
18
|
// Read-only: starts nothing, changes nothing.
|
|
16
19
|
const SCAN_SCRIPT = String.raw `
|
|
20
|
+
echo TRD_SH
|
|
17
21
|
command -v virsh >/dev/null 2>&1 || exit 0
|
|
18
22
|
probe () {
|
|
19
23
|
if command -v python3 >/dev/null 2>&1; then
|
|
@@ -55,10 +59,25 @@ for uri in qemu:///system qemu:///session; do
|
|
|
55
59
|
done
|
|
56
60
|
done
|
|
57
61
|
`;
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The host's VMs that have (or, being off, may have) a desktop: libvirt's running ones whose RDP answers, and Windows
|
|
64
|
+
* ones that are off; on a Windows host, Hyper-V's. `host.windows` says the host is known to run Windows, and is set
|
|
65
|
+
* when the scan finds out: such a host isn't asked for libvirt again (its shell takes a while to say it has no sh).
|
|
66
|
+
* `host.cut` says whether the list stopped at MAX_VMS (each VM is a menu item) with more there, so that the menu can
|
|
67
|
+
* say only so many are listed.
|
|
68
|
+
*/
|
|
69
|
+
async function scanVMs(target, host = {}) {
|
|
70
|
+
const out = host.windows ? '' : await target.exec('sh -s', SCAN_SCRIPT);
|
|
71
|
+
// No sh answered: a Windows host (cmd.exe or PowerShell as its SSH shell), which is where Hyper-V is. Asked only
|
|
72
|
+
// then, so that a Linux host costs nothing extra.
|
|
73
|
+
if (!/^TRD_SH$/m.test(out)) {
|
|
74
|
+
const vms = await (0, hyperv_1.scanHyperV)(target, host);
|
|
75
|
+
host.windows = true;
|
|
76
|
+
return vms.map(vm => ({ hyperv: vm.id, name: vm.name, state: vm.state, windows: true, address: '', rdp: false }));
|
|
77
|
+
}
|
|
61
78
|
const found = [];
|
|
79
|
+
// Names seen so far: looked up, not searched for, so that the time taken grows with the lines, not their square.
|
|
80
|
+
const names = new Set();
|
|
62
81
|
for (const line of out.split('\n')) {
|
|
63
82
|
const m = /^TRD_VM ([^|]+)\|([^|]*)\|([^|]*)\|([^|]*)\|(\d*)\s*$/.exec(line.trim());
|
|
64
83
|
if (!m) {
|
|
@@ -69,14 +88,27 @@ async function scanVMs(target) {
|
|
|
69
88
|
// Windows: RDP answering, or off (started when opened). Others: only xrdp (see XRDP_LIKE).
|
|
70
89
|
const rdp = windows ? protocol !== '' : protocol === XRDP_LIKE;
|
|
71
90
|
const vm = { name, state: state === 'running' ? 'running' : 'off', windows, address, rdp };
|
|
72
|
-
|
|
91
|
+
// An IPv4 address, as the scan only ever finds: the address is part of the VM's session key, which is also the
|
|
92
|
+
// key its saved password is kept under. Anything else (`10.0.0.5:3389@gateway#gw.example`) could make that the
|
|
93
|
+
// key of a desktop configured behind this host, a password saved for its gateway included (see keyParts).
|
|
94
|
+
if ((0, net_1.isIPv4)(vm.address) && (vm.rdp || vm.state === 'off' && vm.windows) && !names.has(vm.name)) {
|
|
95
|
+
// One more than are listed: the list is cut short.
|
|
96
|
+
if (found.length === hyperv_1.MAX_VMS) {
|
|
97
|
+
host.cut = true;
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
names.add(vm.name);
|
|
73
101
|
found.push(vm);
|
|
74
102
|
}
|
|
75
103
|
}
|
|
104
|
+
host.cut = false;
|
|
76
105
|
return found;
|
|
77
106
|
}
|
|
78
107
|
/** A found VM as a desktop behind its host: started by name when it's off (see wake.ts). */
|
|
79
108
|
function vmSpec(vm) {
|
|
109
|
+
if (vm.hyperv) {
|
|
110
|
+
return (0, desktops_1.hyperVSpec)(vm.hyperv, vm.name, { found: vm.state });
|
|
111
|
+
}
|
|
80
112
|
return {
|
|
81
113
|
id: `${vm.address}:3389`,
|
|
82
114
|
name: vm.name,
|
|
@@ -87,4 +119,3 @@ function vmSpec(vm) {
|
|
|
87
119
|
found: vm.state,
|
|
88
120
|
};
|
|
89
121
|
}
|
|
90
|
-
//# sourceMappingURL=vms.js.map
|
package/dist/wake.js
CHANGED
|
@@ -4,19 +4,25 @@ exports.parseWake = parseWake;
|
|
|
4
4
|
exports.wakeFromText = wakeFromText;
|
|
5
5
|
exports.shutDownWhenIdle = shutDownWhenIdle;
|
|
6
6
|
exports.rdpAnswers = rdpAnswers;
|
|
7
|
+
exports.desktopUp = desktopUp;
|
|
7
8
|
exports.wakeDesktop = wakeDesktop;
|
|
8
9
|
exports.waitForRdp = waitForRdp;
|
|
9
10
|
const deskScript_1 = require("./deskScript");
|
|
11
|
+
const hyperv_1 = require("./hyperv");
|
|
12
|
+
const osd_1 = require("./osd");
|
|
10
13
|
const MAC = /^[0-9a-f]{2}([:-]?)[0-9a-f]{2}(\1[0-9a-f]{2}){4}$/i;
|
|
11
14
|
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
12
15
|
/** A `wake` config value, checked; undefined when absent or unusable. */
|
|
13
16
|
function parseWake(raw) {
|
|
17
|
+
if ((0, hyperv_1.vmId)(raw?.hyperv)) {
|
|
18
|
+
return { hyperv: (0, hyperv_1.vmId)(raw.hyperv) };
|
|
19
|
+
}
|
|
14
20
|
if (typeof raw?.vm === 'string' && raw.vm.trim()) {
|
|
15
21
|
return { vm: raw.vm.trim() };
|
|
16
22
|
}
|
|
17
23
|
const mac = typeof raw?.mac === 'string' ? raw.mac.trim() : '';
|
|
18
24
|
if (MAC.test(mac)) {
|
|
19
|
-
const port =
|
|
25
|
+
const port = (0, osd_1.configNumber)(raw.port ?? 9);
|
|
20
26
|
return {
|
|
21
27
|
mac,
|
|
22
28
|
...typeof raw.broadcast === 'string' && IPV4.test(raw.broadcast.trim()) ? { broadcast: raw.broadcast.trim() } : {},
|
|
@@ -93,11 +99,13 @@ const IDLE_SCRIPT = String.raw `
|
|
|
93
99
|
command -v virsh >/dev/null 2>&1 || { echo "RD_ERR virsh is not installed on the SSH host"; exit 0; }
|
|
94
100
|
command -v ss >/dev/null 2>&1 || { echo "RD_ERR ss (iproute2) is not installed on the SSH host"; exit 0; }
|
|
95
101
|
# The pid file: with the plugin's other files, or where the home folder isn't writable, in a folder of its own in /tmp.
|
|
102
|
+
# Private either way, as the desktop's setup keeps it (this may be the first to make it).
|
|
96
103
|
D="$HOME/.local/share/tabby-rdp"
|
|
97
|
-
if ! mkdir -p "$D" 2>/dev/null; then
|
|
104
|
+
if ! { mkdir -p "$D" && chmod 700 "$D"; } 2>/dev/null; then
|
|
98
105
|
D="/tmp/tabby-rdp-$(id -u)"
|
|
99
106
|
mkdir -p -m 700 "$D" 2>/dev/null
|
|
100
|
-
|
|
107
|
+
# One that was there already keeps its mode (mkdir -m only sets one it makes): made private here, or not used.
|
|
108
|
+
[ -d "$D" ] && [ -O "$D" ] && [ ! -L "$D" ] && chmod 700 "$D" 2>/dev/null || { echo "RD_ERR could not create a folder for the watcher's pid file"; exit 0; }
|
|
101
109
|
fi
|
|
102
110
|
PIDF="$D/idle-$(printf %s "$VM" | tr -c 'A-Za-z0-9._-' _).pid"
|
|
103
111
|
if [ -s "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null; then
|
|
@@ -109,8 +117,11 @@ for u in qemu:///system qemu:///session; do
|
|
|
109
117
|
done
|
|
110
118
|
[ -n "$URI" ] || { echo "RD_ERR $VM is not running"; exit 0; }
|
|
111
119
|
A=$(getent ahostsv4 "$H" 2>/dev/null | awk 'NR == 1 { print $1 }')
|
|
120
|
+
# The watcher writes its pid file anew (umask 077): one left by an earlier version keeps its mode otherwise.
|
|
121
|
+
rm -f "$PIDF"
|
|
112
122
|
nohup setsid sh -c '
|
|
113
123
|
VM=$1 URI=$2 A=$3 P=$4 IDLE=$5 PIDF=$6
|
|
124
|
+
umask 077
|
|
114
125
|
echo $$ > "$PIDF"
|
|
115
126
|
trap "rm -f \"$PIDF\"" EXIT
|
|
116
127
|
idle=0 asked=0
|
|
@@ -138,40 +149,49 @@ echo "RD_OK $VM shuts down after $((IDLE / 60)) min without a desktop open ($URI
|
|
|
138
149
|
*/
|
|
139
150
|
async function shutDownWhenIdle(target, vm, host, port, idleSeconds) {
|
|
140
151
|
const out = await target.exec('sh -s', `VM=${(0, deskScript_1.shq)(vm)}\nH=${(0, deskScript_1.shq)(host)}\nP=${port}\nIDLE=${Math.max(1, Math.round(idleSeconds / 30)) * 30}\n${IDLE_SCRIPT}`);
|
|
141
|
-
|
|
152
|
+
// The host's words, as much of them as the log shows: the host decides how long its lines are.
|
|
153
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
142
154
|
if (err) {
|
|
143
155
|
throw new Error(err[1].trim());
|
|
144
156
|
}
|
|
145
|
-
return /^RD_OK (
|
|
157
|
+
return /^RD_OK (.{0,1000})/m.exec(out)?.[1] ?? 'no result';
|
|
146
158
|
}
|
|
147
159
|
/** Whether the RDP server at host:port answers, as seen from the SSH host. */
|
|
148
160
|
async function rdpAnswers(target, host, port) {
|
|
149
161
|
const out = await target.exec('sh -s', `H=${(0, deskScript_1.shq)(host)}\nP=${port}\n${PROBE_SCRIPT}`);
|
|
150
162
|
return /^RD_OPEN$/m.test(out);
|
|
151
163
|
}
|
|
164
|
+
/** Whether the desktop is up: its RDP server answers, or, for a Hyper-V VM (reached through its host), it runs. */
|
|
165
|
+
async function desktopUp(target, host, port, wake) {
|
|
166
|
+
return wake && 'hyperv' in wake ? (await (0, hyperv_1.hyperVState)(target, wake.hyperv)).running : rdpAnswers(target, host, port);
|
|
167
|
+
}
|
|
152
168
|
/** Starts the desktop as `wake` says. Resolves with what was done; throws with the reason when it couldn't. */
|
|
153
169
|
async function wakeDesktop(target, wake) {
|
|
170
|
+
if ('hyperv' in wake) {
|
|
171
|
+
return (0, hyperv_1.startHyperV)(target, wake.hyperv);
|
|
172
|
+
}
|
|
154
173
|
const script = 'vm' in wake
|
|
155
174
|
? `VM=${(0, deskScript_1.shq)(wake.vm)}\n${VM_SCRIPT}`
|
|
156
175
|
: `MAC=${(0, deskScript_1.shq)(wake.mac)}\nBCAST=${(0, deskScript_1.shq)(wake.broadcast ?? '255.255.255.255')}\nPORT=${wake.port ?? 9}\n${WOL_SCRIPT}`;
|
|
157
176
|
const out = await target.exec('sh -s', script);
|
|
158
|
-
|
|
177
|
+
// The host's words, as much of them as a status line shows (see shutDownWhenIdle).
|
|
178
|
+
const err = /^RD_ERR (.{0,1000})/m.exec(out);
|
|
159
179
|
if (err) {
|
|
160
180
|
throw new Error(err[1].trim());
|
|
161
181
|
}
|
|
162
|
-
return /^RD_OK (
|
|
182
|
+
return /^RD_OK (.{0,1000})/m.exec(out)?.[1] ?? 'no result';
|
|
163
183
|
}
|
|
164
184
|
/**
|
|
165
|
-
* Probes until the
|
|
166
|
-
* 3 s; `progress` is called every 5 s.
|
|
185
|
+
* Probes until the desktop is up (true; see desktopUp), the time is up or `stopped()` (false). At most one probe
|
|
186
|
+
* every 3 s; `progress` is called every 5 s.
|
|
167
187
|
*/
|
|
168
|
-
async function waitForRdp(target, host, port, timeoutMs, stopped, progress) {
|
|
188
|
+
async function waitForRdp(target, host, port, timeoutMs, stopped, progress, wake) {
|
|
169
189
|
const deadline = Date.now() + timeoutMs;
|
|
170
190
|
const ticker = setInterval(() => stopped() || progress(), 5000);
|
|
171
191
|
try {
|
|
172
192
|
while (Date.now() < deadline && !stopped()) {
|
|
173
193
|
const started = Date.now();
|
|
174
|
-
if (await
|
|
194
|
+
if (await desktopUp(target, host, port, wake).catch(() => false)) {
|
|
175
195
|
return !stopped();
|
|
176
196
|
}
|
|
177
197
|
await new Promise(resolve => setTimeout(resolve, Math.max(0, 3000 - (Date.now() - started))));
|
|
@@ -182,4 +202,3 @@ async function waitForRdp(target, host, port, timeoutMs, stopped, progress) {
|
|
|
182
202
|
clearInterval(ticker);
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
|
-
//# sourceMappingURL=wake.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-rdp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Remote desktops in Tabby's SSH tabs: Linux (GNOME) and Windows, through the SSH connection, with clipboard, files and sound. Built on IronRDP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tabby-plugin",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"main": "dist/index.js",
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
|
+
"!dist/**/*.map",
|
|
29
30
|
"vendor",
|
|
30
31
|
"remote",
|
|
31
32
|
"LICENSE",
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsc -p tsconfig.json",
|
|
39
|
-
"watch": "tsc -p tsconfig.json --watch",
|
|
40
|
+
"watch": "tsc -p tsconfig.json --watch --sourceMap",
|
|
40
41
|
"typecheck": "tsc -p tsconfig.json --noEmit && npm run typecheck:test",
|
|
41
42
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
42
43
|
"test": "tsx test/run.ts",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"prepublishOnly": "npm run build"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"ws": "^8.
|
|
49
|
+
"ws": "^8.21.1"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@angular/core": "15.2.10",
|