tabby-rdp 0.5.2 → 0.5.3
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 +49 -0
- package/README.md +2 -1
- package/TROUBLESHOOTING.md +7 -2
- package/dist/accounts.js +3 -0
- package/dist/connectionStatus.js +21 -4
- package/dist/desktop.service.js +241 -73
- package/dist/desktopForm.js +3 -3
- package/dist/ironrdp.js +26 -0
- package/dist/keyboard.js +12 -1
- package/dist/rdcleanpath.js +17 -4
- package/dist/rdpFile.js +13 -4
- package/dist/rdpProfile.js +44 -14
- package/dist/releases.js +3 -0
- package/dist/settingsPage.js +19 -14
- package/package.json +3 -3
- package/vendor/SHA256SUMS +1 -0
- package/vendor/ironrdp-factory.js +3237 -0
package/dist/rdpFile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MICROPHONE_NOTE = void 0;
|
|
3
|
+
exports.MAX_RDP_FILE = exports.MICROPHONE_NOTE = void 0;
|
|
4
4
|
exports.parseRdpFile = parseRdpFile;
|
|
5
5
|
/**
|
|
6
6
|
* The note for a file that asks for the microphone: not applied while the setting is off. With it on, every desktop
|
|
@@ -53,6 +53,11 @@ function decode(data) {
|
|
|
53
53
|
}
|
|
54
54
|
return b.toString('utf8');
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The most an .rdp file may be: one Remote Desktop Connection saves is a few KB, a signed one a few more. A larger one is
|
|
58
|
+
* refused before it is read: what it holds would be parsed on the thread that runs Tabby's window.
|
|
59
|
+
*/
|
|
60
|
+
exports.MAX_RDP_FILE = 1024 * 1024;
|
|
56
61
|
/** The connection a .rdp file describes, or null without a usable `full address`. */
|
|
57
62
|
function parseRdpFile(data) {
|
|
58
63
|
const settings = new Map();
|
|
@@ -60,9 +65,13 @@ function parseRdpFile(data) {
|
|
|
60
65
|
// twice, differently, gets the narrower of the two rather than whichever line comes last.
|
|
61
66
|
let clipboardOff = false;
|
|
62
67
|
for (const line of decode(data).split(/\r?\n/)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
// The name up to the first colon, trimmed after: a pattern with room for spaces on both sides of the name (as
|
|
69
|
+
// `\s*([^:]+?)\s*:` had) tries every way of splitting a line of spaces without a colon before it fails, in time
|
|
70
|
+
// that grows with the cube of its length, and a line of a few thousand spaces held up Tabby's window for seconds
|
|
71
|
+
// (security review, finding 2). This one takes a line's length, whatever it holds.
|
|
72
|
+
const m = /^([^:]*):\s*([sib])\s*:(.*)$/i.exec(line);
|
|
73
|
+
const key = m?.[1].trim().toLowerCase();
|
|
74
|
+
if (m && key) {
|
|
66
75
|
const value = m[3].trim();
|
|
67
76
|
settings.set(key, value);
|
|
68
77
|
clipboardOff ||= key === 'redirectclipboard' && /^[+-]?\d+$/.test(value) && Number(value) === 0;
|
package/dist/rdpProfile.js
CHANGED
|
@@ -124,7 +124,7 @@ exports.RDPTabRecovery = RDPTabRecovery = __decorate([
|
|
|
124
124
|
// ---- profile settings --------------------------------------------------------------------------
|
|
125
125
|
/**
|
|
126
126
|
* The profile's own fields in Tabby's profile editor (name, group, icon and color are Tabby's). Plain DOM like the
|
|
127
|
-
* rest of the plugin
|
|
127
|
+
* rest of the plugin. Address and gateway edits stay in the form until Save validates both of them.
|
|
128
128
|
*/
|
|
129
129
|
let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
130
130
|
constructor(element, injector) {
|
|
@@ -188,6 +188,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
188
188
|
Applies on the next connection.</div>
|
|
189
189
|
</div>`;
|
|
190
190
|
const field = (name) => root.querySelector(`[name="${name}"]`);
|
|
191
|
+
this.connectionInputs = { address: field('address'), gateway: field('gateway') };
|
|
191
192
|
field('address').value = o.host ? (0, desktopForm_1.formatAddress)(o.host, o.port || 3389) : '';
|
|
192
193
|
field('kind').value = o.kind === 'gnome' || o.kind === 'xrdp' ? o.kind : 'windows';
|
|
193
194
|
field('username').value = o.username ?? '';
|
|
@@ -196,10 +197,6 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
196
197
|
const text = field('address').value;
|
|
197
198
|
const address = (0, desktopForm_1.parseAddress)(text);
|
|
198
199
|
root.querySelector('[data-for=address]').textContent = text.trim() && !address ? 'This doesn\'t look like an address.' : '';
|
|
199
|
-
if (address) {
|
|
200
|
-
o.host = address.host;
|
|
201
|
-
o.port = address.port;
|
|
202
|
-
}
|
|
203
200
|
});
|
|
204
201
|
field('kind').addEventListener('change', () => {
|
|
205
202
|
const kind = field('kind').value;
|
|
@@ -227,12 +224,12 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
227
224
|
ownAccount();
|
|
228
225
|
field('username').addEventListener('input', () => { o.username = field('username').value.trim(); });
|
|
229
226
|
field('domain').addEventListener('input', () => { o.domain = field('domain').value.trim(); });
|
|
230
|
-
// The gateway, and its account
|
|
227
|
+
// The gateway, and its separate account or sign-in prompt.
|
|
231
228
|
const gateway = field('gateway');
|
|
232
229
|
const gatewayAccount = field('gatewayAccount');
|
|
233
230
|
gateway.value = o.gateway ?? '';
|
|
234
|
-
gatewayAccount.append(new Option('The same as the desktop\'s', ''), ...accounts.map(a => new Option(label(a), a.id)));
|
|
235
|
-
if (o.gatewayAccount && !accounts.some(a => a.id === o.gatewayAccount)) {
|
|
231
|
+
gatewayAccount.append(new Option('The same as the desktop\'s', ''), new Option('Ask for a gateway account when connecting', accounts_1.ASK_GATEWAY_ACCOUNT), ...accounts.map(a => new Option(label(a), a.id)));
|
|
232
|
+
if (o.gatewayAccount && o.gatewayAccount !== accounts_1.ASK_GATEWAY_ACCOUNT && !accounts.some(a => a.id === o.gatewayAccount)) {
|
|
236
233
|
gatewayAccount.append(new Option('(a saved account that no longer exists)', o.gatewayAccount));
|
|
237
234
|
}
|
|
238
235
|
gatewayAccount.value = o.gatewayAccount ?? '';
|
|
@@ -241,10 +238,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
241
238
|
root.querySelector('[data-for=gateway]').textContent = text && !(0, gateway_1.parseGateway)(text) ? 'A name or address, with :port if it isn\'t 443.' : '';
|
|
242
239
|
root.querySelector('[data-gateway-account]').hidden = !text || gatewayAccount.options.length < 2;
|
|
243
240
|
};
|
|
244
|
-
gateway.addEventListener('input',
|
|
245
|
-
o.gateway = gateway.value.trim();
|
|
246
|
-
gatewayChanged();
|
|
247
|
-
});
|
|
241
|
+
gateway.addEventListener('input', gatewayChanged);
|
|
248
242
|
gatewayAccount.addEventListener('change', () => { o.gatewayAccount = gatewayAccount.value; });
|
|
249
243
|
gatewayChanged();
|
|
250
244
|
// Its own clipboard sharing, or the setting's (named, as it is now).
|
|
@@ -265,10 +259,45 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
265
259
|
}
|
|
266
260
|
save() {
|
|
267
261
|
const o = this.profile.options;
|
|
262
|
+
if (this.connectionInputs) {
|
|
263
|
+
const { address: addressInput, gateway: gatewayInput } = this.connectionInputs;
|
|
264
|
+
const addressText = addressInput.value.trim();
|
|
265
|
+
const address = (0, desktopForm_1.parseAddress)(addressText);
|
|
266
|
+
const gatewayText = gatewayInput.value.trim();
|
|
267
|
+
// Empty addresses are useful in group/type defaults. They clear the destination instead of retaining it.
|
|
268
|
+
const addressError = addressText && !address ? 'This doesn\'t look like an address.' : '';
|
|
269
|
+
const gatewayError = gatewayText && !(0, gateway_1.parseGateway)(gatewayText) ? 'A name or address, with :port if it isn\'t 443.' : '';
|
|
270
|
+
const root = this.element.nativeElement;
|
|
271
|
+
root.querySelector('[data-for=address]').textContent = addressError;
|
|
272
|
+
root.querySelector('[data-for=gateway]').textContent = gatewayError;
|
|
273
|
+
if (addressError || gatewayError) {
|
|
274
|
+
;
|
|
275
|
+
(addressError ? addressInput : gatewayInput).focus();
|
|
276
|
+
// Tabby invokes this synchronous hook before cleaning the proxy and closing the modal. Throwing
|
|
277
|
+
// stops that Save operation; a return value would be ignored by the host.
|
|
278
|
+
throw new Error(`Cannot save this RDP profile: ${addressError || gatewayError}`);
|
|
279
|
+
}
|
|
280
|
+
// Commit the two connection fields together only after both drafts pass validation.
|
|
281
|
+
o.host = address?.host ?? '';
|
|
282
|
+
// A blank destination carries no port edit: keep custom/inherited ports in group and type defaults.
|
|
283
|
+
if (address) {
|
|
284
|
+
o.port = address.port;
|
|
285
|
+
}
|
|
286
|
+
o.gateway = gatewayText;
|
|
287
|
+
}
|
|
268
288
|
const { id, account, via, gateway } = this.before;
|
|
269
289
|
if (!id) {
|
|
270
290
|
return;
|
|
271
291
|
}
|
|
292
|
+
if (!o.host) {
|
|
293
|
+
// A saved profile's address cleared: the desktop it led to is gone from it, as when the profile is removed
|
|
294
|
+
// (see RDPProfilesService.deleteProfile). Not for the type's or a group's defaults, which have no id of
|
|
295
|
+
// their own (an empty address is useful there): profiles that name that address keep what is saved for it.
|
|
296
|
+
if (!via && this.profile.id) {
|
|
297
|
+
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop(id, true, `"${this.profile.name}"`).catch(() => null);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
272
301
|
if ((o.via ?? '') !== via) {
|
|
273
302
|
// Reached another way, it is another desktop. A direct one's saved passwords (through any gateway, the ones
|
|
274
303
|
// it had before included) and certificate were its own; one behind a host shares them with that host's
|
|
@@ -374,8 +403,9 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
|
|
|
374
403
|
}
|
|
375
404
|
deleteProfile(profile) {
|
|
376
405
|
// 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.
|
|
378
|
-
|
|
406
|
+
// its own; one behind a host shares them with that host's desktop at the same address. A profile without an
|
|
407
|
+
// address of its own leads to no desktop of its own: desktopIdOf would name 127.0.0.1:3389 for it.
|
|
408
|
+
if (!profile.options.via && (0, desktops_1.configText)(profile.options.host)) {
|
|
379
409
|
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop((0, desktops_1.desktopIdOf)(profile.options), true, `"${profile.name}"`).catch(() => null);
|
|
380
410
|
}
|
|
381
411
|
}
|
package/dist/releases.js
CHANGED
|
@@ -310,6 +310,7 @@ class ReleaseManager {
|
|
|
310
310
|
if (reason) {
|
|
311
311
|
throw new Error(reason);
|
|
312
312
|
}
|
|
313
|
+
const targetFloor = this.catalog.versions.find(p => p.version === version).policy.rollbackFloor;
|
|
313
314
|
if (version === this.running) {
|
|
314
315
|
progress('success', `tabby-rdp ${version} is already running.`);
|
|
315
316
|
return;
|
|
@@ -339,6 +340,8 @@ class ReleaseManager {
|
|
|
339
340
|
this.state.previous = this.running;
|
|
340
341
|
this.state.pending = version;
|
|
341
342
|
this.state.removed = false;
|
|
343
|
+
// Record the target's data boundary before restart, while older windows may still be running.
|
|
344
|
+
this.state.rollbackFloor = greatest(this.state.rollbackFloor, targetFloor);
|
|
342
345
|
if (returnToStable) {
|
|
343
346
|
this.state.channel = 'stable';
|
|
344
347
|
}
|
package/dist/settingsPage.js
CHANGED
|
@@ -597,29 +597,34 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
|
|
|
597
597
|
const accounts = this.desktop.accounts();
|
|
598
598
|
const accountOf = (id) => id ? accounts.find(a => a.id === id) : undefined;
|
|
599
599
|
// Remote desktop profiles first (Tabby's editor), then the desktops added from SSH tabs (the plugin's form).
|
|
600
|
-
const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE
|
|
601
|
-
const
|
|
600
|
+
const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE);
|
|
601
|
+
const gatewaySignIn = (id) => {
|
|
602
|
+
const account = accountOf(id);
|
|
603
|
+
return account ? `, as ${(0, help_1.esc)(account.name)}` : id === accounts_1.ASK_GATEWAY_ACCOUNT ? ', asks for a gateway account'
|
|
604
|
+
: id ? ', saved gateway account missing (asks when connecting)' : '';
|
|
605
|
+
};
|
|
602
606
|
// Every field as text whatever the config holds (see configText): one that can't be shown would leave this list,
|
|
603
607
|
// and the lists after it on the page, unbuilt.
|
|
604
|
-
const profileRows = profiles.
|
|
605
|
-
const o = p.options;
|
|
608
|
+
const profileRows = profiles.flatMap(p => {
|
|
609
|
+
const o = this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options;
|
|
610
|
+
if (!o?.host) {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
606
613
|
const address = `${(0, desktops_1.configText)(o.host)}:${(0, desktops_1.configText)(o.port || 3389)}`;
|
|
607
614
|
const via = o.via ? (0, desktops_1.configText)((this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name) || 'an SSH profile' : null;
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
// Its own clipboard, or its group's or the type's default: what it connects with, as Tabby resolves the profile.
|
|
611
|
-
const clipboard = (0, clipboard_1.ownClipboard)(this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options?.clipboard);
|
|
615
|
+
const account = accountOf(o.account);
|
|
616
|
+
const clipboard = (0, clipboard_1.ownClipboard)(o.clipboard);
|
|
612
617
|
const [username, kind, gateway] = [(0, desktops_1.configText)(o.username), (0, desktops_1.configText)(o.kind), (0, desktops_1.configText)(o.gateway)];
|
|
613
618
|
const details = [
|
|
614
619
|
account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
|
|
615
620
|
kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
|
|
616
|
-
gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${
|
|
621
|
+
gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${gatewaySignIn(o.gatewayAccount)}` : '',
|
|
617
622
|
clipboard ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[clipboard].toLowerCase())}` : '',
|
|
618
623
|
].filter(Boolean).join('<br>');
|
|
619
|
-
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}` : ''}`, [
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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}` : ''}`, [
|
|
625
|
+
{ label: 'Edit…', run: () => { this.editProfile(p.id); } },
|
|
626
|
+
{ label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
|
|
627
|
+
])];
|
|
623
628
|
});
|
|
624
629
|
const desktops = this.desktop.configuredDesktops();
|
|
625
630
|
// An item that is no entry describes no desktop, and isn't listed (see isDesktopEntry).
|
|
@@ -634,7 +639,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
|
|
|
634
639
|
const details = [
|
|
635
640
|
account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
|
|
636
641
|
kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
|
|
637
|
-
gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${
|
|
642
|
+
gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${gatewaySignIn(d.gatewayAccount)}` : '',
|
|
638
643
|
vm ? `Starts VM ${(0, help_1.esc)(vm)} when off` : mac ? `Wakes ${(0, help_1.esc)(mac)} when off` : '',
|
|
639
644
|
(0, clipboard_1.ownClipboard)(d.clipboard) ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[(0, clipboard_1.ownClipboard)(d.clipboard)].toLowerCase())}` : '',
|
|
640
645
|
].filter(Boolean).join('<br>');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabby-rdp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"typecheck": "tsc -p tsconfig.json --noEmit && npm run typecheck:test",
|
|
44
44
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
45
45
|
"test": "tsx test/run.ts",
|
|
46
|
-
"test:smoke": "tsx test/run.ts --packed --hidden smoke-host smoke-rdp",
|
|
46
|
+
"test:smoke": "tsx test/run.ts --packed --hidden smoke-host smoke-rdp direct",
|
|
47
47
|
"test:unit": "tsx --test test/unit/*.ts",
|
|
48
48
|
"build:ironrdp": "scripts/build-ironrdp.sh",
|
|
49
49
|
"prepublishOnly": "npm run build"
|
|
@@ -65,6 +65,6 @@
|
|
|
65
65
|
},
|
|
66
66
|
"tabbyRdp": {
|
|
67
67
|
"minimumTabbyVersion": "1.0.236",
|
|
68
|
-
"rollbackFloor": "0.5.
|
|
68
|
+
"rollbackFloor": "0.5.3"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/vendor/SHA256SUMS
CHANGED
|
@@ -24,3 +24,4 @@ d67aa6029edb142daa1e5069fa79ab1917687474a89a61bd221081f204ba8e12 ironrdp/patche
|
|
|
24
24
|
538641d863e52972074b032c111a67cfa02d06290bff0601423f3dfd25ad563b vendor/iron-remote-desktop-rdp.js
|
|
25
25
|
5a6363b9140a5a2abf0218aa39edb38f10445e8d7e9c5a798c321772e1f07c11 vendor/iron-remote-desktop.js
|
|
26
26
|
31f961cc5fc60f6a3dc580333f871bf9b380950dc849395465844dcc90f871f3 vendor/ironrdp_web_bg.wasm
|
|
27
|
+
82470ad2980db3fe48787cb821e231fc44811b046f7820555f6e25f2b4ccce28 vendor/ironrdp-factory.js
|