tabby-rdp 0.1.0

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.
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startRDCleanPathProxy = startRDCleanPathProxy;
4
+ // RDCleanPath proxy for the IronRDP web client, running inside Tabby's renderer.
5
+ // The WASM client talks WebSocket to this proxy; the proxy opens the upstream stream
6
+ // (a plain socket here, an SSH direct-tcpip channel later), forwards the X.224 request,
7
+ // terminates TLS, hands the server cert chain back to the client (for CredSSP), then relays.
8
+ const tls = require("tls");
9
+ const crypto_1 = require("crypto");
10
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
11
+ const { WebSocketServer } = require('ws');
12
+ const VERSION_1 = 3390;
13
+ const GENERAL_ERROR = 1;
14
+ // ---- minimal DER -------------------------------------------------------------------------------
15
+ function derLength(n) {
16
+ if (n < 0x80) {
17
+ return Buffer.from([n]);
18
+ }
19
+ const bytes = [];
20
+ for (let v = n; v > 0; v = Math.floor(v / 256)) {
21
+ bytes.unshift(v & 0xff);
22
+ }
23
+ return Buffer.from([0x80 | bytes.length, ...bytes]);
24
+ }
25
+ function tlv(tag, content) {
26
+ return Buffer.concat([Buffer.from([tag]), derLength(content.length), content]);
27
+ }
28
+ function derInteger(n) {
29
+ let hex = n.toString(16);
30
+ if (hex.length % 2) {
31
+ hex = '0' + hex;
32
+ }
33
+ let b = Buffer.from(hex, 'hex');
34
+ if (b[0] & 0x80) {
35
+ b = Buffer.concat([Buffer.from([0]), b]);
36
+ }
37
+ return tlv(0x02, b);
38
+ }
39
+ const seq = (...items) => tlv(0x30, Buffer.concat(items));
40
+ const explicit = (n, inner) => tlv(0xa0 | n, inner);
41
+ const octets = (b) => tlv(0x04, b);
42
+ const utf8 = (s) => tlv(0x0c, Buffer.from(s, 'utf8'));
43
+ function readTlv(buf, off) {
44
+ if (buf.length < off + 2) {
45
+ return null;
46
+ }
47
+ let len = buf[off + 1];
48
+ let p = off + 2;
49
+ if (len & 0x80) {
50
+ const k = len & 0x7f;
51
+ if (buf.length < p + k) {
52
+ return null;
53
+ }
54
+ len = 0;
55
+ for (let i = 0; i < k; i++) {
56
+ len = len * 256 + buf[p++];
57
+ }
58
+ }
59
+ return { tag: buf[off], start: p, end: p + len };
60
+ }
61
+ function decodeRequest(buf) {
62
+ const outer = readTlv(buf, 0);
63
+ if (!outer || outer.tag !== 0x30) {
64
+ throw new Error('RDCleanPath: not a SEQUENCE');
65
+ }
66
+ const req = {};
67
+ let p = outer.start;
68
+ while (p < outer.end) {
69
+ const field = readTlv(buf, p);
70
+ const inner = readTlv(buf, field.start);
71
+ const value = buf.subarray(inner.start, inner.end);
72
+ switch (field.tag & 0x1f) {
73
+ case 0:
74
+ req.version = value.readUIntBE(0, value.length);
75
+ break;
76
+ case 2:
77
+ req.destination = value.toString('utf8');
78
+ break;
79
+ case 3:
80
+ req.proxyAuth = value.toString('utf8');
81
+ break;
82
+ case 6:
83
+ req.x224 = Buffer.from(value);
84
+ break;
85
+ }
86
+ p = field.end;
87
+ }
88
+ return req;
89
+ }
90
+ function encodeResponse(x224, certChain, serverAddr) {
91
+ return seq(explicit(0, derInteger(VERSION_1)), explicit(6, octets(x224)), explicit(7, seq(...certChain.map(octets))), explicit(9, utf8(serverAddr)));
92
+ }
93
+ function encodeError() {
94
+ return seq(explicit(0, derInteger(VERSION_1)), explicit(1, seq(explicit(0, derInteger(GENERAL_ERROR)))));
95
+ }
96
+ // ---- upstream handshake ------------------------------------------------------------------------
97
+ // Reads exactly one TPKT-framed PDU (the X.224 Connection Confirm) without consuming TLS bytes.
98
+ function readTpkt(stream) {
99
+ return new Promise((resolve, reject) => {
100
+ let acc = Buffer.alloc(0);
101
+ const cleanup = () => {
102
+ stream.off('data', onData);
103
+ stream.off('error', onError);
104
+ stream.off('close', onClose);
105
+ };
106
+ const onError = (e) => { cleanup(); reject(e); };
107
+ const onClose = () => { cleanup(); reject(new Error('upstream closed during X.224')); };
108
+ const onData = (chunk) => {
109
+ acc = Buffer.concat([acc, chunk]);
110
+ if (acc.length < 4) {
111
+ return;
112
+ }
113
+ const len = acc.readUInt16BE(2);
114
+ if (acc.length < len) {
115
+ return;
116
+ }
117
+ stream.pause();
118
+ cleanup();
119
+ if (acc.length > len) {
120
+ stream.unshift(acc.subarray(len));
121
+ }
122
+ resolve(acc.subarray(0, len));
123
+ };
124
+ stream.on('data', onData);
125
+ stream.on('error', onError);
126
+ stream.on('close', onClose);
127
+ });
128
+ }
129
+ function certChainOf(socket) {
130
+ const chain = [];
131
+ let cert = socket.getPeerCertificate(true);
132
+ const seen = new Set();
133
+ while (cert?.raw && !seen.has(cert.fingerprint256)) {
134
+ seen.add(cert.fingerprint256);
135
+ chain.push(cert.raw);
136
+ cert = cert.issuerCertificate;
137
+ }
138
+ return chain;
139
+ }
140
+ // ---- proxy -------------------------------------------------------------------------------------
141
+ /**
142
+ * TLS for servers whose certificate only allows key encipherment, like Windows' self-signed RDP certificate.
143
+ * BoringSSL (Electron) enforces the certificate's key usage, and every ECDHE or TLS 1.3 handshake needs the
144
+ * digital-signature bit, so only TLS 1.2 with RSA key exchange works with such a certificate (other RDP
145
+ * clients use OpenSSL, which doesn't enforce it).
146
+ */
147
+ const KEY_ENCIPHERMENT_ONLY_TLS = {
148
+ maxVersion: 'TLSv1.2',
149
+ ciphers: 'AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256',
150
+ };
151
+ /** Sends the client's X.224 request upstream, reads the confirm, then does TLS there. */
152
+ async function handshake(openUpstream, destination, x224Request, legacyTls) {
153
+ const raw = await openUpstream(destination);
154
+ try {
155
+ raw.write(x224Request);
156
+ const x224 = await readTpkt(raw);
157
+ const upstream = tls.connect({ socket: raw, rejectUnauthorized: false, ...legacyTls ? KEY_ENCIPHERMENT_ONLY_TLS : {} });
158
+ await new Promise((resolve, reject) => {
159
+ upstream.once('secureConnect', resolve);
160
+ upstream.once('error', reject);
161
+ });
162
+ return { raw, upstream, x224 };
163
+ }
164
+ catch (e) {
165
+ raw.destroy();
166
+ throw e;
167
+ }
168
+ }
169
+ async function startRDCleanPathProxy(openUpstream, log = () => { }) {
170
+ const token = (0, crypto_1.randomBytes)(24).toString('hex');
171
+ // Set once the server turned out to need KEY_ENCIPHERMENT_ONLY_TLS; later connections start with it.
172
+ let legacyTls = false;
173
+ const wss = new WebSocketServer({ host: '127.0.0.1', port: 0 });
174
+ await new Promise((resolve, reject) => {
175
+ wss.once('listening', resolve);
176
+ wss.once('error', reject);
177
+ });
178
+ wss.on('connection', (ws) => {
179
+ let buf = Buffer.alloc(0);
180
+ let stage = 'hello';
181
+ const early = [];
182
+ let upstream = null;
183
+ let raw = null;
184
+ const fail = (why) => {
185
+ log(`RDCleanPath failed: ${why}`);
186
+ try {
187
+ ws.send(encodeError());
188
+ }
189
+ catch { }
190
+ ws.close();
191
+ upstream?.destroy();
192
+ raw?.destroy();
193
+ };
194
+ ws.on('message', async (data) => {
195
+ if (stage === 'relay') {
196
+ upstream.write(data);
197
+ return;
198
+ }
199
+ if (stage === 'handshake') {
200
+ early.push(data);
201
+ return;
202
+ }
203
+ buf = Buffer.concat([buf, data]);
204
+ const outer = readTlv(buf, 0);
205
+ if (!outer || buf.length < outer.end) {
206
+ return;
207
+ }
208
+ stage = 'handshake';
209
+ try {
210
+ const req = decodeRequest(buf.subarray(0, outer.end));
211
+ if (req.version !== VERSION_1 || !req.x224 || !req.destination) {
212
+ return fail('malformed request');
213
+ }
214
+ if (req.proxyAuth !== token) {
215
+ return fail('bad token');
216
+ }
217
+ let up;
218
+ try {
219
+ up = await handshake(openUpstream, req.destination, req.x224, legacyTls);
220
+ }
221
+ catch (e) {
222
+ if (legacyTls || !/KEY_USAGE_BIT_INCORRECT/.test(e?.message ?? '')) {
223
+ throw e;
224
+ }
225
+ // The X.224 exchange is spent on that connection: start over on a fresh one.
226
+ log('TLS: the server certificate only allows key encipherment; using TLS 1.2 with RSA key exchange');
227
+ legacyTls = true;
228
+ up = await handshake(openUpstream, req.destination, req.x224, legacyTls);
229
+ }
230
+ raw = up.raw;
231
+ upstream = up.upstream;
232
+ const x224 = up.x224;
233
+ ws.send(encodeResponse(x224, certChainOf(upstream), req.destination));
234
+ stage = 'relay';
235
+ upstream.on('data', (d) => ws.send(d));
236
+ upstream.on('close', () => ws.close());
237
+ upstream.on('error', (e) => { log(`upstream error: ${e.message}`); ws.close(); });
238
+ for (const d of early) {
239
+ upstream.write(d);
240
+ }
241
+ log(`RDCleanPath relay up to ${req.destination}`);
242
+ }
243
+ catch (e) {
244
+ fail(e?.message ?? String(e));
245
+ }
246
+ });
247
+ ws.on('close', () => {
248
+ upstream?.destroy();
249
+ raw?.destroy();
250
+ });
251
+ });
252
+ const { port } = wss.address();
253
+ return { url: `ws://127.0.0.1:${port}`, token, close: () => wss.close() };
254
+ }
255
+ //# sourceMappingURL=rdcleanpath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rdcleanpath.js","sourceRoot":"","sources":["../src/rdcleanpath.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareRemoteDesktop = prepareRemoteDesktop;
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ // Runs as the SSH user; needs no root. Prints exactly one `RD_OK port=N user=U pass=P` or `RD_ERR <reason>` line.
7
+ // The RDP password is generated once per remote user and kept in a 0600 file; grd reads credentials only
8
+ // at startup, so grd is restarted only when its configuration actually changes (that drops live sessions).
9
+ // (grdctl takes the credentials as arguments, so they are briefly visible in the remote's process list.)
10
+ const SETUP_SCRIPT = String.raw `
11
+ say () { printf '%s\n' "$*"; }
12
+ fail () { say "RD_ERR $*"; exit 0; }
13
+ command -v grdctl >/dev/null 2>&1 || fail "GNOME Remote Desktop (grdctl) is not installed"
14
+ command -v gnome-shell >/dev/null 2>&1 || fail "gnome-shell is not installed"
15
+ if systemctl --user is-active -q gnome-remote-desktop.service; then
16
+ fail "a desktop-session GNOME Remote Desktop is already running for this user"
17
+ fi
18
+ D="$HOME/.local/share/tabby-remote-desktop"
19
+ mkdir -p "$D" && chmod 700 "$D" || fail "could not create $D"
20
+ if [ ! -s "$D/tls.key" ]; then
21
+ openssl req -new -newkey rsa:3072 -days 3650 -nodes -x509 -subj "/CN=$(hostname)" \
22
+ -keyout "$D/tls.key" -out "$D/tls.crt" >/dev/null 2>&1 || fail "could not create a TLS certificate"
23
+ fi
24
+ # 'desk' support (setting, off by default): interactive SSH logins start inside a shareable session (trd-pty,
25
+ # or tmux), so 'desk' can attach this exact console to a terminal on the desktop. Rewritten on every connect;
26
+ # opt out per login with TABBY_NO_SESSION=1. When the setting is off, the hook and helpers are removed.
27
+ if [ "${"$"}{TRD_DESK:-0}" = 1 ]; then
28
+ mkdir -p "$D/bin"
29
+ cat > "$D/login.sh" <<'TRD_EOF'
30
+ # tabby-rdp ('desk' setting): interactive SSH logins run inside a shareable session, so 'desk' in Tabby can
31
+ # bring this console to the remote desktop. Backend ("native" trd-pty or "tmux") is in $TRD_HOME/backend.
32
+ # Set TABBY_NO_SESSION=1 (or TABBY_NO_TMUX=1) to skip; turning 'desk' off in Tabby removes this hook.
33
+ TRD_HOME="$HOME/.local/share/tabby-remote-desktop"
34
+ case ":$PATH:" in *":$TRD_HOME/bin:"*) ;; *) PATH="$TRD_HOME/bin:$PATH"; export PATH ;; esac
35
+ case $- in *i*) ;; *) return 0 ;; esac
36
+ if [ -n "$SSH_TTY" ] && [ -z "$TMUX" ] && [ -z "$TRD_SESSION" ] && [ -z "$TABBY_NO_TMUX$TABBY_NO_SESSION" ]; then
37
+ TRD_BACKEND=$(cat "$TRD_HOME/backend" 2>/dev/null)
38
+ if [ "$TRD_BACKEND" = native ] && command -v python3 >/dev/null 2>&1; then
39
+ "$TRD_HOME/bin/trd-pty" new
40
+ case $? in 0) exit ;; 3) TRD_BACKEND=tmux ;; esac # 3: no session could be started
41
+ fi
42
+ if [ "$TRD_BACKEND" != native ] && command -v tmux >/dev/null 2>&1; then
43
+ tmux -L tabby -f "$TRD_HOME/tmux.conf" new-session && exit
44
+ fi
45
+ fi
46
+ TRD_EOF
47
+ cat > "$D/tmux.conf" <<'TRD_EOF'
48
+ # tmux server for SSH logins (tabby-rdp 'desk'). Your ~/.tmux.conf is applied on top.
49
+ set -g status off
50
+ # Like a plain SSH login: the session ends when its last client (console or desktop terminal) goes away.
51
+ set -g destroy-unattached on
52
+ # Console and desktop terminal can differ in size: follow whichever was used last. Focus reports make
53
+ # switching back (Tabby focuses the console) count as use, so the window resizes right away.
54
+ set -g window-size latest
55
+ set -g focus-events on
56
+ # Lets 'desk' reach Tabby from inside tmux.
57
+ set -g allow-passthrough on
58
+ # Keep the terminal's own scrollback (no alternate screen).
59
+ set -ga terminal-overrides ',*:smcup@:rmcup@'
60
+ source-file -q ~/.tmux.conf
61
+ TRD_EOF
62
+ cat > "$D/bin/desk" <<'TRD_EOF'
63
+ #!/bin/sh
64
+ # desk: open this machine's desktop in Tabby, with this console (its shared session) waiting in a terminal there.
65
+ b64 () { printf %s "$1" | base64 | tr -d '\n'; }
66
+ KIND= SESSION= SOCKET=
67
+ if [ -n "$TRD_SESSION" ]; then
68
+ KIND=native SESSION=$TRD_SESSION SOCKET=$TRD_SOCKET
69
+ elif [ -n "$TMUX" ]; then
70
+ KIND=tmux
71
+ SESSION=$(tmux display-message -p '#{session_name}' 2>/dev/null)
72
+ SOCKET=$(tmux display-message -p '#{socket_path}' 2>/dev/null)
73
+ tmux set-option -p allow-passthrough on 2>/dev/null
74
+ fi
75
+ SEQ=$(printf '\033]7777;desk;%s;%s;%s;%s\007' "$(b64 "$SESSION")" "$(b64 "$SOCKET")" "$(b64 "$PWD")" "$(b64 "$KIND")")
76
+ if [ "$KIND" = tmux ]; then
77
+ printf '\033Ptmux;\033%s\033\\' "$SEQ" # tmux only lets it through as passthrough
78
+ else
79
+ printf '%s' "$SEQ" # trd-pty passes output through byte for byte
80
+ fi
81
+ TRD_EOF
82
+ chmod 755 "$D/bin/desk"
83
+ cat > "$D/bin/trd-pty" <<'TRD_PY_EOF'
84
+ @@TRD_PTY_PY@@
85
+ TRD_PY_EOF
86
+ chmod 755 "$D/bin/trd-pty"
87
+ printf '%s\n' "${"$"}{TRD_BACKEND:-native}" > "$D/backend"
88
+ for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
89
+ [ -f "$rc" ] || continue
90
+ grep -qF '# tabby-remote-desktop' "$rc" && continue
91
+ # Only one line: turning desk off deletes exactly it, so on/off cycles leave the file as it was.
92
+ [ -z "$(tail -c1 "$rc")" ] || echo >> "$rc"
93
+ printf '[ -f "$HOME/.local/share/tabby-remote-desktop/login.sh" ] && . "$HOME/.local/share/tabby-remote-desktop/login.sh" # tabby-remote-desktop\n' >> "$rc"
94
+ done
95
+ else
96
+ for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
97
+ if [ -f "$rc" ] && grep -qF '# tabby-remote-desktop' "$rc"; then
98
+ sed -i.trd-bak '/# tabby-remote-desktop$/d' "$rc" && rm -f "$rc.trd-bak"
99
+ fi
100
+ done
101
+ # Sessions already running keep going; new logins are plain again.
102
+ rm -f "$D/login.sh" "$D/tmux.conf" "$D/backend" "$D/bin/desk" "$D/bin/trd-pty"
103
+ fi
104
+ if [ ! -s "$D/rdp-password" ]; then
105
+ ( umask 077; od -An -N18 -tx1 /dev/urandom | tr -d ' \n' > "$D/rdp-password" ) || fail "could not create credentials"
106
+ fi
107
+ RD_USER=tabby
108
+ RD_PASS=$(cat "$D/rdp-password")
109
+
110
+ STATUS=$(grdctl --headless status --show-credentials 2>/dev/null)
111
+ has () { printf '%s\n' "$STATUS" | grep -qxF "$(printf '\t%s' "$1")"; }
112
+ g () { grdctl --headless rdp "$@" >/dev/null 2>&1; }
113
+ RESTART=0
114
+ if ! has "TLS key: $D/tls.key" || ! has "TLS certificate: $D/tls.crt"; then
115
+ g set-tls-key "$D/tls.key" && g set-tls-cert "$D/tls.crt" || fail "grdctl could not set the TLS certificate"
116
+ RESTART=1
117
+ fi
118
+ if ! has "View-only: no"; then g disable-view-only || fail "grdctl could not disable view-only"; RESTART=1; fi
119
+ if ! has "Status: enabled"; then g enable || fail "grdctl could not enable RDP"; RESTART=1; fi
120
+ if ! has "Username: $RD_USER" || ! has "Password: $RD_PASS"; then
121
+ g set-credentials "$RD_USER" "$RD_PASS" || fail "grdctl could not set credentials"
122
+ RESTART=1
123
+ fi
124
+
125
+ # Session identity for the headless shell: apps like Settings refuse to run without XDG_CURRENT_DESKTOP,
126
+ # and xdg-open picks the desktop's handlers from it. Use Ubuntu's session mode when present (dock etc.).
127
+ if [ -f /usr/share/gnome-shell/modes/ubuntu.json ]; then SESS=ubuntu; DESK=ubuntu:GNOME; else SESS=gnome; DESK=GNOME; fi
128
+ SESSION_VERSION=2
129
+ SHELL_PID=$(pgrep -u "$(id -u)" -f 'gnome-shell --headless' | head -1)
130
+ if [ -n "$SHELL_PID" ] && ! tr '\0' '\n' < /proc/$SHELL_PID/environ 2>/dev/null | grep -qx "TABBY_RD_SESSION=$SESSION_VERSION"; then
131
+ # A shell from an older setup (no session identity): replace it (closes its windows).
132
+ kill "$SHELL_PID" 2>/dev/null
133
+ i=0; while [ $i -lt 40 ] && kill -0 "$SHELL_PID" 2>/dev/null; do sleep 0.25; i=$((i+1)); done
134
+ SHELL_PID=
135
+ fi
136
+ if [ -z "$SHELL_PID" ]; then
137
+ # Apps from a previous headless session (marked below) outlive their display; single-instance apps
138
+ # would then swallow new launches without showing a window. End them before starting afresh.
139
+ for p in $(pgrep -u "$(id -u)"); do
140
+ grep -qz '^TABBY_RD_SESSION=' /proc/$p/environ 2>/dev/null && kill "$p" 2>/dev/null
141
+ done
142
+ systemctl --user stop tabby-headless-shell >/dev/null 2>&1
143
+ systemctl --user reset-failed tabby-headless-shell >/dev/null 2>&1
144
+ systemd-run --user --unit=tabby-headless-shell --setenv=XDG_SESSION_TYPE=wayland \
145
+ --setenv=XDG_CURRENT_DESKTOP=$DESK --setenv=XDG_SESSION_DESKTOP=$SESS --setenv=DESKTOP_SESSION=$SESS \
146
+ --setenv=GNOME_SHELL_SESSION_MODE=$SESS --setenv=TABBY_RD_SESSION=$SESSION_VERSION \
147
+ gnome-shell --headless >/dev/null 2>&1 \
148
+ || fail "could not start a headless GNOME Shell"
149
+ i=0; while [ $i -lt 40 ] && ! busctl --user status org.gnome.Mutter.RemoteDesktop >/dev/null 2>&1; do sleep 0.25; i=$((i+1)); done
150
+ SHELL_PID=$(pgrep -u "$(id -u)" -f 'gnome-shell --headless' | head -1)
151
+ RESTART=1
152
+ fi
153
+ # What gnome-session does for a real login: let D-Bus/systemd-activated apps find this display.
154
+ WL=$(tr '\0' '\n' < /proc/$SHELL_PID/environ 2>/dev/null | sed -n 's/^WAYLAND_DISPLAY=//p')
155
+ [ -n "$WL" ] || WL=$(ls -t "$XDG_RUNTIME_DIR" 2>/dev/null | grep -m1 -x 'wayland-[0-9]*')
156
+ dbus-update-activation-environment --systemd WAYLAND_DISPLAY=${"$"}{WL:-wayland-0} XDG_SESSION_TYPE=wayland \
157
+ XDG_CURRENT_DESKTOP=$DESK XDG_SESSION_DESKTOP=$SESS DESKTOP_SESSION=$SESS >/dev/null 2>&1
158
+ # Mark D-Bus-activated apps as part of this session too (not in systemd's environment: user services would inherit it).
159
+ dbus-update-activation-environment TABBY_RD_SESSION=$SESSION_VERSION >/dev/null 2>&1
160
+ if [ $RESTART = 1 ] || ! systemctl --user is-active -q gnome-remote-desktop-headless.service; then
161
+ systemctl --user restart gnome-remote-desktop-headless.service >/dev/null 2>&1 || fail "could not start gnome-remote-desktop-headless"
162
+ fi
163
+ PORT=$(grdctl --headless status 2>/dev/null | awk '/Port:/ { print $2; exit }')
164
+ PORT=${"$"}{PORT:-3389}
165
+ i=0; while [ $i -lt 40 ] && ! ss -ltnH "sport = :$PORT" 2>/dev/null | grep -q .; do sleep 0.25; i=$((i+1)); done
166
+ ss -ltnH "sport = :$PORT" 2>/dev/null | grep -q . || fail "nothing is listening on port $PORT"
167
+ say "RD_OK port=$PORT user=$RD_USER pass=$RD_PASS"
168
+ `;
169
+ let trdPtySource = null;
170
+ function trdPty() {
171
+ trdPtySource ??= fs.readFileSync(path.join(__dirname, '..', 'remote', 'trd-pty.py'), 'utf8');
172
+ return trdPtySource;
173
+ }
174
+ async function prepareRemoteDesktop(target, desk, backend = 'native') {
175
+ const script = `TRD_DESK=${desk ? 1 : 0}\nTRD_BACKEND=${backend === 'tmux' ? 'tmux' : 'native'}\n` +
176
+ SETUP_SCRIPT.replace('@@TRD_PTY_PY@@', trdPty().trimEnd());
177
+ const out = await target.exec('sh -s', script);
178
+ const err = /^RD_ERR (.*)$/m.exec(out);
179
+ if (err) {
180
+ throw new Error(err[1]);
181
+ }
182
+ const ok = /^RD_OK port=(\d+) user=(\S+) pass=(\S+)$/m.exec(out);
183
+ if (!ok) {
184
+ throw new Error('Remote setup gave no result');
185
+ }
186
+ return { port: Number(ok[1]), username: ok[2], password: ok[3] };
187
+ }
188
+ //# sourceMappingURL=remoteSetup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteSetup.js","sourceRoot":"","sources":["../src/remoteSetup.ts"],"names":[],"mappings":";;AAwLA,oDAaC;AArMD,yBAAwB;AACxB,6BAA4B;AAS5B,kHAAkH;AAClH,yGAAyG;AACzG,2GAA2G;AAC3G,yGAAyG;AACzG,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;QAiBvB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4DM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+DAqE2C,GAAG;;;;;;;;OAQ3D,GAAG;;;;CAIT,CAAA;AAMD,IAAI,YAAY,GAAkB,IAAI,CAAA;AACtC,SAAS,MAAM;IACX,YAAY,KAAK,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAA;IAC5F,OAAO,YAAY,CAAA;AACvB,CAAC;AAEM,KAAK,UAAU,oBAAoB,CAAE,MAAoB,EAAE,IAAa,EAAE,UAA0B,QAAQ;IAC/G,MAAM,MAAM,GAAG,YAAY,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI;QAC9F,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;IAC9D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC9C,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,GAAG,EAAE,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3B,CAAC;IACD,MAAM,EAAE,GAAG,2CAA2C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAChE,IAAI,CAAC,EAAE,EAAE,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAA;IAClD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AACpE,CAAC"}
package/dist/signin.js ADDED
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /** Sign-in for desktops that use their own accounts (Windows): a form in the desktop layer, and the keychain. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.STYLE = void 0;
5
+ exports.loadCredentials = loadCredentials;
6
+ exports.saveCredentials = saveCredentials;
7
+ exports.forgetCredentials = forgetCredentials;
8
+ exports.forgetCredentialsFor = forgetCredentialsFor;
9
+ exports.askCredentials = askCredentials;
10
+ const KEYCHAIN_SERVICE = 'tabby-rdp';
11
+ // Tabby ships keytar (tabby-ssh keeps SSH passwords with it). Without it, nothing is remembered.
12
+ function keytar() {
13
+ try {
14
+ return require('keytar');
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /** Saved credentials for a desktop (keyed by its session key), or null. */
21
+ async function loadCredentials(key) {
22
+ try {
23
+ const saved = await keytar()?.getPassword(KEYCHAIN_SERVICE, key);
24
+ const parsed = saved ? JSON.parse(saved) : null;
25
+ return parsed?.username && typeof parsed.password === 'string' ? { username: parsed.username, password: parsed.password } : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ async function saveCredentials(key, credentials) {
32
+ await keytar()?.setPassword(KEYCHAIN_SERVICE, key, JSON.stringify(credentials));
33
+ }
34
+ async function forgetCredentials(key) {
35
+ try {
36
+ await keytar()?.deletePassword(KEYCHAIN_SERVICE, key);
37
+ }
38
+ catch { }
39
+ }
40
+ /** Forgets saved accounts for a desktop on any SSH host (session keys ending in `#<id>`). */
41
+ async function forgetCredentialsFor(desktopId) {
42
+ try {
43
+ const k = keytar();
44
+ for (const { account } of await k?.findCredentials(KEYCHAIN_SERVICE) ?? []) {
45
+ if (account.endsWith(`#${desktopId}`)) {
46
+ await k.deletePassword(KEYCHAIN_SERVICE, account);
47
+ }
48
+ }
49
+ }
50
+ catch { }
51
+ }
52
+ exports.STYLE = `
53
+ .trd-signin { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; z-index: 1; }
54
+ .trd-signin form { display: flex; flex-direction: column; gap: 10px; width: 300px; max-width: calc(100% - 32px); color: #ccc; font-size: 13px; }
55
+ .trd-signin .trd-signin-title { font-size: 15px; color: #eee; }
56
+ .trd-signin .trd-signin-error { color: #f08080; white-space: pre-line; }
57
+ .trd-signin .trd-signin-error:empty { display: none; }
58
+ .trd-signin label.trd-signin-remember { display: flex; gap: 6px; align-items: center; }
59
+ .trd-signin .trd-signin-buttons { display: flex; gap: 8px; justify-content: flex-end; }
60
+ `;
61
+ /**
62
+ * Asks for the account in the desktop layer. Resolves with what was entered (and whether to remember it),
63
+ * or null when cancelled or when `abort` fires (the desktop was disconnected meanwhile).
64
+ */
65
+ function askCredentials(layer, options, abort) {
66
+ const box = document.createElement('div');
67
+ box.className = 'trd-signin';
68
+ box.innerHTML = `
69
+ <form autocomplete="off">
70
+ <div class="trd-signin-title"></div>
71
+ <div class="trd-signin-error"></div>
72
+ <input class="form-control" name="username" placeholder="User name (or DOMAIN\\user)" spellcheck="false">
73
+ <input class="form-control" name="password" type="password" placeholder="Password">
74
+ <label class="trd-signin-remember"><input type="checkbox" name="remember" checked> Remember in the keychain</label>
75
+ <div class="trd-signin-buttons">
76
+ <button type="button" class="btn btn-secondary" name="cancel">Cancel</button>
77
+ <button type="submit" class="btn btn-primary">Connect</button>
78
+ </div>
79
+ </form>`;
80
+ const form = box.querySelector('form');
81
+ const field = (name) => form.querySelector(`[name="${name}"]`);
82
+ box.querySelector('.trd-signin-title').textContent = options.title;
83
+ box.querySelector('.trd-signin-error').textContent = options.error ?? '';
84
+ field('username').value = options.username ?? '';
85
+ if (!options.canRemember) {
86
+ field('remember').checked = false;
87
+ form.querySelector('.trd-signin-remember').style.display = 'none';
88
+ }
89
+ layer.appendChild(box);
90
+ // Focus goes to the form from DesktopSession.focusDesktop(), which also takes it back from Tabby.
91
+ return new Promise(resolve => {
92
+ const done = (result) => {
93
+ box.remove();
94
+ resolve(result);
95
+ };
96
+ form.addEventListener('submit', event => {
97
+ event.preventDefault();
98
+ const username = field('username').value.trim();
99
+ if (!username) {
100
+ field('username').focus();
101
+ return;
102
+ }
103
+ done({ username, password: field('password').value, remember: field('remember').checked });
104
+ });
105
+ field('cancel').addEventListener('click', () => done(null));
106
+ form.addEventListener('keydown', event => {
107
+ if (event.key === 'Escape') {
108
+ done(null);
109
+ }
110
+ });
111
+ abort.then(() => done(null));
112
+ });
113
+ }
114
+ //# sourceMappingURL=signin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signin.js","sourceRoot":"","sources":["../src/signin.ts"],"names":[],"mappings":"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"}
package/dist/ssh.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSSHTab = isSSHTab;
4
+ exports.openTcpStream = openTcpStream;
5
+ exports.execRemote = execRemote;
6
+ const sshChannelStream_1 = require("./sshChannelStream");
7
+ function isSSHTab(tab) {
8
+ // The profile is set when the tab is created; sshSession only once it connects.
9
+ return tab?.profile?.type === 'ssh' && !!tab.element?.nativeElement;
10
+ }
11
+ function client(tab) {
12
+ const ssh = tab.sshSession?.ssh;
13
+ if (!tab.sshSession?.open || typeof ssh?.openTCPForwardChannel !== 'function') {
14
+ throw new Error('The SSH connection is not open');
15
+ }
16
+ return ssh;
17
+ }
18
+ /** A direct-tcpip channel to host:port as the remote sees it (e.g. its own 127.0.0.1), as a Node stream. */
19
+ async function openTcpStream(tab, host, port) {
20
+ const ssh = client(tab);
21
+ const channel = await ssh.activateChannel(await ssh.openTCPForwardChannel({
22
+ addressToConnectTo: host,
23
+ portToConnectTo: port,
24
+ originatorAddress: '127.0.0.1',
25
+ originatorPort: 0,
26
+ }));
27
+ return new sshChannelStream_1.SSHChannelStream(channel);
28
+ }
29
+ /** Runs `command` on the remote with `stdin`, returns stdout. No PTY, no exit status (russh doesn't expose it). */
30
+ async function execRemote(tab, command, stdin = '', timeoutMs = 60000) {
31
+ const ssh = client(tab);
32
+ const channel = await ssh.activateChannel(await ssh.openSessionChannel());
33
+ const out = [];
34
+ const closed = new Promise(resolve => channel.closed$.subscribe(() => resolve()));
35
+ channel.data$.subscribe((d) => out.push(Buffer.from(d)));
36
+ await channel.requestExec(command);
37
+ if (stdin) {
38
+ await channel.write(new Uint8Array(Buffer.from(stdin)));
39
+ }
40
+ await channel.eof();
41
+ let timer;
42
+ const timeout = new Promise((_, reject) => {
43
+ timer = setTimeout(() => reject(new Error(`Remote command timed out after ${timeoutMs / 1000}s`)), timeoutMs);
44
+ });
45
+ try {
46
+ await Promise.race([closed, timeout]);
47
+ }
48
+ finally {
49
+ clearTimeout(timer);
50
+ channel.close().catch(() => null);
51
+ }
52
+ return Buffer.concat(out).toString('utf8');
53
+ }
54
+ //# sourceMappingURL=ssh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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"}