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
@@ -1,8 +1,171 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MACHINE_ID_COMMAND = void 0;
3
+ exports.MACHINE_ID_COMMAND = exports.DeskGate = exports.DeskRequests = exports.DESK_COOLDOWN_MS = void 0;
4
+ exports.parseDeskRequest = parseDeskRequest;
5
+ exports.deskKeyDigest = deskKeyDigest;
6
+ exports.takeDeskMessages = takeDeskMessages;
4
7
  exports.shq = shq;
5
8
  exports.consoleScript = consoleScript;
9
+ const crypto_1 = require("crypto");
10
+ /** desk's key: 128 random bits in hex, made by the setup (remoteSetup.ts). */
11
+ const DESK_KEY = /^[0-9a-f]{32}$/;
12
+ /** A request's fields, from what follows `desk;`. */
13
+ function parseDeskRequest(payload) {
14
+ const [session, socket, cwd, kind, machine, hostname, key] = payload.split(';').map(f => Buffer.from(f, 'base64').toString('utf8'));
15
+ return {
16
+ session: session ?? '', socket: socket ?? '', cwd: cwd ?? '', kind: kind ?? '',
17
+ machine: machine?.trim() ?? '', hostname: hostname?.trim() ?? '', key: DESK_KEY.test(key ?? '') ? key : '',
18
+ };
19
+ }
20
+ /** What the plugin keeps of a host's desk key: its SHA-256, so that the config never holds the key. '' for no key. */
21
+ function deskKeyDigest(key) {
22
+ return key && DESK_KEY.test(key) ? (0, crypto_1.createHash)('sha256').update(key).digest('hex') : '';
23
+ }
24
+ const PREFIX = Buffer.from('\x1b]7777;');
25
+ const DESK = Buffer.from('desk;');
26
+ const PROBE = Buffer.from('probe;');
27
+ const BEL = 0x07;
28
+ const ESC = 0x1b;
29
+ /** Whether `data` has `prefix` at `at`, before `end`. */
30
+ function startsAt(data, prefix, at, end) {
31
+ return end - at >= prefix.length && data.compare(prefix, 0, prefix.length, at, at + prefix.length) === 0;
32
+ }
33
+ /**
34
+ * Takes the plugin's OSC 7777 messages out of a chunk of terminal output: one `desk` request (output can repeat one any
35
+ * number of times; one is all a chunk gets), the last one that `counts` (its key: see DeskRequests), so that a
36
+ * look-alike after it can't stand in its place; how many others were looked at and didn't count; and probes (see
37
+ * probe.ts).
38
+ */
39
+ function takeDeskMessages(data, counts = () => true) {
40
+ const kept = [];
41
+ const probes = [];
42
+ // Where each desk request's fields are (start, end), read from the last one back once all are out of the output.
43
+ const desks = [];
44
+ let from = 0;
45
+ for (let start = data.indexOf(PREFIX); start !== -1; start = data.indexOf(PREFIX, from)) {
46
+ const body = start + PREFIX.length;
47
+ // The first BEL or ST (ESC \) ends it; one pass, so many messages in a chunk cost no more than its length.
48
+ let end = body;
49
+ while (end < data.length && data[end] !== BEL && !(data[end] === ESC && data[end + 1] === 0x5c)) {
50
+ end++;
51
+ }
52
+ if (end >= data.length) {
53
+ break;
54
+ }
55
+ kept.push(data.subarray(from, start));
56
+ from = end + (data[end] === BEL ? 1 : 2);
57
+ if (startsAt(data, DESK, body, end)) {
58
+ desks.push(body + DESK.length, end);
59
+ }
60
+ else if (startsAt(data, PROBE, body, end)) {
61
+ probes.push(data.subarray(body + PROBE.length, end).toString('utf8'));
62
+ }
63
+ }
64
+ let desk = null;
65
+ let ignored = 0;
66
+ for (let i = desks.length - 2; i >= 0 && !desk; i -= 2) {
67
+ const request = parseDeskRequest(data.subarray(desks[i], desks[i + 1]).toString('utf8'));
68
+ if (counts(request)) {
69
+ desk = request;
70
+ }
71
+ else {
72
+ ignored++;
73
+ }
74
+ }
75
+ const output = from === 0 ? data : Buffer.concat([...kept, data.subarray(from)]);
76
+ return { output, desk, ignored, probes };
77
+ }
78
+ /** How long a pane ignores `desk` requests after handling one. */
79
+ exports.DESK_COOLDOWN_MS = 2000;
80
+ /** How many keys' lookups a chunk of output keeps, to look each of them up once (see DeskRequests.take). */
81
+ const KEYS_KEPT = 16;
82
+ /**
83
+ * The time the cooldowns go by: a clock that only goes forward. The time of day can step back (set by hand, or
84
+ * corrected after sleep), which would keep `desk` quiet for as long as the step.
85
+ */
86
+ const monotonic = () => performance.now();
87
+ /**
88
+ * A terminal's `desk` requests as its output brings them, before any goes further: only one with a key `known` (a key
89
+ * some host's setup gave this plugin, see RemoteDesktopService.knowsDeskKey), and then none for the cooldown. Output
90
+ * can carry any number of look-alikes, in one chunk or in many, and each request let through takes a turn of Tabby's
91
+ * window (and, past the key check there, commands on a host): the others end here, as they are read. Those without a
92
+ * known key get a line in the developer console, at most one per cooldown.
93
+ */
94
+ class DeskRequests {
95
+ constructor(known, cooldownMs = exports.DESK_COOLDOWN_MS, now = monotonic, warn = message => console.warn(message)) {
96
+ this.known = known;
97
+ this.cooldownMs = cooldownMs;
98
+ this.now = now;
99
+ this.warn = warn;
100
+ this.quietUntil = 0;
101
+ this.unknown = 0;
102
+ this.warnAfter = 0;
103
+ }
104
+ /** Takes the plugin's messages out of a chunk of output (see takeDeskMessages): the request to act on, if any. */
105
+ take(data) {
106
+ // A flood repeats a few keys, one or several in turn: each is looked up once per chunk. A few are kept, not all
107
+ // of them (a flood of different ones costs a lookup each whatever is kept, and holds no more memory for it),
108
+ // and the last one besides, so that one repeated past them isn't looked up again each time either.
109
+ const looked = new Map();
110
+ let last = null;
111
+ const counts = (request) => {
112
+ if (last?.key === request.key) {
113
+ return last.known;
114
+ }
115
+ let known = looked.get(request.key);
116
+ if (known === undefined) {
117
+ known = !!request.key && this.known(request.key);
118
+ if (looked.size < KEYS_KEPT) {
119
+ looked.set(request.key, known);
120
+ }
121
+ }
122
+ last = { key: request.key, known };
123
+ return known;
124
+ };
125
+ const { output, desk, ignored, probes } = takeDeskMessages(data, counts);
126
+ const now = this.now();
127
+ this.unknown += ignored;
128
+ if (this.unknown && now >= this.warnAfter) {
129
+ this.warn(`desk: ignored ${this.unknown === 1 ? 'a request' : `${this.unknown} requests`} without a key from a host whose desktop this Tabby set desk up on`);
130
+ this.unknown = 0;
131
+ this.warnAfter = now + this.cooldownMs;
132
+ }
133
+ if (!desk || now < this.quietUntil) {
134
+ return { output, desk: null, probes };
135
+ }
136
+ this.quietUntil = now + this.cooldownMs;
137
+ return { output, desk, probes };
138
+ }
139
+ }
140
+ exports.DeskRequests = DeskRequests;
141
+ /**
142
+ * One `desk` request at a time per pane, and none for a moment after one: output can repeat a request any number of
143
+ * times, and each one costs commands on the host (and a `ps` here, for a local terminal).
144
+ */
145
+ class DeskGate {
146
+ constructor(cooldownMs = exports.DESK_COOLDOWN_MS, now = monotonic) {
147
+ this.cooldownMs = cooldownMs;
148
+ this.now = now;
149
+ this.busy = false;
150
+ this.until = 0;
151
+ }
152
+ /** Runs `task`, unless one runs or the last one ended less than the cooldown ago (false: dropped). */
153
+ async run(task) {
154
+ if (this.busy || this.now() < this.until) {
155
+ return false;
156
+ }
157
+ this.busy = true;
158
+ try {
159
+ await task();
160
+ }
161
+ finally {
162
+ this.busy = false;
163
+ this.until = this.now() + this.cooldownMs;
164
+ }
165
+ return true;
166
+ }
167
+ }
168
+ exports.DeskGate = DeskGate;
6
169
  /** Remote command printing what `desk` sends as `machine`. */
7
170
  exports.MACHINE_ID_COMMAND = 'cat /etc/machine-id 2>/dev/null || hostname';
8
171
  /** Single-quotes a value for sh. */
@@ -10,8 +173,10 @@ function shq(value) {
10
173
  return `'${value.replace(/'/g, `'\\''`)}'`;
11
174
  }
12
175
  /**
13
- * Remote script: open a maximized terminal on the headless desktop, attached to the given tmux
14
- * session (unless something already is), or in the given folder. Prints `RD_OK …` or `RD_ERR …`.
176
+ * Remote script: open a maximized terminal on the headless desktop, attached to the given shared session (unless
177
+ * something already is), or in the given folder. Only sessions of this user's own: a trd-pty session by its id (trd-pty
178
+ * looks it up in its private folder and checks who serves it), or a tmux server whose socket this user owns, in tmux's
179
+ * folder for them. Prints `RD_OK …` or `RD_ERR …`.
15
180
  */
16
181
  function consoleScript(request) {
17
182
  return `SESS=${shq(request.session)}
@@ -23,14 +188,40 @@ command -v gnome-terminal >/dev/null 2>&1 || { echo "RD_ERR gnome-terminal is no
23
188
  W=$(systemctl --user show-environment 2>/dev/null | sed -n 's/^WAYLAND_DISPLAY=//p')
24
189
  export WAYLAND_DISPLAY=${"$"}{W:-wayland-0}
25
190
  TRD="$HOME/.local/share/tabby-rdp/bin/trd-pty"
26
- if [ "$KIND" = native ] && [ -n "$SESS" ] && [ -x "$TRD" ] && [ "$("$TRD" clients "$SESS")" -ge 1 ]; then
191
+ # A tmux client attaches to whatever server answers on the socket it is given, and another user can let anyone in to
192
+ # theirs. So only a socket of this user's, in tmux's folder for them (theirs, no access for others, as tmux makes it),
193
+ # with no symbolic link on the way, and every folder above it root's or this user's, where others can't rename or
194
+ # replace anything (or only their own entries: sticky, like /tmp): nobody else can swap the path out before the attach,
195
+ # later, finds the socket again. (ls -n's first and third fields, its mode and owner, are the portable way to read them.)
196
+ own_tmux () {
197
+ [ -S "$1" ] && [ ! -L "$1" ] && [ -O "$1" ] && [ "$(readlink -f "$1" 2>/dev/null)" = "$1" ] || return 1
198
+ ot_uid=$(id -u)
199
+ ot_dir=${"$"}{1%/*}
200
+ [ "${"$"}{ot_dir##*/}" = "tmux-$ot_uid" ] && [ -O "$ot_dir" ] || return 1
201
+ case $(ls -ld "$ot_dir") in d???------*) ;; *) return 1 ;; esac
202
+ ot_up=${"$"}{ot_dir%/*}
203
+ while :; do
204
+ set -- $(ls -ldn "$ot_up/")
205
+ [ "$3" = "$ot_uid" ] || [ "$3" = 0 ] || return 1
206
+ case $1 in d????[!w]??[!w]*|d????????[tT]*) ;; *) return 1 ;; esac
207
+ case $ot_up in '') return 0 ;; /*) ot_up=${"$"}{ot_up%/*} ;; *) return 1 ;; esac
208
+ done
209
+ }
210
+ # Session names as trd-pty and tmux make them (tmux never puts : or . in one); anything else opens a terminal in the
211
+ # folder instead. A tmux name is otherwise any: it only picks among the sessions of a server of this user's own.
212
+ case $KIND in
213
+ native) case $SESS in ''|*[!0-9a-f]*) KIND= ;; esac ;;
214
+ tmux) case $SESS in ''|*[:.]*) KIND= ;; esac ;;
215
+ *) KIND= ;;
216
+ esac
217
+ if [ "$KIND" = native ] && [ -x "$TRD" ] && [ "$("$TRD" clients "$SESS")" -ge 1 ]; then
27
218
  # The console itself is one attached terminal; a second one means a desktop terminal is already attached.
28
219
  if [ "$("$TRD" clients "$SESS")" -ge 2 ]; then
29
220
  echo "RD_OK attached"; exit 0
30
221
  fi
31
222
  gnome-terminal --maximize -- "$TRD" attach "$SESS" >/dev/null 2>&1 \
32
223
  && echo "RD_OK opened" || echo "RD_ERR could not open a terminal"
33
- elif [ -n "$SESS" ] && [ -S "$SOCK" ] && tmux -S "$SOCK" has-session -t "=$SESS" 2>/dev/null; then
224
+ elif [ "$KIND" = tmux ] && own_tmux "$SOCK" && tmux -S "$SOCK" has-session -t "=$SESS" 2>/dev/null; then
34
225
  # The console itself is one client; a second one means a desktop terminal is already attached.
35
226
  if [ "$(tmux -S "$SOCK" list-clients -t "=$SESS" 2>/dev/null | wc -l)" -ge 2 ]; then
36
227
  echo "RD_OK attached"; exit 0
@@ -44,4 +235,3 @@ else
44
235
  fi
45
236
  `;
46
237
  }
47
- //# sourceMappingURL=deskScript.js.map