9remote 2.2.1 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/desktop-bridge.cs +43 -5
- package/dist/bin/desktop-elevate.cs +70 -11
- package/dist/cli.cjs +93 -92
- package/dist/server.cjs +168 -99
- package/dist/ui/assets/{index-vnJprY0p.js → index-DviNN66Q.js} +2895 -3938
- package/dist/ui/assets/{index-Cq9ojg5L.css → index-ulRsGAYR.css} +8 -5
- package/dist/ui/index.html +2 -2
- package/package.json +4 -2
|
@@ -29,6 +29,19 @@ class DesktopBridge {
|
|
|
29
29
|
const ushort VK_RETURN = 0x0D;
|
|
30
30
|
const ushort VK_SHIFT = 0x10;
|
|
31
31
|
const string PIPE = "9remote-desktop";
|
|
32
|
+
// Bump on every change to this file. The agent reads the same constant out of
|
|
33
|
+
// the shipped .cs and compares it against what the running worker reports, so
|
|
34
|
+
// a stale worker is detected without relying on file mtimes. Same contract as
|
|
35
|
+
// DAEMON_VERSION for the pty daemon.
|
|
36
|
+
const string VERSION = "2";
|
|
37
|
+
// Global\ (not Local\): the boot task runs in session 0 while a user-triggered
|
|
38
|
+
// launcher runs in the console session — a per-session mutex would not see across them.
|
|
39
|
+
const string MUTEX_NAME = "Global\\9remote-desktop-bridge";
|
|
40
|
+
// How long a starting worker waits for a shutting-down one to release the lock.
|
|
41
|
+
const int HANDOFF_WAIT_MS = 10000;
|
|
42
|
+
|
|
43
|
+
// Static so the GC never collects it while Main loops forever.
|
|
44
|
+
static Mutex _instanceLock;
|
|
32
45
|
|
|
33
46
|
[StructLayout(LayoutKind.Sequential)]
|
|
34
47
|
struct MOUSEINPUT { public int dx, dy; public uint mouseData, dwFlags, time; public IntPtr dwExtraInfo; }
|
|
@@ -112,7 +125,7 @@ class DesktopBridge {
|
|
|
112
125
|
short k = VkKeyScanW(c);
|
|
113
126
|
ushort vk = (ushort)(k & 0xFF);
|
|
114
127
|
bool shift = (k & 0x0100) != 0;
|
|
115
|
-
|
|
128
|
+
// Never log the char or its vk — TYPE carries the user's password.
|
|
116
129
|
if (shift) {
|
|
117
130
|
SendKey(VK_SHIFT, false);
|
|
118
131
|
SendKey(vk, false); Thread.Sleep(25); SendKey(vk, true);
|
|
@@ -128,6 +141,28 @@ class DesktopBridge {
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
static void Main() {
|
|
144
|
+
// Single-instance guard. The pipe is created with maxInstances=1, so a second
|
|
145
|
+
// worker could never serve — it would spin in the retry loop forever while
|
|
146
|
+
// holding a lock on this .exe, blocking every future csc rebuild.
|
|
147
|
+
//
|
|
148
|
+
// Held in a static field, not a local: GC.KeepAlive only protects up to the
|
|
149
|
+
// call, after which nothing references a local mutex for the rest of this
|
|
150
|
+
// infinite loop — the finalizer would release it mid-run and let a second
|
|
151
|
+
// worker in. A static root lives as long as the process.
|
|
152
|
+
// Never let the guard itself kill the worker: if the mutex can't be created
|
|
153
|
+
// or opened (ACL, exotic session), carry on unguarded — the pipe's own
|
|
154
|
+
// maxInstances=1 still prevents two workers from serving at once.
|
|
155
|
+
try {
|
|
156
|
+
bool isNew;
|
|
157
|
+
_instanceLock = new Mutex(true, MUTEX_NAME, out isNew);
|
|
158
|
+
if (!isNew) {
|
|
159
|
+
// The previous worker may be shutting down right now (agent issued STOP,
|
|
160
|
+
// then relaunched us via the task). Wait for it to release rather than
|
|
161
|
+
// exiting into a gap where no worker is running at all.
|
|
162
|
+
if (!_instanceLock.WaitOne(HANDOFF_WAIT_MS)) { L("another worker still holds the lock — exiting"); return; }
|
|
163
|
+
L("took over from a previous instance");
|
|
164
|
+
}
|
|
165
|
+
} catch (Exception e) { L("mutex guard unavailable: " + e.Message); }
|
|
131
166
|
try { L("start session=" + Process.GetCurrentProcess().SessionId + " pid=" + Process.GetCurrentProcess().Id + " user=" + WindowsIdentity.GetCurrent().Name); } catch {}
|
|
132
167
|
// ACL: SYSTEM + Administrators + Authenticated Users so the user-scope agent can connect.
|
|
133
168
|
var sec = new PipeSecurity();
|
|
@@ -150,17 +185,20 @@ class DesktopBridge {
|
|
|
150
185
|
while ((line = sr.ReadLine()) != null) {
|
|
151
186
|
line = line.Trim();
|
|
152
187
|
if (line.Length == 0) continue;
|
|
153
|
-
if (line == "
|
|
188
|
+
if (line == "VERSION") {
|
|
189
|
+
sw.WriteLine("VERSION " + VERSION);
|
|
190
|
+
} else if (line == "STATE") {
|
|
154
191
|
sw.WriteLine("DESKTOP " + ActiveDesktop());
|
|
155
192
|
} else if (line.StartsWith("TYPE ")) {
|
|
156
193
|
TypeText(line.Substring(5));
|
|
157
194
|
sw.WriteLine("OK");
|
|
158
195
|
} else if (line == "STOP") {
|
|
159
196
|
sw.WriteLine("OK");
|
|
160
|
-
//
|
|
161
|
-
//
|
|
197
|
+
// Clean exit: STOP is an intentional shutdown (user toggled Off, or
|
|
198
|
+
// the agent is freeing the locked exe for a rebuild). Restarting here
|
|
199
|
+
// would defeat both. The boot task brings it back next reboot.
|
|
162
200
|
try { srv.Dispose(); } catch { }
|
|
163
|
-
Environment.Exit(
|
|
201
|
+
Environment.Exit(0);
|
|
164
202
|
} else {
|
|
165
203
|
sw.WriteLine("ERR unknown");
|
|
166
204
|
}
|
|
@@ -6,6 +6,7 @@ using System;
|
|
|
6
6
|
using System.Diagnostics;
|
|
7
7
|
using System.IO;
|
|
8
8
|
using System.Runtime.InteropServices;
|
|
9
|
+
using System.Threading;
|
|
9
10
|
|
|
10
11
|
class DesktopElevate {
|
|
11
12
|
[DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId();
|
|
@@ -18,6 +19,9 @@ class DesktopElevate {
|
|
|
18
19
|
[DllImport("advapi32.dll", SetLastError = true)] static extern bool AdjustTokenPrivileges(IntPtr h, bool dis, ref TOKEN_PRIVILEGES np, int len, IntPtr p, IntPtr l);
|
|
19
20
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
20
21
|
static extern bool CreateProcessWithTokenW(IntPtr h, uint logonFlags, string app, string cmd, uint flags, IntPtr env, string dir, ref STARTUPINFO si, out PROCESS_INFORMATION pi);
|
|
22
|
+
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
23
|
+
static extern bool CreateProcessAsUserW(IntPtr h, string app, string cmd, IntPtr pa, IntPtr ta, bool inh, uint flags, IntPtr env, string dir, ref STARTUPINFO si, out PROCESS_INFORMATION pi);
|
|
24
|
+
[DllImport("advapi32.dll", SetLastError = true)] static extern bool SetTokenInformation(IntPtr h, int cls, ref uint val, int len);
|
|
21
25
|
[DllImport("userenv.dll", SetLastError = true)] static extern bool CreateEnvironmentBlock(out IntPtr env, IntPtr hToken, bool inherit);
|
|
22
26
|
[DllImport("userenv.dll")] static extern bool DestroyEnvironmentBlock(IntPtr env);
|
|
23
27
|
|
|
@@ -31,6 +35,14 @@ class DesktopElevate {
|
|
|
31
35
|
const int TokenPrimary = 1;
|
|
32
36
|
const uint SE_PRIVILEGE_ENABLED = 0x00000002;
|
|
33
37
|
const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
|
38
|
+
const int TokenSessionId = 12;
|
|
39
|
+
|
|
40
|
+
// Launcher failures are otherwise silent (every error path just returns), and
|
|
41
|
+
// the only symptom downstream is "no worker". Log beside the exe like the worker.
|
|
42
|
+
static string LOG = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "launcher.log");
|
|
43
|
+
static void L(string m) {
|
|
44
|
+
try { File.AppendAllText(LOG, "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + m + "\n"); } catch {}
|
|
45
|
+
}
|
|
34
46
|
|
|
35
47
|
[StructLayout(LayoutKind.Sequential)]
|
|
36
48
|
struct LUID { public uint LowPart; public int HighPart; }
|
|
@@ -57,20 +69,50 @@ class DesktopElevate {
|
|
|
57
69
|
AdjustTokenPrivileges(h, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
// Highest desktop-bridge-<n>.exe in dir, or null. Numeric compare — a string
|
|
73
|
+
// sort would rank "9" above "10" and pin the worker to an old build forever.
|
|
74
|
+
static string PickNewestWorker(string dir) {
|
|
75
|
+
string best = null;
|
|
76
|
+
int bestV = -1;
|
|
77
|
+
try {
|
|
78
|
+
foreach (var f in Directory.GetFiles(dir, "desktop-bridge-*.exe")) {
|
|
79
|
+
var name = Path.GetFileNameWithoutExtension(f);
|
|
80
|
+
int v;
|
|
81
|
+
if (!int.TryParse(name.Substring("desktop-bridge-".Length), out v)) continue;
|
|
82
|
+
if (v > bestV) { bestV = v; best = f; }
|
|
83
|
+
}
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
return best;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
static void Main() {
|
|
61
|
-
uint sid =
|
|
89
|
+
uint sid = 0xFFFFFFFF;
|
|
90
|
+
L("start pid=" + Process.GetCurrentProcess().Id + " session=" + Process.GetCurrentProcess().SessionId);
|
|
62
91
|
|
|
63
92
|
IntPtr hSelf;
|
|
64
93
|
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hSelf)) {
|
|
65
94
|
EnablePriv(hSelf, "SeImpersonatePrivilege");
|
|
66
95
|
EnablePriv(hSelf, "SeAssignPrimaryTokenPrivilege");
|
|
67
96
|
EnablePriv(hSelf, "SeIncreaseQuotaPrivilege");
|
|
97
|
+
// Required to rewrite a token's session id (see the SetTokenInformation below).
|
|
98
|
+
EnablePriv(hSelf, "SeTcbPrivilege");
|
|
68
99
|
CloseHandle(hSelf);
|
|
69
100
|
}
|
|
70
101
|
|
|
102
|
+
// At boot this launcher runs before the console session finishes coming up:
|
|
103
|
+
// WTSGetActiveConsoleSessionId can return 0xFFFFFFFF (no session attached)
|
|
104
|
+
// and winlogon.exe for that session may not exist yet. Poll for up to 2min
|
|
105
|
+
// instead of exiting — a one-shot check would leave the worker dead until
|
|
106
|
+
// the next reboot, which is the very failure this task exists to prevent.
|
|
71
107
|
int wpid = -1;
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
for (int attempt = 0; attempt < 120 && wpid < 0; attempt++) {
|
|
109
|
+
sid = WTSGetActiveConsoleSessionId();
|
|
110
|
+
if (sid != 0xFFFFFFFF) {
|
|
111
|
+
foreach (var p in Process.GetProcesses()) {
|
|
112
|
+
if (p.ProcessName.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && p.SessionId == sid) { wpid = p.Id; break; }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (wpid < 0) Thread.Sleep(1000);
|
|
74
116
|
}
|
|
75
117
|
if (wpid < 0) return;
|
|
76
118
|
|
|
@@ -86,19 +128,36 @@ class DesktopElevate {
|
|
|
86
128
|
IntPtr env;
|
|
87
129
|
if (!CreateEnvironmentBlock(out env, dup, false)) { CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
|
|
88
130
|
|
|
89
|
-
// Worker lives beside this launcher exe.
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
// Worker lives beside this launcher exe, named desktop-bridge-<version>.exe.
|
|
132
|
+
// Version-stamped so the agent can build a new one without overwriting (and
|
|
133
|
+
// locking) the copy currently running — this boot then picks up the newest.
|
|
134
|
+
string app = PickNewestWorker(AppDomain.CurrentDomain.BaseDirectory);
|
|
135
|
+
if (app == null) { DestroyEnvironmentBlock(env); CloseHandle(dup); CloseHandle(ht); CloseHandle(hp); return; }
|
|
92
136
|
|
|
93
137
|
var si = new STARTUPINFO { cb = Marshal.SizeOf(typeof(STARTUPINFO)) };
|
|
94
138
|
si.lpDesktop = @"winsta0\default";
|
|
95
139
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
140
|
+
// Pin the token to the console session. The duplicated winlogon token already
|
|
141
|
+
// carries it, but this is the one thing that must not be wrong — a worker in
|
|
142
|
+
// the wrong session still starts, still answers the pipe, and SendInput still
|
|
143
|
+
// reports success, while every keystroke lands on session 0's Winlogon desktop
|
|
144
|
+
// instead of the screen the user is looking at. Needs SeTcbPrivilege.
|
|
145
|
+
if (!SetTokenInformation(dup, TokenSessionId, ref sid, sizeof(uint)))
|
|
146
|
+
L("SetTokenInformation(session=" + sid + ") failed winerr=" + Marshal.GetLastWin32Error());
|
|
147
|
+
|
|
148
|
+
// CreateProcessAsUserW, not CreateProcessWithTokenW: the latter routes through
|
|
149
|
+
// the Secondary Logon service, which places the child in the CALLER's session
|
|
150
|
+
// and ignores the token's. That was invisible while the launcher was started by
|
|
151
|
+
// UAC from the console session — under the AtStartup task the launcher runs in
|
|
152
|
+
// session 0, and the worker went with it. CreateProcessAsUserW honours the
|
|
153
|
+
// token's session; SYSTEM already holds the required SeAssignPrimaryTokenPrivilege.
|
|
100
154
|
PROCESS_INFORMATION pi;
|
|
101
|
-
bool ok =
|
|
155
|
+
bool ok = CreateProcessAsUserW(dup, app, "\"" + app + "\"", IntPtr.Zero, IntPtr.Zero, false, CREATE_UNICODE_ENVIRONMENT, env, AppDomain.CurrentDomain.BaseDirectory, ref si, out pi);
|
|
156
|
+
if (!ok) {
|
|
157
|
+
L("CreateProcessAsUserW failed winerr=" + Marshal.GetLastWin32Error() + " — falling back to CreateProcessWithTokenW");
|
|
158
|
+
ok = CreateProcessWithTokenW(dup, 0, app, "\"" + app + "\"", CREATE_UNICODE_ENVIRONMENT, env, AppDomain.CurrentDomain.BaseDirectory, ref si, out pi);
|
|
159
|
+
}
|
|
160
|
+
L((ok ? "spawned " : "spawn FAILED ") + app + " session=" + sid + (ok ? " pid=" + pi.dwProcessId : " winerr=" + Marshal.GetLastWin32Error()));
|
|
102
161
|
|
|
103
162
|
DestroyEnvironmentBlock(env);
|
|
104
163
|
if (ok) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
|