windows-development-sandbox 1.13.0 → 1.14.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.
- package/dist/app/features/accounts/accounts.def.d.ts +1 -0
- package/dist/app/features/accounts/accounts.def.js +12 -3
- package/dist/app/features/accounts/accounts.run.d.ts +1 -0
- package/dist/app/features/accounts/accounts.run.js +20 -11
- package/dist/app/features/defs.d.ts +1 -0
- package/dist/app/features/settings/settings-file.d.ts +7 -0
- package/dist/app/features/settings/settings-file.js +19 -2
- package/dist/app/features/settings/settings-rows.js +9 -1
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +7 -1
- package/dist/bin/chunk-BQ23GBLC.js +78790 -0
- package/dist/bin/chunk-F7IMME5G.js +10432 -0
- package/dist/bin/chunk-LQHEUN2A.js +7 -0
- package/dist/bin/claude-host.js +10 -4
- package/dist/bin/claude-sbx.js +8 -2
- package/dist/bin/codex-sbx.js +8 -2
- package/dist/bin/gemini-sbx.js +8 -2
- package/dist/bin/sandbox.js +7 -2
- package/dist/bin/sbx-hide.js +8 -2
- package/dist/bin/sbx-show.js +8 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/launch/prepare.js +24 -7
- package/dist/cli/commands/launch/running.js +4 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/commands/storage/offer.js +1 -1
- package/dist/cli/menu/live.d.ts +29 -0
- package/dist/cli/menu/live.js +92 -21
- package/dist/cli/menu/menu.d.ts +2 -1
- package/dist/cli/menu/menu.js +16 -2
- package/dist/cli/menu/pages/list-page.d.ts +5 -0
- package/dist/cli/menu/pages/list-page.js +19 -0
- package/dist/cli/menu/shell.d.ts +2 -0
- package/dist/cli/menu/shell.js +1 -1
- package/dist/cli/services/accounts-service.d.ts +7 -0
- package/dist/cli/services/accounts-service.js +20 -0
- package/dist/cli/tui/app.d.ts +4 -0
- package/dist/cli/tui/app.js +5 -0
- package/dist/cli/tui/terminal.d.ts +6 -0
- package/dist/cli/tui/terminal.js +8 -1
- package/dist/cli/validate.js +8 -4
- package/dist/core/package-info.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +19 -4
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/storage/contained/state.d.ts +3 -2
- package/dist/host/storage/contained/state.js +5 -2
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +10 -1
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/menu-perf.test.js +198 -12
- package/dist/selftest/menu.test.js +4 -0
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// The pipe \\.\pipe\AgentSandboxStorage. All four instances are created at start (the first one with
|
|
2
|
+
// FILE_FLAG_FIRST_PIPE_INSTANCE, so a pipe someone made first stops the service) and reused for good: the instance
|
|
3
|
+
// count never drops below the maximum, so nobody can add an instance of their own and catch other users' calls.
|
|
4
|
+
// Remote clients are refused (PIPE_REJECT_REMOTE_CLIENTS and a deny entry for NETWORK); only SYSTEM, Administrators and
|
|
5
|
+
// the "Agent Sandbox Users" group may open it. One request per connection: read one line (512 bytes, 5 seconds), find
|
|
6
|
+
// out who called, rate-limit (connections per user before anything is read, calls per user after; a user whose files
|
|
7
|
+
// stalled a call waits five minutes), work out that user's storage from their settings, do the one operation (one at a
|
|
8
|
+
// time, under a lock that only administrators may hold), answer, log.
|
|
9
|
+
using System;
|
|
10
|
+
using System.ComponentModel;
|
|
11
|
+
using System.Runtime.InteropServices;
|
|
12
|
+
using System.Security.AccessControl;
|
|
13
|
+
using System.Security.Principal;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using System.Threading;
|
|
16
|
+
using Microsoft.Win32.SafeHandles;
|
|
17
|
+
|
|
18
|
+
namespace AgentSandbox.StorageHelper
|
|
19
|
+
{
|
|
20
|
+
public sealed class ServerOptions
|
|
21
|
+
{
|
|
22
|
+
public string PipeName { get; set; }
|
|
23
|
+
public string MutexName { get; set; }
|
|
24
|
+
public SecurityIdentifier Group { get; set; }
|
|
25
|
+
public Log Log { get; set; }
|
|
26
|
+
public SettingsSources Sources { get; set; }
|
|
27
|
+
public bool DryRun { get; set; }
|
|
28
|
+
|
|
29
|
+
/// <summary>Where mounts are recorded (null in a dry run of the tests).</summary>
|
|
30
|
+
public MountRecords Records { get; set; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public sealed class Server : IDisposable
|
|
34
|
+
{
|
|
35
|
+
public const string DefaultPipe = "AgentSandboxStorage";
|
|
36
|
+
public const string DefaultMutex = "Global\\AgentSandboxContainedStorage";
|
|
37
|
+
private const int Instances = 4;
|
|
38
|
+
private const int BufferBytes = 4096;
|
|
39
|
+
private const uint PipeAccessDuplex = 0x3;
|
|
40
|
+
private const uint FlagOverlapped = 0x40000000;
|
|
41
|
+
private const uint FlagFirstPipeInstance = 0x00080000;
|
|
42
|
+
private const uint PipeTypeByte = 0x0;
|
|
43
|
+
private const uint PipeRejectRemoteClients = 0x8;
|
|
44
|
+
private const int ErrorIoPending = 997;
|
|
45
|
+
private const int ErrorPipeConnected = 535;
|
|
46
|
+
private const int ErrorBrokenPipe = 109;
|
|
47
|
+
private const int ErrorOperationAborted = 995;
|
|
48
|
+
private const uint WaitObject0 = 0;
|
|
49
|
+
private const int ReadTimeoutMs = 2000;
|
|
50
|
+
private const int CloseWaitMs = 1000;
|
|
51
|
+
private const int LockTimeoutMs = 120000;
|
|
52
|
+
private static readonly TimeSpan Penalty = TimeSpan.FromMinutes(5);
|
|
53
|
+
|
|
54
|
+
private readonly ServerOptions options;
|
|
55
|
+
private readonly RateLimiter limiter = new RateLimiter(10, 60, TimeSpan.FromMinutes(1));
|
|
56
|
+
// Connections, counted per user of the connecting process before anything is read: someone who only connects
|
|
57
|
+
// (and sends nothing) cannot keep the four pipe instances busy for others.
|
|
58
|
+
private readonly RateLimiter connections = new RateLimiter(30, 600, TimeSpan.FromMinutes(1));
|
|
59
|
+
private readonly System.Collections.Generic.Dictionary<string, DateTime> penalized = new System.Collections.Generic.Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
|
60
|
+
private readonly object work = new object();
|
|
61
|
+
private readonly ManualResetEvent stopping = new ManualResetEvent(false);
|
|
62
|
+
private readonly SafePipe[] pipes = new SafePipe[Instances];
|
|
63
|
+
private readonly Thread[] threads = new Thread[Instances];
|
|
64
|
+
private Mutex machineLock;
|
|
65
|
+
|
|
66
|
+
public Server(ServerOptions options)
|
|
67
|
+
{
|
|
68
|
+
this.options = options;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
72
|
+
private struct SecurityAttributes
|
|
73
|
+
{
|
|
74
|
+
public int Length;
|
|
75
|
+
public IntPtr Descriptor;
|
|
76
|
+
public int Inherit;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
80
|
+
private struct Overlapped
|
|
81
|
+
{
|
|
82
|
+
public IntPtr Internal;
|
|
83
|
+
public IntPtr InternalHigh;
|
|
84
|
+
public uint Offset;
|
|
85
|
+
public uint OffsetHigh;
|
|
86
|
+
public IntPtr Event;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public sealed class SafePipe : SafeHandleZeroOrMinusOneIsInvalid
|
|
90
|
+
{
|
|
91
|
+
public SafePipe() : base(true)
|
|
92
|
+
{
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected override bool ReleaseHandle()
|
|
96
|
+
{
|
|
97
|
+
return NativeMethods.CloseHandle(handle);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static class NativeMethods
|
|
102
|
+
{
|
|
103
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
104
|
+
public static extern SafePipe CreateNamedPipeW(string name, uint openMode, uint pipeMode, uint maxInstances, uint outBuffer, uint inBuffer, uint timeout, ref SecurityAttributes security);
|
|
105
|
+
|
|
106
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
107
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
108
|
+
public static extern bool ConnectNamedPipe(SafePipe pipe, IntPtr overlapped);
|
|
109
|
+
|
|
110
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
111
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
112
|
+
public static extern bool DisconnectNamedPipe(SafePipe pipe);
|
|
113
|
+
|
|
114
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
115
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
116
|
+
public static extern bool ReadFile(SafePipe pipe, IntPtr buffer, int count, IntPtr read, IntPtr overlapped);
|
|
117
|
+
|
|
118
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
119
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
120
|
+
public static extern bool WriteFile(SafePipe pipe, IntPtr buffer, int count, IntPtr written, IntPtr overlapped);
|
|
121
|
+
|
|
122
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
123
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
124
|
+
public static extern bool GetOverlappedResult(SafePipe pipe, IntPtr overlapped, out int transferred, [MarshalAs(UnmanagedType.Bool)] bool wait);
|
|
125
|
+
|
|
126
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
127
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
128
|
+
public static extern bool CancelIoEx(SafePipe pipe, IntPtr overlapped);
|
|
129
|
+
|
|
130
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
131
|
+
public static extern uint WaitForMultipleObjects(uint count, IntPtr[] handles, [MarshalAs(UnmanagedType.Bool)] bool waitAll, uint milliseconds);
|
|
132
|
+
|
|
133
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
134
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
135
|
+
public static extern bool CloseHandle(IntPtr handle);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// <summary>SYSTEM, Administrators, the group; nobody over the network (the deny entry comes first).</summary>
|
|
139
|
+
public static string PipeSddl(SecurityIdentifier group)
|
|
140
|
+
{
|
|
141
|
+
return "D:P(D;;GA;;;NU)(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;" + group.Value + ")";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// <summary>Creates the lock and every pipe instance; throws when the pipe exists already (someone made it first).</summary>
|
|
145
|
+
public void Start()
|
|
146
|
+
{
|
|
147
|
+
RawSecurityDescriptor sd = new RawSecurityDescriptor(PipeSddl(options.Group));
|
|
148
|
+
byte[] sdBytes = new byte[sd.BinaryLength];
|
|
149
|
+
sd.GetBinaryForm(sdBytes, 0);
|
|
150
|
+
GCHandle pinned = GCHandle.Alloc(sdBytes, GCHandleType.Pinned);
|
|
151
|
+
try
|
|
152
|
+
{
|
|
153
|
+
SecurityAttributes attributes = new SecurityAttributes();
|
|
154
|
+
attributes.Length = Marshal.SizeOf(typeof(SecurityAttributes));
|
|
155
|
+
attributes.Descriptor = pinned.AddrOfPinnedObject();
|
|
156
|
+
for (int i = 0; i < Instances; i++)
|
|
157
|
+
{
|
|
158
|
+
uint mode = PipeAccessDuplex | FlagOverlapped | (i == 0 ? FlagFirstPipeInstance : 0);
|
|
159
|
+
SafePipe pipe = NativeMethods.CreateNamedPipeW("\\\\.\\pipe\\" + options.PipeName, mode, PipeTypeByte | PipeRejectRemoteClients, Instances, BufferBytes, BufferBytes, 0, ref attributes);
|
|
160
|
+
if (pipe.IsInvalid)
|
|
161
|
+
{
|
|
162
|
+
Win32Exception e = Win32.LastError(i == 0 ? "The pipe " + options.PipeName + " exists already (another program made it); not starting" : "Could not create the pipe");
|
|
163
|
+
pipe.Dispose();
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
pipes[i] = pipe;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally
|
|
170
|
+
{
|
|
171
|
+
pinned.Free();
|
|
172
|
+
}
|
|
173
|
+
// After the pipes: a second copy of the service stops at the pipe, before it looks at the lock.
|
|
174
|
+
machineLock = CreateMachineLock(options.MutexName, options.Log);
|
|
175
|
+
for (int i = 0; i < Instances; i++)
|
|
176
|
+
{
|
|
177
|
+
int index = i;
|
|
178
|
+
threads[i] = new Thread(delegate() { Serve(pipes[index]); });
|
|
179
|
+
threads[i].IsBackground = true;
|
|
180
|
+
threads[i].Start();
|
|
181
|
+
}
|
|
182
|
+
options.Log.Note("started: pipe " + options.PipeName + (options.DryRun ? " (dry run)" : ""));
|
|
183
|
+
// In the background: Windows waits for OnStart, and a tidy can wait for the lock.
|
|
184
|
+
Thread tidy = new Thread(Tidy);
|
|
185
|
+
tidy.IsBackground = true;
|
|
186
|
+
tidy.Start();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// <summary>Mount points left by a restart (their disk is gone) are taken off; mounts that are still there stay.</summary>
|
|
190
|
+
private void Tidy()
|
|
191
|
+
{
|
|
192
|
+
if (options.Records == null)
|
|
193
|
+
{
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
foreach (MountRecord record in options.Records.Read())
|
|
197
|
+
{
|
|
198
|
+
Request request = RecordRequest(record);
|
|
199
|
+
request.OnlyIfDetached = true;
|
|
200
|
+
Outcome outcome = RunLocked(request);
|
|
201
|
+
if (outcome.Ok && !outcome.AlreadyMounted)
|
|
202
|
+
{
|
|
203
|
+
options.Records.Remove(record);
|
|
204
|
+
}
|
|
205
|
+
options.Log.Write(record.Sid, "", 0, "(service start)", "tidy", record.Set, (outcome.Ok ? "ok: " : "refused: ") + outcome.Message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// <summary>A dismount of a recorded mount, with the usual checks (the image must still belong to that user).</summary>
|
|
210
|
+
public static Request RecordRequest(MountRecord record)
|
|
211
|
+
{
|
|
212
|
+
Request request = new Request();
|
|
213
|
+
request.Verb = "dismount";
|
|
214
|
+
request.Set = record.Set;
|
|
215
|
+
request.User = new SecurityIdentifier(record.Sid);
|
|
216
|
+
request.Settings = new StorageSettings(record.StorageRoot, Rules.MaxGb);
|
|
217
|
+
return request;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// <summary>The lock shared with mount-contained.ps1, created with a DACL that lets only SYSTEM and Administrators open it.</summary>
|
|
221
|
+
private static Mutex CreateMachineLock(string name, Log log)
|
|
222
|
+
{
|
|
223
|
+
MutexSecurity security = new MutexSecurity();
|
|
224
|
+
security.SetAccessRuleProtection(true, false);
|
|
225
|
+
security.AddAccessRule(new MutexAccessRule(new SecurityIdentifier("S-1-5-18"), MutexRights.FullControl, AccessControlType.Allow));
|
|
226
|
+
security.AddAccessRule(new MutexAccessRule(new SecurityIdentifier("S-1-5-32-544"), MutexRights.FullControl, AccessControlType.Allow));
|
|
227
|
+
bool createdNew;
|
|
228
|
+
Mutex mutex = new Mutex(false, name, out createdNew, security);
|
|
229
|
+
if (!createdNew)
|
|
230
|
+
{
|
|
231
|
+
// Anyone may create a named lock: one that a normal user made first could be held to block every mount.
|
|
232
|
+
SecurityIdentifier owner = null;
|
|
233
|
+
try
|
|
234
|
+
{
|
|
235
|
+
owner = mutex.GetAccessControl().GetOwner(typeof(SecurityIdentifier)) as SecurityIdentifier;
|
|
236
|
+
}
|
|
237
|
+
catch (UnauthorizedAccessException)
|
|
238
|
+
{
|
|
239
|
+
owner = null;
|
|
240
|
+
}
|
|
241
|
+
if (owner == null || !(owner.Value == "S-1-5-18" || owner.Value == "S-1-5-32-544"))
|
|
242
|
+
{
|
|
243
|
+
mutex.Dispose();
|
|
244
|
+
throw new InvalidOperationException("The lock " + name + " was made by " + (owner == null ? "someone else" : owner.Value) + ", not by an administrator; not starting (restart the PC).");
|
|
245
|
+
}
|
|
246
|
+
log.Note("the lock " + name + " existed before the service started (made by an administrator)");
|
|
247
|
+
}
|
|
248
|
+
return mutex;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private void Serve(SafePipe pipe)
|
|
252
|
+
{
|
|
253
|
+
using (ManualResetEvent ready = new ManualResetEvent(false))
|
|
254
|
+
{
|
|
255
|
+
while (!stopping.WaitOne(0))
|
|
256
|
+
{
|
|
257
|
+
try
|
|
258
|
+
{
|
|
259
|
+
if (!WaitForClient(pipe, ready))
|
|
260
|
+
{
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
Handle(pipe, ready);
|
|
264
|
+
}
|
|
265
|
+
catch (Exception e)
|
|
266
|
+
{
|
|
267
|
+
options.Log.Note("pipe error: " + e.Message);
|
|
268
|
+
Thread.Sleep(100);
|
|
269
|
+
}
|
|
270
|
+
finally
|
|
271
|
+
{
|
|
272
|
+
NativeMethods.DisconnectNamedPipe(pipe);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private bool WaitForClient(SafePipe pipe, ManualResetEvent ready)
|
|
279
|
+
{
|
|
280
|
+
IntPtr overlapped = NewOverlapped(ready);
|
|
281
|
+
try
|
|
282
|
+
{
|
|
283
|
+
if (NativeMethods.ConnectNamedPipe(pipe, overlapped))
|
|
284
|
+
{
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
int error = Marshal.GetLastWin32Error();
|
|
288
|
+
if (error == ErrorPipeConnected)
|
|
289
|
+
{
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
if (error != ErrorIoPending)
|
|
293
|
+
{
|
|
294
|
+
throw new Win32Exception(error, "Waiting for a client failed");
|
|
295
|
+
}
|
|
296
|
+
uint which = NativeMethods.WaitForMultipleObjects(2, new IntPtr[] { ready.SafeWaitHandle.DangerousGetHandle(), stopping.SafeWaitHandle.DangerousGetHandle() }, false, 0xFFFFFFFF);
|
|
297
|
+
if (which != WaitObject0)
|
|
298
|
+
{
|
|
299
|
+
NativeMethods.CancelIoEx(pipe, overlapped);
|
|
300
|
+
int ignored;
|
|
301
|
+
NativeMethods.GetOverlappedResult(pipe, overlapped, out ignored, true);
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
int transferred;
|
|
305
|
+
if (!NativeMethods.GetOverlappedResult(pipe, overlapped, out transferred, false))
|
|
306
|
+
{
|
|
307
|
+
throw Win32.LastError("A client could not connect");
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
finally
|
|
312
|
+
{
|
|
313
|
+
Marshal.FreeHGlobal(overlapped);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private void Handle(SafePipe pipe, ManualResetEvent ready)
|
|
318
|
+
{
|
|
319
|
+
string caller = Client.ProcessUserOf(pipe);
|
|
320
|
+
if (!connections.TryTake(caller, DateTime.UtcNow))
|
|
321
|
+
{
|
|
322
|
+
if (connections.ShouldLogRefusal(caller, DateTime.UtcNow))
|
|
323
|
+
{
|
|
324
|
+
options.Log.Write(caller, "", 0, "", "", "", "refused: too many connections (further refusals this minute are not logged)");
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
string text = ReadLine(pipe, ready);
|
|
329
|
+
if (text == null)
|
|
330
|
+
{
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
Client client;
|
|
334
|
+
try
|
|
335
|
+
{
|
|
336
|
+
client = Client.Of(pipe, options.Group);
|
|
337
|
+
}
|
|
338
|
+
catch (Exception e)
|
|
339
|
+
{
|
|
340
|
+
if (limiter.ShouldLogRefusal("unidentified", DateTime.UtcNow))
|
|
341
|
+
{
|
|
342
|
+
options.Log.Write("", "", 0, "", "", "", "refused: " + e.Message);
|
|
343
|
+
}
|
|
344
|
+
Reply(pipe, ready, Refused(e is RefusedException ? e.Message : "The caller could not be identified."));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
using (client)
|
|
348
|
+
{
|
|
349
|
+
string sid = client.Sid.Value;
|
|
350
|
+
string verb = "";
|
|
351
|
+
string set = "";
|
|
352
|
+
Outcome outcome;
|
|
353
|
+
if (IsPenalized(sid))
|
|
354
|
+
{
|
|
355
|
+
Reply(pipe, ready, Refused("Your last call had to be stopped (your files took too long to open); wait a few minutes."));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (!limiter.TryTake(sid, DateTime.UtcNow))
|
|
359
|
+
{
|
|
360
|
+
if (limiter.ShouldLogRefusal(sid, DateTime.UtcNow))
|
|
361
|
+
{
|
|
362
|
+
options.Log.Write(sid, client.Account, client.Pid, client.Program, "", "", "refused: too many calls (further refusals this minute are not logged)");
|
|
363
|
+
}
|
|
364
|
+
Reply(pipe, ready, Refused("Too many calls; wait a minute and try again."));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try
|
|
368
|
+
{
|
|
369
|
+
Protocol.Parse(text, out verb, out set);
|
|
370
|
+
StorageSettings settings = ReadSettings(client);
|
|
371
|
+
Request request = new Request();
|
|
372
|
+
request.Verb = verb;
|
|
373
|
+
request.Set = set;
|
|
374
|
+
request.User = client.Sid;
|
|
375
|
+
request.Token = client.Token;
|
|
376
|
+
request.Settings = settings;
|
|
377
|
+
request.DryRun = options.DryRun;
|
|
378
|
+
outcome = RunLocked(request);
|
|
379
|
+
}
|
|
380
|
+
catch (RefusedException e)
|
|
381
|
+
{
|
|
382
|
+
outcome = Refused(e.Message);
|
|
383
|
+
}
|
|
384
|
+
catch (Win32Exception e)
|
|
385
|
+
{
|
|
386
|
+
outcome = Refused(e.Message);
|
|
387
|
+
}
|
|
388
|
+
catch (TimedOutException e)
|
|
389
|
+
{
|
|
390
|
+
outcome = Refused(e.Message);
|
|
391
|
+
outcome.TimedOut = true;
|
|
392
|
+
}
|
|
393
|
+
catch (Exception e)
|
|
394
|
+
{
|
|
395
|
+
// Never leave a client waiting for an answer that will not come.
|
|
396
|
+
outcome = Refused("The storage helper could not read your settings (" + e.GetType().Name + ").");
|
|
397
|
+
}
|
|
398
|
+
if (outcome.TimedOut)
|
|
399
|
+
{
|
|
400
|
+
Penalize(sid);
|
|
401
|
+
}
|
|
402
|
+
options.Log.Write(sid, client.Account, client.Pid, client.Program, verb, set, (outcome.Ok ? "ok: " : "refused: ") + outcome.Message);
|
|
403
|
+
Reply(pipe, ready, outcome);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// <summary>The caller's settings, read under the time limit (the files are the caller's own).</summary>
|
|
408
|
+
private StorageSettings ReadSettings(Client client)
|
|
409
|
+
{
|
|
410
|
+
using (Watchdog dog = new Watchdog(TimeSpan.FromSeconds(10)))
|
|
411
|
+
{
|
|
412
|
+
try
|
|
413
|
+
{
|
|
414
|
+
return UserSettings.For(client, options.Sources);
|
|
415
|
+
}
|
|
416
|
+
catch (Exception)
|
|
417
|
+
{
|
|
418
|
+
if (dog.Fired)
|
|
419
|
+
{
|
|
420
|
+
throw new TimedOutException();
|
|
421
|
+
}
|
|
422
|
+
throw;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private bool IsPenalized(string sid)
|
|
428
|
+
{
|
|
429
|
+
lock (penalized)
|
|
430
|
+
{
|
|
431
|
+
DateTime until;
|
|
432
|
+
return penalized.TryGetValue(sid, out until) && DateTime.UtcNow < until;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private void Penalize(string sid)
|
|
437
|
+
{
|
|
438
|
+
lock (penalized)
|
|
439
|
+
{
|
|
440
|
+
if (penalized.Count > 1000)
|
|
441
|
+
{
|
|
442
|
+
penalized.Clear();
|
|
443
|
+
}
|
|
444
|
+
penalized[sid] = DateTime.UtcNow + Penalty;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private Outcome RunLocked(Request request)
|
|
449
|
+
{
|
|
450
|
+
if (!Monitor.TryEnter(work, LockTimeoutMs))
|
|
451
|
+
{
|
|
452
|
+
return Refused("Another mount or dismount is running. Try again in a minute.");
|
|
453
|
+
}
|
|
454
|
+
try
|
|
455
|
+
{
|
|
456
|
+
bool held = false;
|
|
457
|
+
try
|
|
458
|
+
{
|
|
459
|
+
try
|
|
460
|
+
{
|
|
461
|
+
held = machineLock.WaitOne(30000);
|
|
462
|
+
}
|
|
463
|
+
catch (AbandonedMutexException)
|
|
464
|
+
{
|
|
465
|
+
held = true;
|
|
466
|
+
}
|
|
467
|
+
if (!held)
|
|
468
|
+
{
|
|
469
|
+
return Refused("Another mount or dismount is running. Try again in a minute.");
|
|
470
|
+
}
|
|
471
|
+
if (!RoomToRecord(request))
|
|
472
|
+
{
|
|
473
|
+
return Refused("You have too many data sets mounted through the helper; dismount one first (sandbox repair).");
|
|
474
|
+
}
|
|
475
|
+
Outcome outcome = Operations.Run(request);
|
|
476
|
+
Record(request, outcome);
|
|
477
|
+
return outcome;
|
|
478
|
+
}
|
|
479
|
+
finally
|
|
480
|
+
{
|
|
481
|
+
if (held)
|
|
482
|
+
{
|
|
483
|
+
machineLock.ReleaseMutex();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finally
|
|
488
|
+
{
|
|
489
|
+
Monitor.Exit(work);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private bool RoomToRecord(Request request)
|
|
494
|
+
{
|
|
495
|
+
if (options.Records == null || request.DryRun || request.Verb != "mount")
|
|
496
|
+
{
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
return options.Records.HasRoomFor(new MountRecord(request.User.Value, request.Settings.StorageRoot.TrimEnd('\\'), request.Set));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private void Record(Request request, Outcome outcome)
|
|
503
|
+
{
|
|
504
|
+
if (options.Records == null || request.DryRun || !outcome.Ok)
|
|
505
|
+
{
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
try
|
|
509
|
+
{
|
|
510
|
+
MountRecord record = new MountRecord(request.User.Value, request.Settings.StorageRoot.TrimEnd('\\'), request.Set);
|
|
511
|
+
if (request.Verb == "mount")
|
|
512
|
+
{
|
|
513
|
+
options.Records.Add(record);
|
|
514
|
+
}
|
|
515
|
+
else if (!outcome.AlreadyMounted)
|
|
516
|
+
{
|
|
517
|
+
options.Records.Remove(record);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (Exception e)
|
|
521
|
+
{
|
|
522
|
+
options.Log.Note("could not update the mount record: " + e.Message);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private static Outcome Refused(string message)
|
|
527
|
+
{
|
|
528
|
+
Outcome outcome = new Outcome();
|
|
529
|
+
outcome.Ok = false;
|
|
530
|
+
outcome.Message = message;
|
|
531
|
+
return outcome;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// <summary>One line (up to a newline, 512 bytes, 5 seconds); null when the client sent nothing usable.</summary>
|
|
535
|
+
private string ReadLine(SafePipe pipe, ManualResetEvent ready)
|
|
536
|
+
{
|
|
537
|
+
byte[] collected = new byte[Protocol.MaxRequestBytes + 1];
|
|
538
|
+
int count = 0;
|
|
539
|
+
DateTime until = DateTime.UtcNow.AddMilliseconds(ReadTimeoutMs);
|
|
540
|
+
while (count < collected.Length)
|
|
541
|
+
{
|
|
542
|
+
int left = (int)(until - DateTime.UtcNow).TotalMilliseconds;
|
|
543
|
+
if (left <= 0)
|
|
544
|
+
{
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
int n = Transfer(pipe, ready, false, collected, count, collected.Length - count, left);
|
|
548
|
+
if (n <= 0)
|
|
549
|
+
{
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
for (int i = count; i < count + n; i++)
|
|
553
|
+
{
|
|
554
|
+
if (collected[i] == (byte)'\n')
|
|
555
|
+
{
|
|
556
|
+
return Encoding.UTF8.GetString(collected, 0, i).TrimEnd('\r');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
count += n;
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private void Reply(SafePipe pipe, ManualResetEvent ready, Outcome outcome)
|
|
565
|
+
{
|
|
566
|
+
byte[] bytes = Encoding.UTF8.GetBytes(Protocol.Answer(outcome) + "\n");
|
|
567
|
+
Transfer(pipe, ready, true, bytes, 0, bytes.Length, ReadTimeoutMs);
|
|
568
|
+
// Wait (briefly) for the client to read and close, so disconnecting does not throw the answer away.
|
|
569
|
+
byte[] rest = new byte[64];
|
|
570
|
+
Transfer(pipe, ready, false, rest, 0, rest.Length, CloseWaitMs);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// <summary>One overlapped read or write with a timeout; the bytes moved, 0 on end, timeout or error.</summary>
|
|
574
|
+
private int Transfer(SafePipe pipe, ManualResetEvent ready, bool write, byte[] data, int offset, int count, int timeoutMs)
|
|
575
|
+
{
|
|
576
|
+
ready.Reset();
|
|
577
|
+
IntPtr overlapped = NewOverlapped(ready);
|
|
578
|
+
IntPtr buffer = Marshal.AllocHGlobal(count);
|
|
579
|
+
try
|
|
580
|
+
{
|
|
581
|
+
if (write)
|
|
582
|
+
{
|
|
583
|
+
Marshal.Copy(data, offset, buffer, count);
|
|
584
|
+
}
|
|
585
|
+
bool done = write ? NativeMethods.WriteFile(pipe, buffer, count, IntPtr.Zero, overlapped) : NativeMethods.ReadFile(pipe, buffer, count, IntPtr.Zero, overlapped);
|
|
586
|
+
if (!done)
|
|
587
|
+
{
|
|
588
|
+
int error = Marshal.GetLastWin32Error();
|
|
589
|
+
if (error != ErrorIoPending)
|
|
590
|
+
{
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
uint which = NativeMethods.WaitForMultipleObjects(2, new IntPtr[] { ready.SafeWaitHandle.DangerousGetHandle(), stopping.SafeWaitHandle.DangerousGetHandle() }, false, (uint)Math.Max(timeoutMs, 1));
|
|
594
|
+
if (which != WaitObject0)
|
|
595
|
+
{
|
|
596
|
+
NativeMethods.CancelIoEx(pipe, overlapped);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
int transferred;
|
|
600
|
+
if (!NativeMethods.GetOverlappedResult(pipe, overlapped, out transferred, true))
|
|
601
|
+
{
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
if (!write && transferred > 0)
|
|
605
|
+
{
|
|
606
|
+
Marshal.Copy(buffer, data, offset, transferred);
|
|
607
|
+
}
|
|
608
|
+
return transferred;
|
|
609
|
+
}
|
|
610
|
+
finally
|
|
611
|
+
{
|
|
612
|
+
Marshal.FreeHGlobal(buffer);
|
|
613
|
+
Marshal.FreeHGlobal(overlapped);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private static IntPtr NewOverlapped(ManualResetEvent ready)
|
|
618
|
+
{
|
|
619
|
+
ready.Reset();
|
|
620
|
+
Overlapped o = new Overlapped();
|
|
621
|
+
o.Event = ready.SafeWaitHandle.DangerousGetHandle();
|
|
622
|
+
IntPtr p = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Overlapped)));
|
|
623
|
+
Marshal.StructureToPtr(o, p, false);
|
|
624
|
+
return p;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
public void Stop()
|
|
628
|
+
{
|
|
629
|
+
stopping.Set();
|
|
630
|
+
foreach (Thread t in threads)
|
|
631
|
+
{
|
|
632
|
+
if (t != null)
|
|
633
|
+
{
|
|
634
|
+
t.Join(10000);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
public void Dispose()
|
|
640
|
+
{
|
|
641
|
+
Stop();
|
|
642
|
+
foreach (SafePipe p in pipes)
|
|
643
|
+
{
|
|
644
|
+
if (p != null)
|
|
645
|
+
{
|
|
646
|
+
p.Dispose();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (machineLock != null)
|
|
650
|
+
{
|
|
651
|
+
machineLock.Dispose();
|
|
652
|
+
}
|
|
653
|
+
stopping.Dispose();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|