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.
Files changed (76) hide show
  1. package/CHANGELOG.md +580 -0
  2. package/README.md +252 -34
  3. package/dist/accountForm.js +0 -1
  4. package/dist/accounts.js +0 -1
  5. package/dist/audio.js +0 -1
  6. package/dist/authorities.js +331 -0
  7. package/dist/byteQueue.js +102 -0
  8. package/dist/clipboard.js +106 -0
  9. package/dist/connectionStatus.js +0 -1
  10. package/dist/desk.js +16 -47
  11. package/dist/deskScript.js +196 -6
  12. package/dist/desktop.service.js +1763 -204
  13. package/dist/desktopForm.js +72 -13
  14. package/dist/desktops.js +243 -19
  15. package/dist/drives.js +525 -0
  16. package/dist/fileTransfer.js +1064 -72
  17. package/dist/gateway.js +651 -0
  18. package/dist/header.js +76 -5
  19. package/dist/help.js +34 -13
  20. package/dist/hyperv.js +129 -0
  21. package/dist/index.js +2 -2
  22. package/dist/keyboard.js +0 -1
  23. package/dist/microphone.js +57 -13
  24. package/dist/ntlm.js +225 -0
  25. package/dist/osd.js +56 -15
  26. package/dist/probe.js +15 -3
  27. package/dist/rdcleanpath.js +468 -52
  28. package/dist/rdpFile.js +53 -4
  29. package/dist/rdpProfile.js +75 -22
  30. package/dist/remoteSetup.js +103 -23
  31. package/dist/sessionLog.js +53 -0
  32. package/dist/settingsPage.js +117 -27
  33. package/dist/signin.js +429 -54
  34. package/dist/ssh.js +99 -18
  35. package/dist/sshChannelStream.js +50 -6
  36. package/dist/targets.js +185 -39
  37. package/dist/ui.js +98 -20
  38. package/dist/unshowable.js +41 -0
  39. package/dist/updates.js +37 -2
  40. package/dist/vms.js +36 -5
  41. package/dist/wake.js +31 -12
  42. package/package.json +4 -3
  43. package/remote/trd-pty.py +74 -13
  44. package/vendor/SHA256SUMS +26 -0
  45. package/vendor/iron-remote-desktop-rdp.js +418 -390
  46. package/vendor/ironrdp_web_bg.wasm +0 -0
  47. package/dist/accountForm.js.map +0 -1
  48. package/dist/accounts.js.map +0 -1
  49. package/dist/audio.js.map +0 -1
  50. package/dist/connectionStatus.js.map +0 -1
  51. package/dist/desk.js.map +0 -1
  52. package/dist/deskScript.js.map +0 -1
  53. package/dist/desktop.service.js.map +0 -1
  54. package/dist/desktopForm.js.map +0 -1
  55. package/dist/desktops.js.map +0 -1
  56. package/dist/fileTransfer.js.map +0 -1
  57. package/dist/header.js.map +0 -1
  58. package/dist/help.js.map +0 -1
  59. package/dist/index.js.map +0 -1
  60. package/dist/keyboard.js.map +0 -1
  61. package/dist/microphone.js.map +0 -1
  62. package/dist/osd.js.map +0 -1
  63. package/dist/probe.js.map +0 -1
  64. package/dist/rdcleanpath.js.map +0 -1
  65. package/dist/rdpFile.js.map +0 -1
  66. package/dist/rdpProfile.js.map +0 -1
  67. package/dist/remoteSetup.js.map +0 -1
  68. package/dist/settingsPage.js.map +0 -1
  69. package/dist/signin.js.map +0 -1
  70. package/dist/ssh.js.map +0 -1
  71. package/dist/sshChannelStream.js.map +0 -1
  72. package/dist/targets.js.map +0 -1
  73. package/dist/ui.js.map +0 -1
  74. package/dist/updates.js.map +0 -1
  75. package/dist/vms.js.map +0 -1
  76. package/dist/wake.js.map +0 -1
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.certificateAuthorities = certificateAuthorities;
4
+ exports.chainStanding = chainStanding;
5
+ exports.whyNotValid = whyNotValid;
6
+ exports.certificateDetails = certificateDetails;
7
+ exports.describeCertificate = describeCertificate;
8
+ exports.vouched = vouched;
9
+ /**
10
+ * What a server's certificate is checked against where an authority's word is enough to connect without asking (a
11
+ * desktop connected to directly, an RD Gateway; see RemoteDesktopService.checkTrusted), why one that fails doesn't
12
+ * pass, and what a certificate says of itself, in words for the user.
13
+ */
14
+ const crypto_1 = require("crypto");
15
+ const tls = require("tls");
16
+ const unshowable_1 = require("./unshowable");
17
+ let authorities = null;
18
+ let systemRead = false;
19
+ /**
20
+ * The certificate authorities a server's certificate is checked against: Node's own list (Mozilla's, and any in
21
+ * NODE_EXTRA_CA_CERTS) and those of this computer's own store, such as an organisation's authority an administrator
22
+ * installed, where Tabby's Node.js can read that store (tls.getCACertificates, Node 22.15 and later). Read once.
23
+ * Undefined: Node's own list alone, its default.
24
+ */
25
+ function certificateAuthorities() {
26
+ if (authorities === null) {
27
+ authorities = undefined;
28
+ const read = tls.getCACertificates;
29
+ if (typeof read === 'function') {
30
+ try {
31
+ const own = read('default');
32
+ try {
33
+ authorities = [...new Set([...own, ...read('system')])];
34
+ systemRead = true;
35
+ }
36
+ catch {
37
+ authorities = own;
38
+ }
39
+ }
40
+ catch {
41
+ authorities = undefined;
42
+ }
43
+ }
44
+ }
45
+ return authorities;
46
+ }
47
+ /** Anchors by the `ca` list they were read from. */
48
+ const anchorsByList = new WeakMap();
49
+ function anchorsOf(authorities) {
50
+ const cached = typeof authorities === 'object' && authorities ? anchorsByList.get(authorities) : undefined;
51
+ if (cached) {
52
+ return cached;
53
+ }
54
+ const found = { fingerprints: new Set(), keys: new Set() };
55
+ for (const item of [authorities ?? []].flat()) {
56
+ for (const pem of String(item).matchAll(/-----BEGIN CERTIFICATE-----([^-]+)-----END CERTIFICATE-----/g)) {
57
+ const der = Buffer.from(pem[1].replace(/\s/g, ''), 'base64');
58
+ const hex = (0, crypto_1.createHash)('sha256').update(der).digest('hex').toUpperCase();
59
+ found.fingerprints.add(hex.replace(/(..)(?!$)/g, '$1:'));
60
+ const key = nameAndKey(der);
61
+ if (key) {
62
+ found.keys.add(key);
63
+ }
64
+ }
65
+ }
66
+ if (typeof authorities === 'object' && authorities) {
67
+ anchorsByList.set(authorities, found);
68
+ }
69
+ return found;
70
+ }
71
+ /**
72
+ * A certificate's subject and public key (from its DER), by which an authority is known whichever of its certificates
73
+ * comes: a certificate below it was signed with that key. '' when it can't be read.
74
+ */
75
+ function nameAndKey(der) {
76
+ try {
77
+ const certificate = new crypto_1.X509Certificate(der);
78
+ return `${certificate.subject}\n${certificate.publicKey.export({ type: 'spki', format: 'der' }).toString('base64')}`;
79
+ }
80
+ catch {
81
+ return '';
82
+ }
83
+ }
84
+ /** Whether `issuer`'s key made the signature on `certificate` (both as Node reads them: their DER is `raw`). */
85
+ function signedBy(certificate, issuer) {
86
+ try {
87
+ return new crypto_1.X509Certificate(certificate.raw).verify(new crypto_1.X509Certificate(issuer.raw).publicKey);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Whether a certificate (its DER) names itself as its issuer and is signed with its own key, whatever its key usage.
95
+ * Node makes such a certificate its own issuer only where its key usage allows signing certificates, which that of
96
+ * Windows' own RDP certificate (key and data encipherment) and of a self-signed gateway's often don't.
97
+ */
98
+ function selfSigned(der) {
99
+ try {
100
+ const certificate = new crypto_1.X509Certificate(der);
101
+ return certificate.issuer === certificate.subject && certificate.verify(certificate.publicKey);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ /** Whether a certificate (its DER) may issue others: an authority by its basic constraints (and key usage, if any). */
108
+ function issuesCertificates(der) {
109
+ try {
110
+ return new crypto_1.X509Certificate(der).ca;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ /**
117
+ * Where the chain of a server's certificate stands (see ChainStanding) by `authorities`: the `ca` its connection was
118
+ * checked against, or Node's own list without one. From Node's reading of the chain (getPeerCertificate(true)), which
119
+ * follows the certificates the server sent, then those authorities, by the issuer each certificate names. Each link is
120
+ * checked here as the TLS library checks it: the certificate was signed with the next one's key, and the next one may
121
+ * issue certificates (an authority by its basic constraints, or one of the authorities whatever it says: an old root
122
+ * says nothing). So a certificate that only names an authority, or one signed with the key of an authority's
123
+ * certificate for a server, doesn't count as that authority's. One of the authorities is known by its fingerprint, or
124
+ * by its subject and key (see Anchors), as the TLS library takes it.
125
+ *
126
+ * Node's code for why a certificate isn't valid (TLSSocket.authorizationError) is the last of the TLS library's
127
+ * checks that failed, and whoever makes the certificate can choose which: one that is marked for other uses than a
128
+ * server's (INVALID_PURPOSE), or that has expired, gets that code whoever issued it, a made-up authority included.
129
+ * This says what the code can hide.
130
+ */
131
+ function chainStanding(certificate, authorities) {
132
+ const anchors = anchorsOf(authorities ?? certificateAuthorities() ?? tls.rootCertificates);
133
+ const anchor = (c) => anchors.fingerprints.has(c.fingerprint256) || anchors.keys.has(nameAndKey(c.raw));
134
+ const seen = new Set();
135
+ let current = certificate;
136
+ while (current?.raw && !seen.has(current.fingerprint256)) {
137
+ if (anchor(current)) {
138
+ return 'authority';
139
+ }
140
+ seen.add(current.fingerprint256);
141
+ if (selfSigned(current.raw)) {
142
+ return current === certificate ? 'self-signed' : 'unknown authority';
143
+ }
144
+ // Node also makes a certificate that names itself as its issuer, but wasn't signed with its own key, its own
145
+ // issuer: the one that signed it isn't there.
146
+ const issuer = current.issuerCertificate;
147
+ if (!issuer?.raw || issuer.fingerprint256 === current.fingerprint256 || !signedBy(current, issuer) || !(issuesCertificates(issuer.raw) || anchor(issuer))) {
148
+ return 'incomplete';
149
+ }
150
+ current = issuer;
151
+ }
152
+ return 'incomplete';
153
+ }
154
+ /** Server authentication, in an extended key usage. */
155
+ const SERVER_AUTH = '1.3.6.1.5.5.7.3.1';
156
+ /** Whether a certificate's details (see CertificateDetails) mark it for other uses than a server's. */
157
+ function otherUses(details) {
158
+ return !!details?.extendedKeyUsage && !details.extendedKeyUsage.includes(SERVER_AUTH);
159
+ }
160
+ /**
161
+ * Why a certificate isn't valid for `name` by those authorities, in words. Where the plugin checked it itself
162
+ * (`details` with a `chain`, see certificateDetails): first where its chain stands, unless it leads to an authority
163
+ * this computer trusts (otherwise nothing in the certificate is any authority's word); then whether it was issued for
164
+ * another name, its dates and the uses it is marked for; and, for a chain that leads to an authority, what else the
165
+ * TLS library found (Node's code, from TLSSocket.authorizationError), which is then about another certificate in the
166
+ * chain, or a check of the library's own. Without those checks, from Node's code alone. An unknown code is named as it
167
+ * is.
168
+ */
169
+ function whyNotValid(reason, name, details) {
170
+ certificateAuthorities();
171
+ // The server sent its authority's own certificate, which this computer doesn't trust. Without the system's store,
172
+ // an organisation's own authority is one Tabby doesn't know, whatever the computer trusts: said so rather than
173
+ // claimed otherwise.
174
+ const untrusted = systemRead
175
+ ? 'it isn\'t from a certificate authority this computer trusts'
176
+ : 'it isn\'t from a certificate authority Tabby checks against (the public ones; an organisation\'s own counts as unknown here)';
177
+ // No way to an authority from what the server sent: the issuer a certificate names is its own word. Windows fetches
178
+ // an intermediate certificate a server leaves out by itself; Node doesn't. But that is no likelier than a
179
+ // certificate naming an issuer that never signed it, which anyone can make.
180
+ const incomplete = `no certificate authority ${systemRead ? 'this computer trusts' : 'Tabby checks against (the public ones; an organisation\'s own counts as unknown here)'} ` +
181
+ 'vouches for it from what the server sent (a server can leave out an intermediate certificate, which Tabby doesn\'t fetch, but any server can name any issuer)';
182
+ const selfSigned = 'it is self-signed (made by the server itself, as RDP servers make theirs unless given one), so no certificate authority vouches for it';
183
+ const anotherName = `it was issued for another name than ${name}`;
184
+ const expired = 'it has expired';
185
+ const notYet = 'it isn\'t valid yet (or this computer\'s clock is wrong)';
186
+ const marked = 'it is marked for other uses than a server\'s (its extended key usage)';
187
+ const chain = details?.chain;
188
+ if (chain) {
189
+ const parts = chain === 'self-signed' ? [selfSigned] : chain === 'unknown authority' ? [untrusted] : chain === 'incomplete' ? [incomplete] : [];
190
+ // Node checks the name only once the chain checked out; the plugin checked it whatever the chain.
191
+ const named = details.forName === undefined ? reason !== 'ERR_TLS_CERT_ALTNAME_INVALID' : details.forName;
192
+ if (!named) {
193
+ parts.push(anotherName);
194
+ }
195
+ // From its own dates: the code can be another certificate's, or another check's.
196
+ const now = Date.now();
197
+ const from = Date.parse(details.validFrom);
198
+ const to = Date.parse(details.validTo);
199
+ const past = Number.isNaN(to) ? reason === 'CERT_HAS_EXPIRED' : to < now;
200
+ const early = Number.isNaN(from) ? reason === 'CERT_NOT_YET_VALID' : from > now;
201
+ if (past) {
202
+ parts.push(expired);
203
+ }
204
+ if (early) {
205
+ parts.push(notYet);
206
+ }
207
+ // An organisation's Remote Desktop certificate template can name only the Remote Desktop use, not a server's:
208
+ // said so only of a certificate for this name whose chain leads to an authority this computer trusts.
209
+ // Otherwise anyone may have made it so.
210
+ const uses = otherUses(details);
211
+ if (uses) {
212
+ parts.push(chain === 'authority' && named ? `${marked}, as some organisations' Remote Desktop certificates are` : marked);
213
+ }
214
+ // A chain that leads to an authority, and a code that none of the above accounts for: it is about another
215
+ // certificate in the chain (an intermediate one that has expired, one marked for other uses), or a check of the
216
+ // TLS library's own.
217
+ const accounted = reason === 'ERR_TLS_CERT_ALTNAME_INVALID' ? !named : reason === 'CERT_HAS_EXPIRED' ? past
218
+ : reason === 'CERT_NOT_YET_VALID' ? early : reason === 'INVALID_PURPOSE' ? uses : false;
219
+ if (chain === 'authority' && !accounted) {
220
+ parts.push(reason === 'CERT_HAS_EXPIRED' ? 'a certificate in its chain has expired'
221
+ : reason === 'CERT_NOT_YET_VALID' ? 'a certificate in its chain isn\'t valid yet (or this computer\'s clock is wrong)'
222
+ : reason === 'INVALID_PURPOSE' ? 'a certificate in its chain isn\'t one that may vouch for it'
223
+ : `it couldn't be verified${reason ? ` (${reason})` : ''}`);
224
+ }
225
+ return parts.length > 1 ? `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}` : parts[0] ?? 'it couldn\'t be verified';
226
+ }
227
+ switch (reason) {
228
+ case 'DEPTH_ZERO_SELF_SIGNED_CERT':
229
+ return selfSigned;
230
+ case 'SELF_SIGNED_CERT_IN_CHAIN':
231
+ return untrusted;
232
+ case 'UNABLE_TO_GET_ISSUER_CERT':
233
+ case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY':
234
+ case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE':
235
+ return incomplete;
236
+ case 'INVALID_PURPOSE':
237
+ // Without the chain checked, the code may stand for a certificate anyone made (see chainStanding).
238
+ return `${marked}; it may also be self-signed, or not from a certificate authority this computer trusts`;
239
+ case 'CERT_HAS_EXPIRED':
240
+ return expired;
241
+ case 'CERT_NOT_YET_VALID':
242
+ return notYet;
243
+ case 'ERR_TLS_CERT_ALTNAME_INVALID':
244
+ return anotherName;
245
+ case '':
246
+ return 'no certificate authority vouches for it';
247
+ default:
248
+ return `it couldn't be verified (${reason})`;
249
+ }
250
+ }
251
+ /**
252
+ * The codes of the TLS library's verdict (TLSSocket.authorizationError) that the plugin's own reading of a certificate
253
+ * accounts for, as whyNotValid puts them: where its chain stands, its name, its dates and the uses it is marked for.
254
+ * For a chain that leads to an authority this computer trusts, any other code is a check the library made and the
255
+ * plugin doesn't (a limit the authority set on the names it signs for, on the length of a path, the strength of a key
256
+ * or a digest, a revocation): the chain's standing says nothing of what the library refused there.
257
+ */
258
+ const READ_REASONS = new Set([
259
+ 'ERR_TLS_CERT_ALTNAME_INVALID', 'CERT_HAS_EXPIRED', 'CERT_NOT_YET_VALID', 'INVALID_PURPOSE', 'DEPTH_ZERO_SELF_SIGNED_CERT',
260
+ 'SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'UNABLE_TO_GET_ISSUER_CERT', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
261
+ ]);
262
+ /**
263
+ * A certificate's details, from Node's reading of it (TLSSocket.getPeerCertificate(true)); undefined without one.
264
+ * `checked`: it isn't valid for `name` by `authorities` (as its connection was checked), for Node's `reason`, and the
265
+ * plugin checks it itself: where its chain stands, and whether it was issued for that name, which Node checks only
266
+ * once the chain checked out. So a name Node found wrong (ERR_TLS_CERT_ALTNAME_INVALID) says the TLS library took the
267
+ * chain to one of the authorities, whatever Node's reading of the chain shows (an issuer of the same name the server
268
+ * sent ahead of the one that signed, say).
269
+ */
270
+ function certificateDetails(certificate, checked) {
271
+ if (!certificate || typeof certificate !== 'object' || !certificate.valid_to) {
272
+ return undefined;
273
+ }
274
+ const uses = Array.isArray(certificate.ext_key_usage) ? certificate.ext_key_usage.filter(use => typeof use === 'string') : undefined;
275
+ let forName;
276
+ if (checked) {
277
+ try {
278
+ forName = !tls.checkServerIdentity(checked.name, certificate);
279
+ }
280
+ catch { }
281
+ }
282
+ // A field can hold several values (two common names, say), which Node gives as an array.
283
+ const text = (value) => Array.isArray(value) ? value.map(String).join(', ') : typeof value === 'string' ? value : '';
284
+ const issuer = [text(certificate.issuer?.CN), text(certificate.issuer?.O)].filter(Boolean);
285
+ const chain = checked ? checked.reason === 'ERR_TLS_CERT_ALTNAME_INVALID' ? 'authority' : chainStanding(certificate, checked.authorities) : undefined;
286
+ return {
287
+ subject: text(certificate.subject?.CN),
288
+ // `DNS:pc.example, IP Address:10.0.0.5`: the names and addresses, without their kind.
289
+ names: typeof certificate.subjectaltname === 'string'
290
+ ? certificate.subjectaltname.split(/, (?=[A-Za-z ]+:)/).map(name => name.replace(/^(DNS|IP Address):/, ''))
291
+ : [],
292
+ issuer: issuer.length === 2 && issuer[0] !== issuer[1] ? `${issuer[0]} (${issuer[1]})` : issuer[0] ?? '',
293
+ validFrom: String(certificate.valid_from ?? ''),
294
+ validTo: String(certificate.valid_to ?? ''),
295
+ ...uses ? { extendedKeyUsage: uses } : {},
296
+ ...chain ? { chain } : {},
297
+ ...forName === undefined ? {} : { forName },
298
+ ...chain === 'authority' && !READ_REASONS.has(checked?.reason ?? '') ? { unchecked: true } : {},
299
+ };
300
+ }
301
+ /**
302
+ * A certificate's details in a line: "Issued to <name> (<names>) by <issuer>, valid <from> to <to>." The server chose
303
+ * all of it, so what could reorder or hide part of it shows as U+FFFD, and each part is cut short. `unchecked`: no
304
+ * authority vouched for the certificate, so the line says that this is its own word, which nothing has checked: a
305
+ * certificate anyone made can name the issuer one would expect, and the question shown with it is where that matters.
306
+ * (One whose chain leads to an authority this computer trusts is that authority's word, whatever else is wrong with
307
+ * it, unless the TLS library refused it for a reason of its own: see vouched.)
308
+ */
309
+ function describeCertificate(details, unchecked = false) {
310
+ const date = (text) => {
311
+ const time = new Date(text);
312
+ return Number.isNaN(time.getTime()) ? (0, unshowable_1.showable)(text, 40) : `${time.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
313
+ };
314
+ const listed = details.names.slice(0, 8).join(', ') + (details.names.length > 8 ? `, and ${details.names.length - 8} more` : '');
315
+ const what = `${(0, unshowable_1.showable)(details.subject || '(no name)', 100)}${listed ? ` (${(0, unshowable_1.showable)(listed, 300)})` : ''} ` +
316
+ `by ${(0, unshowable_1.showable)(details.issuer || '(no name)', 100)}, valid ${date(details.validFrom)} to ${date(details.validTo)}`;
317
+ return unchecked
318
+ ? `It says it was issued to ${what}: its own word, which nothing here has checked; a certificate anyone made can say the same.`
319
+ : `Issued to ${what}.`;
320
+ }
321
+ /**
322
+ * Whether what a certificate says of itself (`details`) is more than its own word: an authority vouched for it
323
+ * (`valid`), or its chain leads to an authority this computer trusts, though the certificate isn't valid here (issued
324
+ * for another name, expired, marked for other uses): an authority's word all the same, and the names in it are what
325
+ * show it to be another machine's. Not where the TLS library refused the certificate for a reason the plugin's reading
326
+ * doesn't account for (`unchecked`, see READ_REASONS): the authority's word may be limited, say to other names, in a
327
+ * way nothing here checks.
328
+ */
329
+ function vouched(valid, details) {
330
+ return valid === true || details?.chain === 'authority' && !details.unchecked;
331
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ByteQueue = void 0;
4
+ /**
5
+ * Bytes from a stream, kept as they arrived and taken off the front a whole message at a time. Joining each arrival
6
+ * onto all that is still waiting copies all of it again every time: a message that comes a byte at a time (a server
7
+ * can send one-byte TLS records) costs the square of its size, on the thread that runs Tabby's window. Here a byte is
8
+ * copied in once, as it arrives, and at most once more, when the message it is in is taken.
9
+ *
10
+ * What arrives is copied into storage of the queue's own rather than kept as it came: a piece can be a few bytes of a
11
+ * much larger buffer (a TLS record that also held other frames, say), and keeping the piece would keep all of that
12
+ * buffer too. A sender that had each record add a byte to a message it never finishes could otherwise have a whole
13
+ * record kept for every byte. So the queue holds what is waiting, and at most two slabs more.
14
+ */
15
+ class ByteQueue {
16
+ constructor() {
17
+ /** Storage, oldest first: what is waiting runs from `offset` in the first slab to `filled` in the last. */
18
+ this.slabs = [];
19
+ this.offset = 0;
20
+ this.filled = 0;
21
+ /** How many bytes are waiting. */
22
+ this.length = 0;
23
+ }
24
+ push(chunk) {
25
+ for (let at = 0; at < chunk.length;) {
26
+ let last = this.slabs[this.slabs.length - 1];
27
+ if (!last || this.filled === last.length) {
28
+ if (!this.length) {
29
+ // Nothing is waiting: the slabs there are only hold what was taken.
30
+ this.slabs = [];
31
+ this.offset = 0;
32
+ }
33
+ // Not from Node's pool, whose slabs are shared with whatever else is small.
34
+ last = Buffer.allocUnsafeSlow(ByteQueue.SLAB);
35
+ this.slabs.push(last);
36
+ this.filled = 0;
37
+ }
38
+ const n = Math.min(chunk.length - at, last.length - this.filled);
39
+ last.set(chunk.subarray(at, at + n), this.filled);
40
+ this.filled += n;
41
+ this.length += n;
42
+ at += n;
43
+ }
44
+ }
45
+ /** Where what is waiting in slab `i` ends. */
46
+ endOf(i) {
47
+ return i === this.slabs.length - 1 ? this.filled : this.slabs[i].length;
48
+ }
49
+ /** The first `n` bytes (or all there are, if fewer), left in place: for a header, so `n` is small. */
50
+ peek(n) {
51
+ n = Math.min(n, this.length);
52
+ if (!n) {
53
+ return Buffer.alloc(0);
54
+ }
55
+ if (this.endOf(0) - this.offset >= n) {
56
+ return this.slabs[0].subarray(this.offset, this.offset + n);
57
+ }
58
+ const out = Buffer.alloc(n);
59
+ for (let i = 0, copied = 0; copied < n; i++) {
60
+ const from = i ? 0 : this.offset;
61
+ copied += this.slabs[i].copy(out, copied, from, Math.min(this.endOf(i), from + n - copied));
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Takes the first `n` bytes (at most all there are): part of the slab they are in when they are in one, else a
67
+ * copy, made once. What is written to a slab stays as it is, so such a part doesn't change after it is taken.
68
+ */
69
+ take(n) {
70
+ n = Math.min(n, this.length);
71
+ if (!n) {
72
+ return Buffer.alloc(0);
73
+ }
74
+ let out;
75
+ if (this.endOf(0) - this.offset >= n) {
76
+ out = this.slabs[0].subarray(this.offset, this.offset + n);
77
+ this.offset += n;
78
+ }
79
+ else {
80
+ out = Buffer.allocUnsafe(n);
81
+ for (let copied = 0; copied < n;) {
82
+ const end = Math.min(this.endOf(0), this.offset + n - copied);
83
+ copied += this.slabs[0].copy(out, copied, this.offset, end);
84
+ this.offset = end;
85
+ if (copied < n) {
86
+ this.slabs.shift();
87
+ this.offset = 0;
88
+ }
89
+ }
90
+ }
91
+ this.length -= n;
92
+ // A slab whose bytes have all been taken is let go, unless it is the one being filled.
93
+ if (this.offset === this.endOf(0) && this.slabs.length > 1) {
94
+ this.slabs.shift();
95
+ this.offset = 0;
96
+ }
97
+ return out;
98
+ }
99
+ }
100
+ exports.ByteQueue = ByteQueue;
101
+ /** How large each slab of storage is. */
102
+ ByteQueue.SLAB = 64 * 1024;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * Clipboard sharing with remote desktops: the ways the clipboard goes (the setting `clipboard`, or a desktop's own),
4
+ * and IronRDP's component set up for them before each connection.
5
+ *
6
+ * While a desktop has the keyboard, the component reads this computer's clipboard every 100 ms and announces what
7
+ * changed to the server, and it writes what the server's clipboard gets to this one; files go through the same
8
+ * channel (CLIPRDR, see fileTransfer.ts). What the component lets the plugin enforce:
9
+ * - Off: the component's clipboard turned off, so IronRDP leaves the clipboard channel out of the connection (it
10
+ * attaches CLIPRDR only with the clipboard callbacks the component hands it). Nothing goes either way, files
11
+ * included, and this computer's clipboard isn't read.
12
+ * - Only from the remote desktop: the reading stopped through the hook the component composes into the file transfer
13
+ * provider (`onUploadStarted`, which it has to keep the reading from overwriting a paste of files), with the
14
+ * matching `onUploadFinished` held back so that nothing starts it again. A server connected this way only ever
15
+ * gets an empty clipboard from this side (one narrowed to it while connected gets that in full from its next
16
+ * connection: see RemoteDesktopService.applyClipboard); the plugin's own sending (paste to all, files) checks the
17
+ * ways as well.
18
+ * The other way only, from this computer to the remote desktop, isn't offered.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.CLIPBOARD_LABELS = void 0;
22
+ exports.ownClipboard = ownClipboard;
23
+ exports.clipboardSetting = clipboardSetting;
24
+ exports.clipboardWays = clipboardWays;
25
+ exports.narrowest = narrowest;
26
+ exports.setUpClipboard = setUpClipboard;
27
+ exports.splitPaste = splitPaste;
28
+ /** The modes, in the order the settings page, the forms and the menus offer them. */
29
+ exports.CLIPBOARD_LABELS = {
30
+ both: 'Both ways',
31
+ fromRemote: 'Only from the remote desktop to this computer',
32
+ off: 'Off',
33
+ };
34
+ /**
35
+ * A desktop's own mode (its entry's or profile's `clipboard`): none when unset or empty, so the setting applies. A
36
+ * value the plugin doesn't know (one written by hand, say) is off: what limits the clipboard must never end up
37
+ * letting more through than it says.
38
+ */
39
+ function ownClipboard(value) {
40
+ if (value === undefined || value === null || value === '') {
41
+ return undefined;
42
+ }
43
+ return value === 'both' || value === 'fromRemote' || value === 'off' ? value : 'off';
44
+ }
45
+ /** The setting's mode: both ways when unset (the default, and how the clipboard always went), else as ownClipboard. */
46
+ function clipboardSetting(value) {
47
+ return ownClipboard(value) ?? 'both';
48
+ }
49
+ /** The ways that every one of the modes lets the clipboard go. */
50
+ function clipboardWays(...modes) {
51
+ return {
52
+ toRemote: modes.every(mode => mode === 'both'),
53
+ fromRemote: modes.every(mode => mode !== 'off'),
54
+ };
55
+ }
56
+ /** The mode that lets the clipboard go only the ways every one of the modes does. */
57
+ function narrowest(...modes) {
58
+ const ways = clipboardWays(...modes);
59
+ return ways.toRemote && ways.fromRemote ? 'both' : ways.fromRemote ? 'fromRemote' : 'off';
60
+ }
61
+ /**
62
+ * Sets up the component's clipboard for `mode`, before connect(): registers the provider (files through the
63
+ * clipboard) and turns off what the mode doesn't allow. `sending` says whether this computer's clipboard may still go
64
+ * to the desktop: while it may, a paste of files ends with the component's reading started again, as usual.
65
+ */
66
+ function setUpClipboard(ui, provider, mode, sending) {
67
+ if (typeof ui.setEnableClipboard !== 'function') {
68
+ throw new Error('this build of IronRDP can\'t turn its clipboard off');
69
+ }
70
+ // Registering the provider is what composes the hooks, and it turns the component's clipboard on: whatever is turned
71
+ // off comes after it.
72
+ const own = provider.onUploadStarted;
73
+ ui.enableFileTransfer(provider);
74
+ const stop = provider.onUploadStarted;
75
+ const resume = provider.onUploadFinished;
76
+ // Without the hooks composed (a component that doesn't), its reading can't be stopped: nothing goes either way.
77
+ const applied = typeof stop === 'function' && typeof resume === 'function' && stop !== own ? mode : 'off';
78
+ provider.onUploadFinished = () => {
79
+ if (sending()) {
80
+ resume?.call(provider);
81
+ }
82
+ };
83
+ const stopSending = () => stop?.call(provider);
84
+ if (applied !== 'both') {
85
+ stopSending();
86
+ }
87
+ if (applied === 'off') {
88
+ ui.setEnableClipboard(false);
89
+ }
90
+ return { mode: applied, stopSending };
91
+ }
92
+ /** Splits a paste to all desktops by the ways the clipboard goes with each one now (`waysOf`). */
93
+ function splitPaste(desktops, waysOf, nameOf) {
94
+ const taking = desktops.map(desktop => waysOf(desktop).toRemote);
95
+ const targets = desktops.filter((_, i) => taking[i]);
96
+ const left = desktops.filter((_, i) => !taking[i]);
97
+ if (!left.length) {
98
+ return { targets, left, note: null };
99
+ }
100
+ const names = left.map(nameOf);
101
+ const which = names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}` : names[0];
102
+ const note = targets.length
103
+ ? `Pasting on ${targets.length} of ${desktops.length} desktops: not on ${which}, which ${left.length > 1 ? 'don\'t' : 'doesn\'t'} take this computer's clipboard.`
104
+ : `Not pasted: ${desktops.length > 1 ? 'these desktops don\'t' : 'this desktop doesn\'t'} take this computer's clipboard.`;
105
+ return { targets, left, note };
106
+ }
@@ -137,4 +137,3 @@ class ConnectionStatus {
137
137
  }
138
138
  }
139
139
  exports.ConnectionStatus = ConnectionStatus;
140
- //# sourceMappingURL=connectionStatus.js.map
package/dist/desk.js CHANGED
@@ -11,78 +11,48 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.DeskTriggerDecorator = void 0;
13
13
  const core_1 = require("@angular/core");
14
- const tabby_core_1 = require("tabby-core");
15
14
  const tabby_terminal_1 = require("tabby-terminal");
16
15
  const desktop_service_1 = require("./desktop.service");
17
16
  const targets_1 = require("./targets");
17
+ const deskScript_1 = require("./deskScript");
18
18
  const probe_1 = require("./probe");
19
- const PREFIX = Buffer.from('\x1b]7777;');
20
- const BEL = 0x07;
21
- const ST = Buffer.from('\x1b\\');
22
- function parse(payload) {
23
- const [session, socket, cwd, kind, machine, hostname] = payload.split(';').map(f => Buffer.from(f ?? '', 'base64').toString('utf8'));
24
- // Older `desk` scripts sent no kind (they only knew tmux), and no machine.
25
- return {
26
- session: session ?? '', socket: socket ?? '', cwd: cwd ?? '', kind: kind ?? (session ? 'tmux' : ''),
27
- machine: machine?.trim() ?? '', hostname: hostname?.trim() ?? '',
28
- };
29
- }
30
19
  /** Strips `desk` requests (and probes, see probe.ts) out of a session's output and reports them. */
31
20
  class DeskTrigger extends tabby_terminal_1.SessionMiddleware {
32
- constructor(onDesk, onProbe) {
21
+ constructor(known, onDesk, onProbe) {
33
22
  super();
34
23
  this.onDesk = onDesk;
35
24
  this.onProbe = onProbe;
25
+ this.requests = new deskScript_1.DeskRequests(known);
36
26
  }
37
27
  feedFromSession(data) {
38
28
  // Tabby's OSC processor runs first and passes unknown OSCs through whole, so each request
39
29
  // arrives complete within one chunk.
40
- let start = data.indexOf(PREFIX);
41
- while (start !== -1) {
42
- const body = start + PREFIX.length;
43
- const bel = data.indexOf(BEL, body);
44
- const st = data.indexOf(ST, body);
45
- const end = bel === -1 ? st : st === -1 ? bel : Math.min(bel, st);
46
- if (end === -1) {
47
- break;
48
- }
49
- const payload = data.subarray(body, end).toString('utf8');
50
- data = Buffer.concat([data.subarray(0, start), data.subarray(end + (end === st ? ST.length : 1))]);
51
- if (payload.startsWith('desk;')) {
52
- const request = parse(payload.slice('desk;'.length));
53
- setTimeout(() => this.onDesk(request));
54
- }
55
- else if (payload.startsWith('probe;')) {
56
- this.onProbe(payload.slice('probe;'.length));
57
- }
58
- start = data.indexOf(PREFIX, start);
30
+ const { output, desk, probes } = this.requests.take(data);
31
+ probes.forEach(id => this.onProbe(id));
32
+ if (desk) {
33
+ setTimeout(() => this.onDesk(desk));
59
34
  }
60
- if (data.length) {
61
- super.feedFromSession(data);
35
+ if (output.length) {
36
+ super.feedFromSession(output);
62
37
  }
63
38
  }
64
39
  }
65
40
  /** Listens for `desk` in every terminal (SSH tabs and local terminals running ssh). */
66
41
  let DeskTriggerDecorator = class DeskTriggerDecorator extends tabby_terminal_1.TerminalDecorator {
67
- constructor(desktop, zone, notifications) {
42
+ constructor(desktop, zone) {
68
43
  super();
69
44
  this.desktop = desktop;
70
45
  this.zone = zone;
71
- this.notifications = notifications;
72
46
  }
73
47
  attach(terminal) {
74
48
  const install = () => {
75
- terminal.session?.middleware.push(new DeskTrigger(request => this.zone.run(() => {
49
+ // Only requests with a key a host gave get this far, one in a while (see DeskRequests); the service checks
50
+ // the rest, the `desk` setting included.
51
+ terminal.session?.middleware.push(new DeskTrigger(key => this.desktop.knowsDeskKey(key), request => this.zone.run(() => {
76
52
  const pane = (0, targets_1.desktopPaneOf)(terminal);
77
- if (!pane) {
78
- return;
79
- }
80
- if (!this.desktop.settings().desk) {
81
- // A hook left from when it was on; it goes away the next time a desktop opens there.
82
- this.notifications.info('`desk` is off: turn it on in the menu under Remote Desktop › Settings');
83
- return;
53
+ if (pane) {
54
+ this.desktop.openConsole(pane, request);
84
55
  }
85
- this.desktop.openConsole(pane, request);
86
56
  }), id => (0, probe_1.probeArrived)(id, terminal)));
87
57
  };
88
58
  install();
@@ -92,6 +62,5 @@ let DeskTriggerDecorator = class DeskTriggerDecorator extends tabby_terminal_1.T
92
62
  exports.DeskTriggerDecorator = DeskTriggerDecorator;
93
63
  exports.DeskTriggerDecorator = DeskTriggerDecorator = __decorate([
94
64
  (0, core_1.Injectable)(),
95
- __metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone, tabby_core_1.NotificationsService])
65
+ __metadata("design:paramtypes", [desktop_service_1.RemoteDesktopService, core_1.NgZone])
96
66
  ], DeskTriggerDecorator);
97
- //# sourceMappingURL=desk.js.map