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
package/remote/trd-pty.py CHANGED
@@ -2,8 +2,8 @@
2
2
  """trd-pty: a shell session that outlives its terminals and can be shown in several at once.
3
3
 
4
4
  A session is one small process that owns a PTY running the user's login shell and listens on a Unix
5
- socket in a private per-user directory (0700; peers must have the same uid). Terminals attach with
6
- `trd-pty attach`:
5
+ socket in a private per-user directory (0700; both ends check that the other has the same uid).
6
+ Terminals attach with `trd-pty attach`:
7
7
 
8
8
  - output from the shell goes, byte for byte, to every attached terminal;
9
9
  - input from any attached terminal goes to the shell;
@@ -30,6 +30,7 @@ import secrets
30
30
  import selectors
31
31
  import signal
32
32
  import socket
33
+ import stat
33
34
  import struct
34
35
  import sys
35
36
  import termios
@@ -48,23 +49,66 @@ SIZE = struct.Struct('!HH')
48
49
  # session -> client: o output | x exit(status) | q query answer (json)
49
50
 
50
51
 
52
+ def unsafe(message):
53
+ sys.stderr.write(f'trd-pty: {message}\n')
54
+ sys.exit(3) # for `new`: no session could be started, so the login falls back to tmux
55
+
56
+
57
+ def safe_base(path, owners):
58
+ """Whether no other user can rename or replace what is in `path`: a directory of one of `owners` that others
59
+ can't write to, or only for their own entries (sticky, like /tmp)."""
60
+ try:
61
+ st = os.stat(path)
62
+ except OSError:
63
+ return False
64
+ return (stat.S_ISDIR(st.st_mode) and st.st_uid in owners
65
+ and (not st.st_mode & 0o022 or bool(st.st_mode & stat.S_ISVTX)))
66
+
67
+
51
68
  def runtime_dir():
52
69
  """Private per-user directory for session sockets; refuses anything another user could touch."""
53
70
  uid = os.getuid()
54
71
  base = os.environ.get('XDG_RUNTIME_DIR')
55
- if not base or not os.path.isdir(base) or os.stat(base).st_uid != uid:
72
+ if not base or not safe_base(base, (uid,)):
56
73
  base = '/tmp'
74
+ if not safe_base(base, (0, uid)):
75
+ unsafe(f'{base} lets other users replace what is in it (no sticky bit)')
57
76
  path = os.path.join(base, 'trd-pty' if base != '/tmp' else f'trd-pty-{uid}')
58
77
  try:
59
78
  os.mkdir(path, 0o700)
60
79
  except FileExistsError:
61
80
  pass
62
- st = os.lstat(path)
63
- if not os.path.isdir(path) or os.path.islink(path) or st.st_uid != uid or st.st_mode & 0o077:
64
- sys.exit(f'trd-pty: unsafe session directory {path}')
81
+ except OSError as e: # a base that can't be written to, say: no session here either
82
+ unsafe(f'cannot create the session directory {path} ({e.strerror})')
83
+ try:
84
+ st = os.lstat(path)
85
+ except OSError as e:
86
+ unsafe(f'cannot check the session directory {path} ({e.strerror})')
87
+ if not stat.S_ISDIR(st.st_mode) or st.st_uid != uid or st.st_mode & 0o077:
88
+ unsafe(f'unsafe session directory {path}')
65
89
  return path
66
90
 
67
91
 
92
+ def peer_uid(sock):
93
+ """The uid of the process at the other end of a connected Unix socket, as the system tells it; None where it
94
+ can't (OSError if it fails)."""
95
+ if hasattr(socket, 'SO_PEERCRED'): # Linux: struct ucred (pid, uid, gid); uid_t is unsigned, as os.getuid() has it
96
+ return struct.unpack('iII', sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))[1]
97
+ if hasattr(socket, 'LOCAL_PEERCRED'): # macOS, BSD: struct xucred (version, uid, …) at level SOL_LOCAL, 0
98
+ return struct.unpack_from('=II', sock.getsockopt(0, socket.LOCAL_PEERCRED, 256))[1]
99
+ return None
100
+
101
+
102
+ def serves_me(sock):
103
+ """Whether the session at the other end of `sock` runs as this user. The directory is private, but what answers
104
+ is checked too: the client is about to send it this terminal's keystrokes. Only the system's word about the
105
+ connection counts (who owns the socket's path now says nothing about who answered), so without it, no."""
106
+ try:
107
+ return peer_uid(sock) == os.getuid()
108
+ except OSError:
109
+ return False
110
+
111
+
68
112
  def socket_path(session_id):
69
113
  if not session_id or not all(c in '0123456789abcdef' for c in session_id):
70
114
  sys.exit(f'trd-pty: bad session id {session_id!r}')
@@ -212,11 +256,14 @@ class Session:
212
256
  sock, _ = self.listener.accept()
213
257
  except OSError:
214
258
  return
215
- if hasattr(socket, 'SO_PEERCRED'):
216
- _, uid, _ = struct.unpack('3i', sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))
217
- if uid != os.getuid():
218
- sock.close()
219
- return
259
+ # Only a client the system says runs as this user: without that word (no peer credentials here), nobody.
260
+ try:
261
+ uid = peer_uid(sock)
262
+ except OSError:
263
+ uid = None
264
+ if uid != os.getuid():
265
+ sock.close()
266
+ return
220
267
  sock.setblocking(False)
221
268
  client = Client(sock)
222
269
  self.clients[sock] = client
@@ -416,11 +463,15 @@ FOCUS_IN, FOCUS_OUT = b'\x1b[I', b'\x1b[O'
416
463
 
417
464
  def attach(session_id, claim=True):
418
465
  """Relays this terminal to a session until the session ends (0) or the connection breaks (1)."""
466
+ path = socket_path(session_id)
419
467
  sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
420
468
  try:
421
- sock.connect(socket_path(session_id))
469
+ sock.connect(path)
422
470
  except OSError as e:
423
471
  sys.exit(f'trd-pty: no session {session_id} ({e.strerror})')
472
+ if not serves_me(sock):
473
+ sock.close()
474
+ sys.exit(f'trd-pty: session {session_id} is not yours')
424
475
  fd_in, fd_out = sys.stdin.fileno(), sys.stdout.fileno()
425
476
  rows, cols = term_size(fd_in)
426
477
  sock.sendall(frame(b'h', struct.pack('!HHB', rows, cols, 1 if claim else 0)))
@@ -507,6 +558,9 @@ def query(path):
507
558
  sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
508
559
  sock.settimeout(2)
509
560
  sock.connect(path)
561
+ if not serves_me(sock):
562
+ sock.close()
563
+ raise PermissionError(f'{path} is not a session of yours')
510
564
  sock.sendall(frame(b'q'))
511
565
  frames = Frames()
512
566
  while True:
@@ -529,7 +583,11 @@ def list_sessions():
529
583
  try:
530
584
  sessions.append(query(path))
531
585
  except ConnectionRefusedError:
532
- os.unlink(path) # its owner is gone (killed, or before a reboot's tmpfs wipe)
586
+ try:
587
+ if stat.S_ISSOCK(os.lstat(path).st_mode):
588
+ os.unlink(path) # its owner is gone (killed, or before a reboot's tmpfs wipe)
589
+ except OSError:
590
+ pass
533
591
  except OSError:
534
592
  pass
535
593
  return sessions
@@ -540,6 +598,9 @@ def main(argv):
540
598
  if cmd == 'new':
541
599
  if not os.isatty(0):
542
600
  sys.exit('trd-pty: new needs a terminal')
601
+ if not hasattr(socket, 'SO_PEERCRED') and not hasattr(socket, 'LOCAL_PEERCRED'):
602
+ # Both ends check who is at the other one, and here they can't: no session (the login falls back to tmux).
603
+ unsafe('this system does not say which user is at the other end of a socket')
543
604
  return attach(start_session(*term_size(0)))
544
605
  if cmd == 'attach' and len(argv) == 3:
545
606
  return attach(argv[2])
@@ -0,0 +1,26 @@
1
+ # vendor/ is IronRDP 9b151c4c2e47c6014e1e8e55909d4180aa8bdb99 with the patches below, built by scripts/build-ironrdp.sh with
2
+ # rustc 1.94.1 (e408947bf 2026-03-25), wasm-pack 0.15.0 (wasm-bindgen 0.2.126, binaryen version_117) and
3
+ # Node.js v26.8.2, on aarch64-apple-darwin. Check from the repository's root: sha256sum -c vendor/SHA256SUMS
4
+ # (or shasum -a 256 -c vendor/SHA256SUMS).
5
+ 965bc4a29f18bd6d766fe397c96dbe1483b23607ae8dd5a7458149d3625c2023 ironrdp/BASE_COMMIT
6
+ 315f1de9185945c1bf248706353758ef0af9f94a2bcd01d9709454f5da752dca ironrdp/patches/0001-fix-dvc-drop-data-for-a-channel-that-is-not-open-ins.patch
7
+ 637157633fc962bdd4f300477134d42106dc606aaad933f74d70f9f2fc90cecd ironrdp/patches/0002-fix-web-fit-the-canvas-to-the-host-element-not-the-w.patch
8
+ 81b3ea3fd8d339cf982f1dfb10fa765aae0beec9aa1333b2c918c3f57a89eccb ironrdp/patches/0003-fix-web-dispatch-the-ready-event-after-clipboard-ini.patch
9
+ 30c1202263bd6654b2617e787ca3385f15361eb26d809ebc9ad9aef52c7b98cf ironrdp/patches/0004-fix-rdpsnd-echo-the-Training-PDU-s-wPackSize-in-the-.patch
10
+ f6193b9fcc3d9be6442f7ad906a1a586c842459952ed201dabee0364e6437ae0 ironrdp/patches/0005-feat-web-graphics-pipeline-EGFX-per-connection-and-f.patch
11
+ 4b8d33f188be91d478886fd2f1ee58c8ae86ac7594ca682b7b3f54de2fe57839 ironrdp/patches/0006-feat-web-audio-playback-RDPSND-through-an-audioPlayb.patch
12
+ ebe7d796727f9855c32ba1b8d5b5d2429483be54cb6654a130bfb9c57bbab2c1 ironrdp/patches/0007-feat-web-microphone-redirection-AUDIO_INPUT-through-.patch
13
+ 8859fbb775635beaef4d558fe8512ad47dac4b9610b31e44fa99987d8d4cbe76 ironrdp/patches/0008-feat-web-H.264-in-the-graphics-pipeline-decoded-by-W.patch
14
+ f597cc1fa3e8c4fce560b55024f6e4f3ea58d469c4f775da7abd0e6555a7c2ef ironrdp/patches/0009-feat-web-let-the-host-hand-init-the-WebAssembly-modu.patch
15
+ c7f82ac4b382e478a135734e7864ba83bcb2ba715b27cf40b17ad90342a936f3 ironrdp/patches/0010-fix-graphics-read-Progressive-SRL-code-words-as-valu.patch
16
+ 92237eed373c3b597fea0dd6e5548052ac13ad34f6b3276d0ce53dd667a1c0d7 ironrdp/patches/0011-feat-web-let-the-host-send-key-events-to-a-session.patch
17
+ 4c843f724d8f348297b5d17174c0c2d1dbc611a57f7eb2bb5cba633ebf8c5ba3 ironrdp/patches/0012-fix-web-keep-clipboard-monitoring-and-focus-writes-p.patch
18
+ 48e697ea31969259faebc743026f91995b33af1e9cbc6af6970861c7c59c671c ironrdp/patches/0013-fix-web-sync-the-clipboard-automatically-for-the-foc.patch
19
+ a9029bbb677deb208eda1202daec144b6a9e033fc987480eab4bf8b8e0ac70d6 ironrdp/patches/0014-fix-web-read-H.264-pictures-correctly-where-WebKit-i.patch
20
+ 459b08a00dc1e0341e1e80c0dab9b724b952b886964042ebe6ed9d80be5880c4 ironrdp/patches/0015-feat-web-drive-redirection-through-a-JavaScript-file.patch
21
+ d67aa6029edb142daa1e5069fa79ab1917687474a89a61bd221081f204ba8e12 ironrdp/patches/0016-fix-svc-send-chunked-messages-without-CHANNEL_FLAG_S.patch
22
+ 35a592496b2c735d78450cf417e16a260143af2490817e57cd6a13b5f5b340c4 ironrdp/patches/0017-perf-web-read-H.264-regions-that-are-far-apart-one-b.patch
23
+ 08ccf512db2cfe21b16f958eda6fa4b23e052a0c0e91829a4fc2657f8bd8dd4d vendor/IRONRDP-LICENSE
24
+ 538641d863e52972074b032c111a67cfa02d06290bff0601423f3dfd25ad563b vendor/iron-remote-desktop-rdp.js
25
+ 5a6363b9140a5a2abf0218aa39edb38f10445e8d7e9c5a798c321772e1f07c11 vendor/iron-remote-desktop.js
26
+ 31f961cc5fc60f6a3dc580333f871bf9b380950dc849395465844dcc90f871f3 vendor/ironrdp_web_bg.wasm