windows-development-sandbox 1.12.0 → 1.13.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/README.md +1 -0
- package/config.example.json +11 -1
- package/dist/app/features/access/access.def.d.ts +190 -0
- package/dist/app/features/access/access.def.js +273 -0
- package/dist/app/features/access/access.run.d.ts +79 -0
- package/dist/app/features/access/access.run.js +129 -0
- package/dist/app/features/access/index.d.ts +2 -0
- package/dist/app/features/access/index.js +3 -0
- package/dist/app/features/agents/agent-entry.js +5 -0
- package/dist/app/features/agents/agents.def.js +1 -0
- package/dist/app/features/apps/apps.def.js +1 -0
- package/dist/app/features/audit/audit.def.d.ts +11 -2
- package/dist/app/features/audit/audit.def.js +7 -4
- package/dist/app/features/audit/audit.run.d.ts +3 -0
- package/dist/app/features/defs.d.ts +155 -3
- package/dist/app/features/defs.js +12 -1
- package/dist/app/features/files/files.def.js +1 -0
- package/dist/app/features/health/health.def.js +1 -0
- package/dist/app/features/sandbox/sandbox.def.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +36 -0
- package/dist/app/features/sessions/sessions.def.js +77 -9
- package/dist/app/features/sessions/sessions.run.d.ts +25 -2
- package/dist/app/features/sessions/sessions.run.js +71 -3
- package/dist/app/features/settings/settings.def.d.ts +8 -0
- package/dist/app/features/settings/settings.def.js +17 -0
- package/dist/app/features/web/web.def.d.ts +9 -0
- package/dist/app/features/web/web.def.js +26 -1
- package/dist/app/features/web/web.run.d.ts +3 -0
- package/dist/app/features/web/web.run.js +11 -3
- package/dist/app/kernel/access.d.ts +33 -0
- package/dist/app/kernel/access.js +44 -0
- package/dist/app/kernel/define.d.ts +14 -1
- package/dist/app/kernel/index.d.ts +1 -0
- package/dist/app/kernel/index.js +1 -0
- package/dist/app/kernel/rpc.d.ts +5 -2
- package/dist/app/registry.d.ts +4 -1
- package/dist/app/registry.js +24 -4
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/remote.d.ts +2 -0
- package/dist/cli/commands/remote.js +49 -0
- package/dist/cli/commands/sessions/reopen.js +3 -0
- package/dist/cli/commands/web.js +66 -0
- package/dist/cli/manifest.d.ts +63 -4
- package/dist/cli/manifest.js +33 -4
- package/dist/cli/menu/io-menu.d.ts +5 -3
- package/dist/cli/menu/io-menu.js +12 -5
- package/dist/cli/menu/remote-menu.d.ts +19 -0
- package/dist/cli/menu/remote-menu.js +108 -0
- package/dist/cli/menu/views.js +2 -0
- package/dist/cli/operations/handler.js +15 -1
- package/dist/cli/services/remote-client.d.ts +16 -0
- package/dist/cli/services/remote-client.js +141 -0
- package/dist/cli/services/sessions-service.d.ts +5 -0
- package/dist/cli/services/sessions-service.js +17 -1
- package/dist/core/config/defaults.js +11 -1
- package/dist/core/local-config/file.js +16 -2
- package/dist/core/local-config/overrides.js +7 -0
- package/dist/core/run/versions.js +9 -8
- package/dist/core/schema/config.d.ts +22 -3
- package/dist/core/schema/config.js +39 -2
- package/dist/core/schema/web.d.ts +51 -0
- package/dist/core/schema/web.js +46 -1
- package/dist/core/util/fs.d.ts +5 -0
- package/dist/core/util/fs.js +23 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -1
- package/dist/guest/daemon/sessions.d.ts +16 -2
- package/dist/guest/daemon/sessions.js +126 -20
- package/dist/guest/git/decision.js +7 -0
- package/dist/guest/sessions/cmdline.d.ts +14 -0
- package/dist/guest/sessions/cmdline.js +10 -0
- package/dist/guest/toolchains/setup.js +3 -0
- package/dist/host/audit/log.d.ts +3 -2
- package/dist/host/audit/log.js +3 -3
- package/dist/host/channel/channel.d.ts +5 -1
- package/dist/host/channel/channel.js +12 -0
- package/dist/host/guard/agent-files.js +7 -1
- package/dist/host/guard/commands.js +13 -1
- package/dist/host/guard/git-config.js +27 -7
- package/dist/host/guard/git-dirs.js +78 -3
- package/dist/host/guard/mark-of-the-web.js +3 -1
- package/dist/host/guard/nested-repos.js +7 -2
- package/dist/host/guard/store.js +1 -0
- package/dist/host/guard/types.d.ts +2 -0
- package/dist/host/launch/mappings.js +11 -1
- package/dist/host/launch/project-scan.js +2 -21
- package/dist/host/launch/stage.js +4 -0
- package/dist/host/life/session-ledger.js +2 -1
- package/dist/host/remote/access-store.d.ts +156 -0
- package/dist/host/remote/access-store.js +308 -0
- package/dist/host/remote/cert.d.ts +20 -0
- package/dist/host/remote/cert.js +103 -0
- package/dist/host/remote/door-client.d.ts +13 -0
- package/dist/host/remote/door-client.js +54 -0
- package/dist/host/remote/firewall.d.ts +44 -0
- package/dist/host/remote/firewall.js +118 -0
- package/dist/host/remote/gate.d.ts +125 -0
- package/dist/host/remote/gate.js +391 -0
- package/dist/host/remote/index.d.ts +8 -0
- package/dist/host/remote/index.js +10 -0
- package/dist/host/remote/lan.d.ts +24 -0
- package/dist/host/remote/lan.js +88 -0
- package/dist/host/remote/tls-store.d.ts +22 -0
- package/dist/host/remote/tls-store.js +63 -0
- package/dist/host/remote/windows-helper.d.ts +67 -0
- package/dist/host/remote/windows-helper.js +129 -0
- package/dist/host/web/bridge.d.ts +5 -1
- package/dist/host/web/bridge.js +6 -2
- package/dist/host/web/browser.js +1 -1
- package/dist/host/web/control.d.ts +4 -1
- package/dist/host/web/control.js +28 -4
- package/dist/host/web/rpc.d.ts +16 -2
- package/dist/host/web/rpc.js +37 -4
- package/dist/host/web/runner.d.ts +4 -0
- package/dist/host/web/runner.js +12 -0
- package/dist/host/web/server.d.ts +18 -2
- package/dist/host/web/server.js +97 -24
- package/dist/host/web/state.d.ts +5 -0
- package/dist/host/web/state.js +8 -0
- package/dist/runtime/agent.js +74 -3
- package/dist/runtime/daemon.js +205 -24
- package/dist/runtime/db.js +74 -3
- package/dist/runtime/gitguard.js +6 -0
- package/dist/runtime/main.js +77 -3
- package/dist/runtime/netproxy.js +74 -3
- package/dist/runtime/view.js +76 -3
- package/dist/selftest/app.test.js +41 -6
- package/dist/selftest/channel.test.js +16 -0
- package/dist/selftest/cmdline.test.js +16 -0
- package/dist/selftest/git-access.test.js +8 -0
- package/dist/selftest/hostile-input.test.js +12 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/live-daemon.test.js +25 -0
- package/dist/selftest/local-config.test.js +30 -0
- package/dist/selftest/remote.test.d.ts +2 -0
- package/dist/selftest/remote.test.js +536 -0
- package/dist/selftest/repo-guard.test.js +56 -2
- package/dist/selftest/resume.test.js +2 -1
- package/dist/selftest/sharing.test.js +9 -2
- package/dist/selftest/support/fixtures.d.ts +13 -0
- package/dist/selftest/support/web-preview.js +63 -0
- package/dist/selftest/web.test.js +2 -0
- package/dist/shared/channel/schema.d.ts +23 -0
- package/dist/shared/channel/schema.js +8 -0
- package/dist/shared/pty/protocol.d.ts +2 -0
- package/dist/shared/pty/protocol.js +2 -0
- package/dist/web/index.html +1 -1
- package/dist/web/main-Z2WYAE2Z.js +221 -0
- package/native/csharp/HostTools/Program.cs +17 -1
- package/native/csharp/HostTools/WebHelper.cs +499 -0
- package/native/scripts/get-network-profile.ps1 +19 -0
- package/native/scripts/set-folder-access.ps1 +11 -0
- package/native/scripts/set-web-firewall.ps1 +103 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +20 -0
- package/schemas/config.schema.json +65 -0
- package/schemas/sandbox-config.schema.json +0 -18
- package/dist/web/main-UD4QCXZR.js +0 -219
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// hide-window --wait S hides the sandbox window once it appears; prints the hidden handles as JSON
|
|
7
7
|
// show-window --handles a,b shows those windows (else any sandbox window); prints how many
|
|
8
8
|
// user-path --add|--remove D D first on, or off, your PATH; exit 3 when PATH would be too long
|
|
9
|
+
// web-helper --pipe P --server-pid N Windows sign-in for the web UI (WebHelper.cs), until stdin closes
|
|
10
|
+
// door-client --owner USER another user's side of that sign-in: prints the answer
|
|
9
11
|
using System;
|
|
10
12
|
using System.Collections.Generic;
|
|
11
13
|
using System.Diagnostics;
|
|
@@ -96,6 +98,20 @@ static class Program
|
|
|
96
98
|
Console.Error.WriteLine(e.Message);
|
|
97
99
|
return 1;
|
|
98
100
|
}
|
|
101
|
+
case "web-helper":
|
|
102
|
+
int serverPid;
|
|
103
|
+
string pipeName;
|
|
104
|
+
if (!options.TryGetValue("pipe", out pipeName) || !options.TryGetValue("server-pid", out value) || !int.TryParse(value, out serverPid))
|
|
105
|
+
{
|
|
106
|
+
return Usage();
|
|
107
|
+
}
|
|
108
|
+
return AgentSandbox.WebHelper.Serve(pipeName, serverPid);
|
|
109
|
+
case "door-client":
|
|
110
|
+
if (!options.TryGetValue("owner", out value))
|
|
111
|
+
{
|
|
112
|
+
return Usage();
|
|
113
|
+
}
|
|
114
|
+
return AgentSandbox.WebHelper.Client(value);
|
|
99
115
|
default:
|
|
100
116
|
return Usage();
|
|
101
117
|
}
|
|
@@ -103,7 +119,7 @@ static class Program
|
|
|
103
119
|
|
|
104
120
|
static int Usage()
|
|
105
121
|
{
|
|
106
|
-
Console.Error.WriteLine("Usage: HostTools keep-awake --owner-pid N | stop-events --since <ISO time> | memory-status | hide-window --wait S | show-window [--handles a,b] | user-path --add|--remove <folder>");
|
|
122
|
+
Console.Error.WriteLine("Usage: HostTools keep-awake --owner-pid N | stop-events --since <ISO time> | memory-status | hide-window --wait S | show-window [--handles a,b] | user-path --add|--remove <folder> | web-helper --pipe P --server-pid N | door-client --owner USER");
|
|
107
123
|
return 2;
|
|
108
124
|
}
|
|
109
125
|
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// Windows sign-in for the web UI (docs/design/remote-access.md). C# 5.
|
|
2
|
+
// web-helper --pipe NAME --server-pid N long-running, for the owner's web server: serves the pipe NAME (one client
|
|
3
|
+
// at a time) and asks Windows who connected (the client's token: user SID, integrity, AppContainer), writing
|
|
4
|
+
// "door\t<n>\t<sid>\t<integrity>\t<appcontainer 0|1>\t<DOMAIN\user>" to stdout and writing the server's answer
|
|
5
|
+
// ("answer\t<n>\t<text>" on stdin) back down the pipe. Requests on stdin, one per line, tab-separated:
|
|
6
|
+
// peer <id> <local address> <local port> <remote address> <remote port>
|
|
7
|
+
// the Windows user of the process at the other end of that loopback TCP connection (our server's end is
|
|
8
|
+
// local): "peer\t<id>\tok\t<sid>\t<DOMAIN\user>" or "peer\t<id>\tnone\t<reason>"
|
|
9
|
+
// lookup <id> <name> "lookup\t<id>\tok\t<sid>\t<DOMAIN\user>" or "lookup\t<id>\tnone"
|
|
10
|
+
// Ends when stdin closes.
|
|
11
|
+
// door-client --owner USER for another user's sandbox remote: USER (a name or a SID) is the owner; connects to
|
|
12
|
+
// agent-sandbox-web-<their SID>, checks that the pipe's server runs in a session of that SID (so nobody else can
|
|
13
|
+
// squat the name), sends "hello", prints the answer line. Exit 3: the pipe is not the owner's; 4: no such pipe;
|
|
14
|
+
// 5: no such user.
|
|
15
|
+
using System;
|
|
16
|
+
using System.ComponentModel;
|
|
17
|
+
using System.Diagnostics;
|
|
18
|
+
using System.IO;
|
|
19
|
+
using System.IO.Pipes;
|
|
20
|
+
using System.Net;
|
|
21
|
+
using System.Runtime.InteropServices;
|
|
22
|
+
using System.Security.AccessControl;
|
|
23
|
+
using System.Security.Principal;
|
|
24
|
+
using System.Text;
|
|
25
|
+
using System.Threading;
|
|
26
|
+
|
|
27
|
+
namespace AgentSandbox
|
|
28
|
+
{
|
|
29
|
+
public static class WebHelper
|
|
30
|
+
{
|
|
31
|
+
private const int AfInet = 2;
|
|
32
|
+
private const int TcpTableOwnerPidConnections = 4;
|
|
33
|
+
private const int ErrorInsufficientBuffer = 122;
|
|
34
|
+
private const int StateEstablished = 5;
|
|
35
|
+
private const int WtsUserName = 5;
|
|
36
|
+
private const int WtsDomainName = 7;
|
|
37
|
+
private const int TokenIntegrityLevel = 25;
|
|
38
|
+
private const int TokenIsAppContainer = 29;
|
|
39
|
+
private const int AnswerTimeoutMs = 10000;
|
|
40
|
+
private const int MaxLine = 512;
|
|
41
|
+
public const int NotOwner = 3;
|
|
42
|
+
public const int NoPipe = 4;
|
|
43
|
+
public const int NoUser = 5;
|
|
44
|
+
public const string PipePrefix = "agent-sandbox-web-";
|
|
45
|
+
|
|
46
|
+
private static readonly object StdoutLock = new object();
|
|
47
|
+
private static readonly object AnswerLock = new object();
|
|
48
|
+
private static string pendingAnswer;
|
|
49
|
+
private static int pendingNumber;
|
|
50
|
+
|
|
51
|
+
private static class NativeMethods
|
|
52
|
+
{
|
|
53
|
+
[DllImport("iphlpapi.dll", SetLastError = true)]
|
|
54
|
+
public static extern int GetExtendedTcpTable(IntPtr table, ref int size, bool order, int family, int tableClass, int reserved);
|
|
55
|
+
|
|
56
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
57
|
+
public static extern bool ProcessIdToSessionId(uint processId, out uint sessionId);
|
|
58
|
+
|
|
59
|
+
[DllImport("wtsapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
60
|
+
public static extern bool WTSQuerySessionInformation(IntPtr server, uint sessionId, int infoClass, out IntPtr buffer, out int bytes);
|
|
61
|
+
|
|
62
|
+
[DllImport("wtsapi32.dll")]
|
|
63
|
+
public static extern void WTSFreeMemory(IntPtr memory);
|
|
64
|
+
|
|
65
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
66
|
+
public static extern bool GetTokenInformation(IntPtr token, int infoClass, IntPtr info, int length, out int returned);
|
|
67
|
+
|
|
68
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
69
|
+
public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
|
|
70
|
+
|
|
71
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
72
|
+
public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index);
|
|
73
|
+
|
|
74
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
75
|
+
public static extern bool GetNamedPipeServerProcessId(IntPtr pipe, out uint serverProcessId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private static void Emit(string line)
|
|
79
|
+
{
|
|
80
|
+
lock (StdoutLock)
|
|
81
|
+
{
|
|
82
|
+
Console.Out.WriteLine(line);
|
|
83
|
+
Console.Out.Flush();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static string Clean(string text)
|
|
88
|
+
{
|
|
89
|
+
StringBuilder b = new StringBuilder();
|
|
90
|
+
foreach (char c in text ?? "")
|
|
91
|
+
{
|
|
92
|
+
if (c >= ' ' && c != '\t' && c != '\u007f')
|
|
93
|
+
{
|
|
94
|
+
b.Append(c);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return b.ToString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// <summary>Serves the pipe and answers requests until stdin closes.</summary>
|
|
101
|
+
public static int Serve(string pipeName, int serverPid)
|
|
102
|
+
{
|
|
103
|
+
// An empty name: no Windows sign-in, only the peer checks and lookups.
|
|
104
|
+
if (pipeName.Length > 0)
|
|
105
|
+
{
|
|
106
|
+
Thread door = new Thread(delegate () { DoorLoop(pipeName); });
|
|
107
|
+
door.IsBackground = true;
|
|
108
|
+
door.Start();
|
|
109
|
+
}
|
|
110
|
+
string line;
|
|
111
|
+
while ((line = Console.In.ReadLine()) != null)
|
|
112
|
+
{
|
|
113
|
+
if (line.Length > MaxLine)
|
|
114
|
+
{
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
string[] f = line.Split('\t');
|
|
118
|
+
try
|
|
119
|
+
{
|
|
120
|
+
if (f[0] == "peer" && f.Length == 6)
|
|
121
|
+
{
|
|
122
|
+
Emit(Peer(f[1], f[2], f[3], f[4], f[5], serverPid));
|
|
123
|
+
}
|
|
124
|
+
else if (f[0] == "lookup" && f.Length == 3)
|
|
125
|
+
{
|
|
126
|
+
Emit(Lookup(f[1], f[2]));
|
|
127
|
+
}
|
|
128
|
+
else if (f[0] == "answer" && f.Length == 3)
|
|
129
|
+
{
|
|
130
|
+
int n;
|
|
131
|
+
if (int.TryParse(f[1], out n))
|
|
132
|
+
{
|
|
133
|
+
lock (AnswerLock)
|
|
134
|
+
{
|
|
135
|
+
if (n == pendingNumber)
|
|
136
|
+
{
|
|
137
|
+
pendingAnswer = f[2];
|
|
138
|
+
Monitor.PulseAll(AnswerLock);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (Exception e)
|
|
145
|
+
{
|
|
146
|
+
Emit("error\t" + Clean(f.Length > 1 ? f[1] : "") + "\t" + Clean(e.Message));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private static PipeSecurity Security()
|
|
153
|
+
{
|
|
154
|
+
PipeSecurity security = new PipeSecurity();
|
|
155
|
+
SecurityIdentifier me = WindowsIdentity.GetCurrent().User;
|
|
156
|
+
security.AddAccessRule(new PipeAccessRule(me, PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
157
|
+
security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
158
|
+
security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
|
159
|
+
return security;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static void DoorLoop(string pipeName)
|
|
163
|
+
{
|
|
164
|
+
int number = 0;
|
|
165
|
+
while (true)
|
|
166
|
+
{
|
|
167
|
+
NamedPipeServerStream pipe;
|
|
168
|
+
try
|
|
169
|
+
{
|
|
170
|
+
// One instance: if someone else made this name first, this fails and sign-in stays off.
|
|
171
|
+
pipe = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 1024, 1024, Security());
|
|
172
|
+
}
|
|
173
|
+
catch (Exception e)
|
|
174
|
+
{
|
|
175
|
+
Emit("door-failed\t" + Clean(e.Message));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
pipe.WaitForConnection();
|
|
181
|
+
number++;
|
|
182
|
+
string sid = "";
|
|
183
|
+
string user = "";
|
|
184
|
+
int integrity = 0;
|
|
185
|
+
bool container = true;
|
|
186
|
+
pipe.RunAsClient(delegate ()
|
|
187
|
+
{
|
|
188
|
+
using (WindowsIdentity client = WindowsIdentity.GetCurrent(TokenAccessLevels.Query))
|
|
189
|
+
{
|
|
190
|
+
sid = client.User == null ? "" : client.User.Value;
|
|
191
|
+
user = client.Name;
|
|
192
|
+
integrity = Integrity(client.Token);
|
|
193
|
+
container = IsAppContainer(client.Token);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
StreamReader reader = new StreamReader(pipe, Encoding.UTF8, false, 1024, true);
|
|
197
|
+
string hello = ReadBounded(reader);
|
|
198
|
+
string answer = "refused\tNo answer.";
|
|
199
|
+
if (hello == "hello")
|
|
200
|
+
{
|
|
201
|
+
lock (AnswerLock)
|
|
202
|
+
{
|
|
203
|
+
pendingNumber = number;
|
|
204
|
+
pendingAnswer = null;
|
|
205
|
+
Emit("door\t" + number + "\t" + sid + "\t" + integrity + "\t" + (container ? "1" : "0") + "\t" + Clean(user));
|
|
206
|
+
Monitor.Wait(AnswerLock, AnswerTimeoutMs);
|
|
207
|
+
if (pendingAnswer != null)
|
|
208
|
+
{
|
|
209
|
+
answer = pendingAnswer;
|
|
210
|
+
}
|
|
211
|
+
pendingNumber = 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
byte[] bytes = Encoding.UTF8.GetBytes(Clean(answer.Replace('\t', ' ')) + "\n");
|
|
215
|
+
pipe.Write(bytes, 0, bytes.Length);
|
|
216
|
+
pipe.Flush();
|
|
217
|
+
pipe.WaitForPipeDrain();
|
|
218
|
+
}
|
|
219
|
+
catch (Exception)
|
|
220
|
+
{
|
|
221
|
+
// A client that went away or sent nonsense: next one.
|
|
222
|
+
}
|
|
223
|
+
finally
|
|
224
|
+
{
|
|
225
|
+
pipe.Dispose();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private static string ReadBounded(StreamReader reader)
|
|
231
|
+
{
|
|
232
|
+
StringBuilder b = new StringBuilder();
|
|
233
|
+
int c;
|
|
234
|
+
while (b.Length < MaxLine && (c = reader.Read()) >= 0 && c != '\n')
|
|
235
|
+
{
|
|
236
|
+
if (c != '\r')
|
|
237
|
+
{
|
|
238
|
+
b.Append((char)c);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return b.ToString();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static int Integrity(IntPtr token)
|
|
245
|
+
{
|
|
246
|
+
int size;
|
|
247
|
+
NativeMethods.GetTokenInformation(token, TokenIntegrityLevel, IntPtr.Zero, 0, out size);
|
|
248
|
+
if (size <= 0)
|
|
249
|
+
{
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
IntPtr info = Marshal.AllocHGlobal(size);
|
|
253
|
+
try
|
|
254
|
+
{
|
|
255
|
+
if (!NativeMethods.GetTokenInformation(token, TokenIntegrityLevel, info, size, out size))
|
|
256
|
+
{
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
IntPtr sid = Marshal.ReadIntPtr(info);
|
|
260
|
+
int count = Marshal.ReadByte(NativeMethods.GetSidSubAuthorityCount(sid));
|
|
261
|
+
return count == 0 ? 0 : Marshal.ReadInt32(NativeMethods.GetSidSubAuthority(sid, (uint)(count - 1)));
|
|
262
|
+
}
|
|
263
|
+
finally
|
|
264
|
+
{
|
|
265
|
+
Marshal.FreeHGlobal(info);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static bool IsAppContainer(IntPtr token)
|
|
270
|
+
{
|
|
271
|
+
IntPtr info = Marshal.AllocHGlobal(4);
|
|
272
|
+
try
|
|
273
|
+
{
|
|
274
|
+
int returned;
|
|
275
|
+
// A token that cannot be asked counts as an AppContainer: refused.
|
|
276
|
+
return !NativeMethods.GetTokenInformation(token, TokenIsAppContainer, info, 4, out returned) || Marshal.ReadInt32(info) != 0;
|
|
277
|
+
}
|
|
278
|
+
finally
|
|
279
|
+
{
|
|
280
|
+
Marshal.FreeHGlobal(info);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private struct Row
|
|
285
|
+
{
|
|
286
|
+
public int State;
|
|
287
|
+
public uint LocalAddr;
|
|
288
|
+
public int LocalPort;
|
|
289
|
+
public uint RemoteAddr;
|
|
290
|
+
public int RemotePort;
|
|
291
|
+
public uint Pid;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private static Row[] TcpTable()
|
|
295
|
+
{
|
|
296
|
+
int size = 0;
|
|
297
|
+
NativeMethods.GetExtendedTcpTable(IntPtr.Zero, ref size, false, AfInet, TcpTableOwnerPidConnections, 0);
|
|
298
|
+
for (int attempt = 0; attempt < 5; attempt++)
|
|
299
|
+
{
|
|
300
|
+
IntPtr buffer = Marshal.AllocHGlobal(size);
|
|
301
|
+
try
|
|
302
|
+
{
|
|
303
|
+
int result = NativeMethods.GetExtendedTcpTable(buffer, ref size, false, AfInet, TcpTableOwnerPidConnections, 0);
|
|
304
|
+
if (result == ErrorInsufficientBuffer)
|
|
305
|
+
{
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (result != 0)
|
|
309
|
+
{
|
|
310
|
+
throw new Win32Exception(result);
|
|
311
|
+
}
|
|
312
|
+
int count = Marshal.ReadInt32(buffer);
|
|
313
|
+
Row[] rows = new Row[count];
|
|
314
|
+
for (int i = 0; i < count; i++)
|
|
315
|
+
{
|
|
316
|
+
IntPtr at = buffer + 4 + i * 24;
|
|
317
|
+
rows[i].State = Marshal.ReadInt32(at);
|
|
318
|
+
rows[i].LocalAddr = (uint)Marshal.ReadInt32(at + 4);
|
|
319
|
+
rows[i].LocalPort = Port(Marshal.ReadInt32(at + 8));
|
|
320
|
+
rows[i].RemoteAddr = (uint)Marshal.ReadInt32(at + 12);
|
|
321
|
+
rows[i].RemotePort = Port(Marshal.ReadInt32(at + 16));
|
|
322
|
+
rows[i].Pid = (uint)Marshal.ReadInt32(at + 20);
|
|
323
|
+
}
|
|
324
|
+
return rows;
|
|
325
|
+
}
|
|
326
|
+
finally
|
|
327
|
+
{
|
|
328
|
+
Marshal.FreeHGlobal(buffer);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw new InvalidOperationException("The TCP table kept growing.");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private static int Port(int raw)
|
|
335
|
+
{
|
|
336
|
+
return ((raw & 0xff) << 8) | ((raw >> 8) & 0xff);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private static uint Address(string text)
|
|
340
|
+
{
|
|
341
|
+
IPAddress ip = IPAddress.Parse(text);
|
|
342
|
+
byte[] b = ip.GetAddressBytes();
|
|
343
|
+
if (b.Length != 4)
|
|
344
|
+
{
|
|
345
|
+
throw new ArgumentException("IPv4 only.");
|
|
346
|
+
}
|
|
347
|
+
return BitConverter.ToUInt32(b, 0);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// <summary>The process at the client end, read twice around the session lookup (same pid, still established).</summary>
|
|
351
|
+
private static string Peer(string id, string localAddr, string localPort, string remoteAddr, string remotePort, int serverPid)
|
|
352
|
+
{
|
|
353
|
+
uint la = Address(localAddr);
|
|
354
|
+
uint ra = Address(remoteAddr);
|
|
355
|
+
int lp = int.Parse(localPort);
|
|
356
|
+
int rp = int.Parse(remotePort);
|
|
357
|
+
uint pid;
|
|
358
|
+
string why = ClientPid(la, lp, ra, rp, serverPid, out pid);
|
|
359
|
+
if (why != null)
|
|
360
|
+
{
|
|
361
|
+
return "peer\t" + id + "\tnone\t" + why;
|
|
362
|
+
}
|
|
363
|
+
uint session;
|
|
364
|
+
if (!NativeMethods.ProcessIdToSessionId(pid, out session))
|
|
365
|
+
{
|
|
366
|
+
return "peer\t" + id + "\tnone\tno session";
|
|
367
|
+
}
|
|
368
|
+
if (session == 0)
|
|
369
|
+
{
|
|
370
|
+
return "peer\t" + id + "\tnone\tservice session";
|
|
371
|
+
}
|
|
372
|
+
string account = SessionUser(session);
|
|
373
|
+
uint again;
|
|
374
|
+
if (account == null || ClientPid(la, lp, ra, rp, serverPid, out again) != null || again != pid)
|
|
375
|
+
{
|
|
376
|
+
return "peer\t" + id + "\tnone\tchanged while checking";
|
|
377
|
+
}
|
|
378
|
+
string sid = SidOf(account);
|
|
379
|
+
return sid == null ? "peer\t" + id + "\tnone\tunknown user" : "peer\t" + id + "\tok\t" + sid + "\t" + Clean(account);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private static string ClientPid(uint la, int lp, uint ra, int rp, int serverPid, out uint pid)
|
|
383
|
+
{
|
|
384
|
+
pid = 0;
|
|
385
|
+
bool ours = false;
|
|
386
|
+
foreach (Row r in TcpTable())
|
|
387
|
+
{
|
|
388
|
+
if (r.LocalAddr == la && r.LocalPort == lp && r.RemoteAddr == ra && r.RemotePort == rp)
|
|
389
|
+
{
|
|
390
|
+
ours = r.Pid == (uint)serverPid && r.State == StateEstablished;
|
|
391
|
+
}
|
|
392
|
+
if (r.LocalAddr == ra && r.LocalPort == rp && r.RemoteAddr == la && r.RemotePort == lp && r.State == StateEstablished)
|
|
393
|
+
{
|
|
394
|
+
pid = r.Pid;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (!ours)
|
|
398
|
+
{
|
|
399
|
+
return "not our connection";
|
|
400
|
+
}
|
|
401
|
+
return pid == 0 ? "no such connection" : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private static string SessionUser(uint session)
|
|
405
|
+
{
|
|
406
|
+
string user = SessionText(session, WtsUserName);
|
|
407
|
+
string domain = SessionText(session, WtsDomainName);
|
|
408
|
+
if (string.IsNullOrEmpty(user))
|
|
409
|
+
{
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
return string.IsNullOrEmpty(domain) ? user : domain + "\\" + user;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private static string SessionText(uint session, int infoClass)
|
|
416
|
+
{
|
|
417
|
+
IntPtr buffer;
|
|
418
|
+
int bytes;
|
|
419
|
+
if (!NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, session, infoClass, out buffer, out bytes))
|
|
420
|
+
{
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
try
|
|
424
|
+
{
|
|
425
|
+
return Marshal.PtrToStringUni(buffer);
|
|
426
|
+
}
|
|
427
|
+
finally
|
|
428
|
+
{
|
|
429
|
+
NativeMethods.WTSFreeMemory(buffer);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private static string SidOf(string account)
|
|
434
|
+
{
|
|
435
|
+
try
|
|
436
|
+
{
|
|
437
|
+
return ((SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier))).Value;
|
|
438
|
+
}
|
|
439
|
+
catch (IdentityNotMappedException)
|
|
440
|
+
{
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private static string Lookup(string id, string name)
|
|
446
|
+
{
|
|
447
|
+
string sid = SidOf(name);
|
|
448
|
+
if (sid == null)
|
|
449
|
+
{
|
|
450
|
+
return "lookup\t" + id + "\tnone";
|
|
451
|
+
}
|
|
452
|
+
string full = ((NTAccount)new SecurityIdentifier(sid).Translate(typeof(NTAccount))).Value;
|
|
453
|
+
return "lookup\t" + id + "\tok\t" + sid + "\t" + Clean(full);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/// <summary>The other user's side: the pipe must be served from a session of the owner; prints the answer.</summary>
|
|
457
|
+
public static int Client(string owner)
|
|
458
|
+
{
|
|
459
|
+
string ownerSid = owner.StartsWith("S-1-", StringComparison.OrdinalIgnoreCase) ? owner : SidOf(owner);
|
|
460
|
+
if (ownerSid == null)
|
|
461
|
+
{
|
|
462
|
+
return NoUser;
|
|
463
|
+
}
|
|
464
|
+
string pipeName = PipePrefix + ownerSid;
|
|
465
|
+
using (NamedPipeClientStream pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Identification))
|
|
466
|
+
{
|
|
467
|
+
try
|
|
468
|
+
{
|
|
469
|
+
pipe.Connect(3000);
|
|
470
|
+
}
|
|
471
|
+
catch (TimeoutException)
|
|
472
|
+
{
|
|
473
|
+
return NoPipe;
|
|
474
|
+
}
|
|
475
|
+
catch (IOException)
|
|
476
|
+
{
|
|
477
|
+
return NoPipe;
|
|
478
|
+
}
|
|
479
|
+
uint serverPid;
|
|
480
|
+
uint session;
|
|
481
|
+
if (!NativeMethods.GetNamedPipeServerProcessId(pipe.SafePipeHandle.DangerousGetHandle(), out serverPid) || !NativeMethods.ProcessIdToSessionId(serverPid, out session))
|
|
482
|
+
{
|
|
483
|
+
return NotOwner;
|
|
484
|
+
}
|
|
485
|
+
string account = SessionUser(session);
|
|
486
|
+
if (account == null || !string.Equals(SidOf(account), ownerSid, StringComparison.OrdinalIgnoreCase))
|
|
487
|
+
{
|
|
488
|
+
return NotOwner;
|
|
489
|
+
}
|
|
490
|
+
byte[] hello = Encoding.UTF8.GetBytes("hello\n");
|
|
491
|
+
pipe.Write(hello, 0, hello.Length);
|
|
492
|
+
pipe.Flush();
|
|
493
|
+
StreamReader reader = new StreamReader(pipe, Encoding.UTF8);
|
|
494
|
+
Console.Out.Write(Clean(ReadBounded(reader)));
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# The network an address of this PC is on, for the web UI's LAN mode: its category (Private, Public, DomainAuthenticated)
|
|
3
|
+
# and name, as one line of JSON. LAN mode starts only on a Private network, and only the one it was turned on for.
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$Address
|
|
6
|
+
)
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
if ($Address -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { throw 'An IPv4 address, please.' }
|
|
9
|
+
$ip = Get-NetIPAddress -IPAddress $Address -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
10
|
+
if (-not $ip) {
|
|
11
|
+
@{ found = $false; category = ''; name = '' } | ConvertTo-Json -Compress
|
|
12
|
+
exit 0
|
|
13
|
+
}
|
|
14
|
+
$conn = Get-NetConnectionProfile -InterfaceIndex $ip.InterfaceIndex -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
15
|
+
if (-not $conn) {
|
|
16
|
+
@{ found = $true; category = ''; name = '' } | ConvertTo-Json -Compress
|
|
17
|
+
exit 0
|
|
18
|
+
}
|
|
19
|
+
@{ found = $true; category = [string]$conn.NetworkCategory; name = [string]$conn.Name } | ConvertTo-Json -Compress
|
|
@@ -50,6 +50,17 @@ try {
|
|
|
50
50
|
$who, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
|
|
51
51
|
}
|
|
52
52
|
$have = Get-Acl -LiteralPath $e.path
|
|
53
|
+
# A folder someone else owns is never made "private" for you: under an all-users install another member of
|
|
54
|
+
# the group could have made %ProgramData%\AgentSandbox\users\<you> first, and as its owner can give
|
|
55
|
+
# themselves access to every file you write later. Administrators own what an elevated you creates.
|
|
56
|
+
$owner = $have.GetOwner([System.Security.Principal.SecurityIdentifier])
|
|
57
|
+
$adminsSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
|
|
58
|
+
$elevated = (New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole($adminsSid)
|
|
59
|
+
if (-not ($owner -eq $me -or ($owner -eq $adminsSid -and $elevated))) {
|
|
60
|
+
$ownerName = $owner.Value
|
|
61
|
+
try { $ownerName = $owner.Translate([System.Security.Principal.NTAccount]).Value } catch { $ownerName = $owner.Value }
|
|
62
|
+
throw "$($e.path) belongs to $ownerName, not to you (someone else may have created it). An administrator must delete it before you use it."
|
|
63
|
+
}
|
|
53
64
|
if ($have.GetSecurityDescriptorSddlForm('Access') -eq $want.GetSecurityDescriptorSddlForm('Access')) { continue }
|
|
54
65
|
$children = @(Get-ChildItem -LiteralPath $e.path -Force | Select-Object -First 1)
|
|
55
66
|
if ($children.Count -eq 0) { Set-OwnDacl $e.path $want } else { Set-Acl -LiteralPath $e.path -AclObject $want }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Adds or removes the one Windows Firewall rule of the web UI's LAN mode (docs/design/remote-access.md). Run elevated
|
|
3
|
+
# through run-elevated.ps1 (one Windows permission prompt). The request is a JSON file:
|
|
4
|
+
# { action: "add" | "remove", ruleName, port, localAddress, program }
|
|
5
|
+
# Everything is checked again here, from the bytes whose SHA-256 was given: the rule name is exactly ours, the port is
|
|
6
|
+
# 1024-65535, the local address is a private IPv4 address (10/8, 172.16/12, 192.168/16), the program is an existing
|
|
7
|
+
# node.exe not in a temp folder. The rule only allows inbound TCP to that address and port, from the local subnet, on
|
|
8
|
+
# Private networks: never the internet. -DryRun checks and prints what it would run (as JSON) and changes nothing; a
|
|
9
|
+
# real run writes result.json next to the request file.
|
|
10
|
+
param(
|
|
11
|
+
[Parameter(Mandatory = $true)][string]$Json,
|
|
12
|
+
[string]$Sha256 = '',
|
|
13
|
+
[switch]$DryRun
|
|
14
|
+
)
|
|
15
|
+
$ErrorActionPreference = 'Stop'
|
|
16
|
+
$RuleName = 'Agent Sandbox web UI (LAN)'
|
|
17
|
+
$result = @{ ok = $false; message = ''; commands = @() }
|
|
18
|
+
|
|
19
|
+
function Test-PrivateIPv4([string]$Ip) {
|
|
20
|
+
if ($Ip -notmatch '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$') { return $false }
|
|
21
|
+
$p = @([int]$Matches[1], [int]$Matches[2], [int]$Matches[3], [int]$Matches[4])
|
|
22
|
+
foreach ($n in $p) { if ($n -gt 255) { return $false } }
|
|
23
|
+
return ($p[0] -eq 10) -or ($p[0] -eq 172 -and $p[1] -ge 16 -and $p[1] -le 31) -or ($p[0] -eq 192 -and $p[1] -eq 168)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Read-Request([string]$Path, [string]$Expected) {
|
|
27
|
+
$bytes = [IO.File]::ReadAllBytes($Path)
|
|
28
|
+
if ($bytes.Length -gt 4096) { throw 'The request file is too big.' }
|
|
29
|
+
if ($Expected) {
|
|
30
|
+
$sha = [Security.Cryptography.SHA256]::Create()
|
|
31
|
+
$hash = -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') })
|
|
32
|
+
if ($hash -ine $Expected) { throw 'The request file changed after it was written; nothing was done.' }
|
|
33
|
+
}
|
|
34
|
+
return ([Text.Encoding]::UTF8.GetString($bytes) | ConvertFrom-Json)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Assert-Request($Req) {
|
|
38
|
+
foreach ($name in @('action', 'ruleName')) {
|
|
39
|
+
if (-not ($Req.PSObject.Properties.Name -contains $name)) { throw "The request has no $name." }
|
|
40
|
+
}
|
|
41
|
+
if ($Req.ruleName -cne $RuleName) { throw "Only the rule '$RuleName' is managed here." }
|
|
42
|
+
if ($Req.action -ne 'add' -and $Req.action -ne 'remove') { throw 'The action must be add or remove.' }
|
|
43
|
+
if ($Req.action -eq 'remove') { return }
|
|
44
|
+
foreach ($name in @('port', 'localAddress', 'program')) {
|
|
45
|
+
if (-not ($Req.PSObject.Properties.Name -contains $name)) { throw "The request has no $name." }
|
|
46
|
+
}
|
|
47
|
+
if (-not ($Req.port -is [int] -or $Req.port -is [long]) -or $Req.port -lt 1024 -or $Req.port -gt 65535) {
|
|
48
|
+
throw 'The port must be a whole number from 1024 to 65535.'
|
|
49
|
+
}
|
|
50
|
+
if (-not (Test-PrivateIPv4 ([string]$Req.localAddress))) {
|
|
51
|
+
throw 'The address must be a private IPv4 address (10.x, 172.16-31.x, 192.168.x): LAN mode is for your own network only.'
|
|
52
|
+
}
|
|
53
|
+
$program = [string]$Req.program
|
|
54
|
+
if (-not [IO.Path]::IsPathRooted($program) -or $program.Contains('..')) { throw 'The program must be a full path.' }
|
|
55
|
+
if ([IO.Path]::GetFileName($program) -ine 'node.exe') { throw 'The program must be node.exe.' }
|
|
56
|
+
if (-not (Test-Path -LiteralPath $program -PathType Leaf)) { throw "The program does not exist: $program" }
|
|
57
|
+
$full = [IO.Path]::GetFullPath($program)
|
|
58
|
+
foreach ($temp in @($env:TEMP, $env:TMP, (Join-Path $env:SystemRoot 'Temp'))) {
|
|
59
|
+
if ($temp -and $full.StartsWith([IO.Path]::GetFullPath($temp).TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
|
60
|
+
throw 'The program must not be in a temp folder.'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Get-Commands($Req) {
|
|
66
|
+
$remove = "Remove-NetFirewallRule -DisplayName '$RuleName'"
|
|
67
|
+
if ($Req.action -eq 'remove') { return @($remove) }
|
|
68
|
+
$add = "New-NetFirewallRule -DisplayName '$RuleName' -Direction Inbound -Action Allow -Protocol TCP " +
|
|
69
|
+
"-LocalPort $($Req.port) -LocalAddress $($Req.localAddress) -RemoteAddress LocalSubnet -Profile Private " +
|
|
70
|
+
"-Program '$([IO.Path]::GetFullPath([string]$Req.program))'"
|
|
71
|
+
return @($remove, $add)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
$request = Read-Request $Json $Sha256
|
|
76
|
+
Assert-Request $request
|
|
77
|
+
$result.commands = @(Get-Commands $request)
|
|
78
|
+
if ($DryRun) {
|
|
79
|
+
$result.ok = $true
|
|
80
|
+
$result.message = 'Checked; nothing was changed.'
|
|
81
|
+
$result | ConvertTo-Json -Compress
|
|
82
|
+
exit 0
|
|
83
|
+
}
|
|
84
|
+
Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
85
|
+
if ($request.action -eq 'add') {
|
|
86
|
+
New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP `
|
|
87
|
+
-LocalPort ([int]$request.port) -LocalAddress ([string]$request.localAddress) -RemoteAddress LocalSubnet `
|
|
88
|
+
-Profile Private -Program ([IO.Path]::GetFullPath([string]$request.program)) | Out-Null
|
|
89
|
+
$result.message = 'The firewall rule is in place.'
|
|
90
|
+
} else {
|
|
91
|
+
$result.message = 'The firewall rule is removed.'
|
|
92
|
+
}
|
|
93
|
+
$result.ok = $true
|
|
94
|
+
} catch {
|
|
95
|
+
$result.ok = $false
|
|
96
|
+
$result.message = $_.Exception.Message
|
|
97
|
+
if ($DryRun) {
|
|
98
|
+
$result | ConvertTo-Json -Compress
|
|
99
|
+
exit 2
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
$result | ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path (Split-Path -Parent $Json) 'result.json') -Encoding UTF8
|
|
103
|
+
if ($result.ok) { exit 0 } else { exit 2 }
|