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/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
- const m = /^\s*([^:]+?)\s*:\s*([sib])\s*:(.*)$/i.exec(line);
64
- if (m) {
65
- const key = m[1].toLowerCase();
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;
@@ -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, writing to the profile as you type.
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 where that isn't the desktop's (offered once there is a gateway and a saved 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
- if (!profile.options.via) {
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
  }
@@ -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 && p.options?.host);
601
- const groups = this.config.store.groups ?? [];
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.map(p => {
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 inherited = o.account === undefined ? groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account : undefined;
609
- const account = accountOf(o.account || inherited);
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)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
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
- { label: 'Edit…', run: () => { this.editProfile(p.id); } },
621
- { label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
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)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
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.2",
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.1"
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