Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 11f71fe

Browse filesBrowse files
authored
Automatic Port Selection and Secured Communication (#1038)
Adds dynamic OS-selected socket port handling and secured Electron/.NET communication. The dotnet-first startup flow now generates the auth token on the .NET side and passes it to Electron through an environment variable. Electron reports the selected socket port through a temporary startup-info file, avoiding any dependency on parsing Electron console output. Also avoids logging backend startup parameters because they include the auth token.
1 parent 1e8b026 commit 11f71fe
Copy full SHA for 11f71fe

21 files changed

+528-123Lines changed: 528 additions & 123 deletions
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/ElectronNET.API/Bridge/SocketIOConnection.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/Bridge/SocketIOConnection.cs
+11-2Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@
33
namespace ElectronNET.API;
44

55
using System;
6+
using System.Collections.Generic;
67
using System.Threading.Tasks;
78
using ElectronNET.API.Serialization;
89
using SocketIO.Serializer.SystemTextJson;
910
using SocketIO = SocketIOClient.SocketIO;
11+
using SocketIOOptions = SocketIOClient.SocketIOOptions;
1012

1113
internal class SocketIOConnection : ISocketConnection
1214
{
1315
private readonly SocketIO _socket;
1416
private readonly object _lockObj = new object();
1517
private bool _isDisposed;
1618

17-
public SocketIOConnection(string uri)
19+
public SocketIOConnection(string uri, string authorization)
1820
{
19-
_socket = new SocketIO(uri);
21+
var opts = string.IsNullOrEmpty(authorization) ? new SocketIOOptions() : new SocketIOOptions
22+
{
23+
ExtraHeaders = new Dictionary<string, string>
24+
{
25+
["authorization"] = authorization
26+
},
27+
};
28+
_socket = new SocketIO(uri, opts);
2029
_socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options);
2130
// Use default System.Text.Json serializer from SocketIOClient.
2231
// Outgoing args are normalized to camelCase via SerializeArg in Emit.
Collapse file

‎src/ElectronNET.API/Common/ProcessRunner.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/Common/ProcessRunner.cs
+17Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace ElectronNET.Common
22
{
33
using System;
4+
using System.Collections.Generic;
45
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
67
using System.Text;
@@ -26,6 +27,8 @@ public class ProcessRunner : IDisposable
2627
private readonly StringBuilder stdOut = new StringBuilder(4 * 1024);
2728
private readonly StringBuilder stdErr = new StringBuilder(4 * 1024);
2829

30+
public event EventHandler<string> LineReceived;
31+
2932
private volatile ManualResetEvent stdOutEvent;
3033
private volatile ManualResetEvent stdErrEvent;
3134
private volatile Stopwatch stopwatch;
@@ -109,6 +112,11 @@ public string StandardError
109112
public int? LastExitCode { get; private set; }
110113

111114
public bool Run(string exeFileName, string commandLineArgs, string workingDirectory)
115+
{
116+
return this.Run(exeFileName, commandLineArgs, workingDirectory, null);
117+
}
118+
119+
public bool Run(string exeFileName, string commandLineArgs, string workingDirectory, IDictionary<string, string> environmentVariables)
112120
{
113121
this.CommandLine = commandLineArgs;
114122
this.WorkingFolder = workingDirectory;
@@ -126,6 +134,14 @@ public bool Run(string exeFileName, string commandLineArgs, string workingDirect
126134
WorkingDirectory = workingDirectory
127135
};
128136

137+
if (environmentVariables != null)
138+
{
139+
foreach (var kv in environmentVariables)
140+
{
141+
startInfo.EnvironmentVariables[kv.Key] = kv.Value;
142+
}
143+
}
144+
129145
return this.Run(startInfo);
130146
}
131147

@@ -571,6 +587,7 @@ private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
571587
if (e.Data != null)
572588
{
573589
Console.WriteLine("|| " + e.Data);
590+
LineReceived?.Invoke(this, e.Data);
574591
}
575592
else
576593
{
Collapse file

‎src/ElectronNET.API/ElectronNetRuntime.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/ElectronNetRuntime.cs
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public static class ElectronNetRuntime
1616
internal const int DefaultWebPort = 8001;
1717
internal const string ElectronPortArgumentName = "electronPort";
1818
internal const string ElectronPidArgumentName = "electronPID";
19+
internal const string ElectronAuthTokenArgumentName = "electronAuthToken";
1920

2021
/// <summary>Initializes the <see cref="ElectronNetRuntime"/> class.</summary>
2122
static ElectronNetRuntime()
@@ -26,6 +27,8 @@ static ElectronNetRuntime()
2627

2728
public static string ElectronExtraArguments { get; set; }
2829

30+
public static string ElectronAuthToken { get; internal set; }
31+
2932
public static int? ElectronSocketPort { get; internal set; }
3033

3134
public static int? AspNetWebPort { get; internal set; }
Collapse file

‎src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs
+5-10Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ internal class RuntimeControllerDotNetFirst : RuntimeControllerBase
1313
{
1414
private ElectronProcessBase electronProcess;
1515
private SocketBridgeService socketBridge;
16-
private int? port;
1716

1817
public RuntimeControllerDotNetFirst()
1918
{
@@ -41,19 +40,13 @@ protected override Task StartCore()
4140
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
4241
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
4342
var args = string.Format("{0} {1}", ElectronNetRuntime.ElectronExtraArguments, Environment.CommandLine).Trim();
44-
this.port = ElectronNetRuntime.ElectronSocketPort;
45-
46-
if (!this.port.HasValue)
47-
{
48-
this.port = PortHelper.GetFreePort(ElectronNetRuntime.DefaultSocketPort);
49-
ElectronNetRuntime.ElectronSocketPort = this.port;
50-
}
43+
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;
5144

5245
Console.Error.WriteLine("[StartCore]: isUnPacked: {0}", isUnPacked);
5346
Console.Error.WriteLine("[StartCore]: electronBinaryName: {0}", electronBinaryName);
5447
Console.Error.WriteLine("[StartCore]: args: {0}", args);
5548

56-
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value);
49+
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, port);
5750
this.electronProcess.Ready += this.ElectronProcess_Ready;
5851
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
5952

@@ -63,8 +56,10 @@ protected override Task StartCore()
6356

6457
private void ElectronProcess_Ready(object sender, EventArgs e)
6558
{
59+
var port = ElectronNetRuntime.ElectronSocketPort.Value;
60+
var token = ElectronNetRuntime.ElectronAuthToken;
6661
this.TransitionState(LifetimeState.Started);
67-
this.socketBridge = new SocketBridgeService(this.port!.Value);
62+
this.socketBridge = new SocketBridgeService(port, token);
6863
this.socketBridge.Ready += this.SocketBridge_Ready;
6964
this.socketBridge.Stopped += this.SocketBridge_Stopped;
7065
this.socketBridge.Start();
Collapse file

‎src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs
+3-8Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ internal class RuntimeControllerElectronFirst : RuntimeControllerBase
1111
{
1212
private ElectronProcessBase electronProcess;
1313
private SocketBridgeService socketBridge;
14-
private int? port;
1514

1615
public RuntimeControllerElectronFirst()
1716
{
@@ -36,20 +35,16 @@ internal override ISocketConnection Socket
3635

3736
protected override Task StartCore()
3837
{
39-
this.port = ElectronNetRuntime.ElectronSocketPort;
40-
41-
if (!this.port.HasValue)
42-
{
43-
throw new Exception("No port has been specified by Electron!");
44-
}
38+
var port = ElectronNetRuntime.ElectronSocketPort.Value;
39+
var token = ElectronNetRuntime.ElectronAuthToken;
4540

4641
if (!ElectronNetRuntime.ElectronProcessId.HasValue)
4742
{
4843
throw new Exception("No electronPID has been specified by Electron!");
4944
}
5045

5146
this.TransitionState(LifetimeState.Starting);
52-
this.socketBridge = new SocketBridgeService(this.port!.Value);
47+
this.socketBridge = new SocketBridgeService(port, token);
5348
this.socketBridge.Ready += this.SocketBridge_Ready;
5449
this.socketBridge.Stopped += this.SocketBridge_Stopped;
5550
this.socketBridge.Start();
Collapse file

‎src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs‎

Copy file name to clipboardExpand all lines: src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs
+134-12Lines changed: 134 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
namespace ElectronNET.Runtime.Services.ElectronProcess
22
{
33
using System;
4+
using System.Collections.Generic;
45
using System.ComponentModel;
6+
using System.Diagnostics;
57
using System.IO;
68
using System.Linq;
79
using System.Runtime.InteropServices;
10+
using System.Security.Cryptography;
11+
using System.Text.Json;
12+
using System.Threading;
813
using System.Threading.Tasks;
914
using ElectronNET.Common;
1015
using ElectronNET.Runtime.Data;
@@ -15,6 +20,9 @@
1520
[Localizable(false)]
1621
internal class ElectronProcessActive : ElectronProcessBase
1722
{
23+
private const string AuthTokenEnvVar = "ELECTRONNET_AUTH_TOKEN";
24+
private const string StartupInfoEnvVar = "ELECTRONNET_STARTUP_INFO";
25+
1826
private readonly bool isUnpackaged;
1927
private readonly string electronBinaryName;
2028
private readonly string extraArguments;
@@ -88,8 +96,23 @@ protected override async Task StartCore()
8896
workingDir = dir.FullName;
8997
}
9098

99+
// Generate the auth token on the .NET side (256 bit entropy) and pass it
100+
// to Electron via an environment variable. Electron will report the
101+
// OS-selected port via a temporary handshake file - this avoids any
102+
// dependency on parsing Electron's console output.
103+
var authToken = CreateAuthToken();
104+
var startupInfoPath = Path.Combine(
105+
Path.GetTempPath(),
106+
$"electronnet-startup-{Environment.ProcessId}-{Guid.NewGuid():N}.json");
107+
91108
// We don't await this in order to let the state transition to "Starting"
92-
Task.Run(async () => await this.StartInternal(startCmd, args, workingDir).ConfigureAwait(false));
109+
Task.Run(async () => await this.StartInternal(startCmd, args, workingDir, authToken, startupInfoPath).ConfigureAwait(false));
110+
}
111+
112+
private static string CreateAuthToken()
113+
{
114+
var bytes = RandomNumberGenerator.GetBytes(32);
115+
return Convert.ToHexString(bytes).ToLowerInvariant();
93116
}
94117

95118
private void CheckRuntimeIdentifier()
@@ -101,7 +124,6 @@ private void CheckRuntimeIdentifier()
101124
}
102125

103126
var osPart = buildInfoRid.Split('-').First();
104-
105127
var mismatch = false;
106128

107129
switch (osPart)
@@ -155,20 +177,56 @@ protected override Task StopCore()
155177
return Task.CompletedTask;
156178
}
157179

158-
private async Task StartInternal(string startCmd, string args, string directoriy)
180+
private async Task StartInternal(string startCmd, string args, string directoriy, string authToken, string startupInfoPath)
159181
{
160-
try
182+
var tcs = new TaskCompletionSource();
183+
using var cts = new CancellationTokenSource(2 * 60_000); // cancel after 2 minutes
184+
using var _ = cts.Token.Register(() =>
185+
{
186+
// Time is over - let's kill the process and move on
187+
this.process.Cancel();
188+
// We don't want to raise exceptions here - just pass the barrier
189+
tcs.TrySetResult();
190+
});
191+
192+
void Monitor_SocketIO_Failure(object sender, EventArgs e)
161193
{
162-
await Task.Delay(10.ms()).ConfigureAwait(false);
194+
// We don't want to raise exceptions here - just pass the barrier
195+
if (tcs.Task.IsCompleted)
196+
{
197+
this.Process_Exited(sender, e);
198+
}
199+
else
200+
{
201+
tcs.TrySetResult();
202+
}
203+
}
163204

205+
try
206+
{
164207
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
165208
Console.Error.WriteLine("[StartInternal]: args: {0}", args);
166209

167210
this.process = new ProcessRunner("ElectronRunner");
168-
this.process.ProcessExited += this.Process_Exited;
169-
this.process.Run(startCmd, args, directoriy);
211+
this.process.ProcessExited += Monitor_SocketIO_Failure;
170212

171-
await Task.Delay(500.ms()).ConfigureAwait(false);
213+
var env = new Dictionary<string, string>
214+
{
215+
[AuthTokenEnvVar] = authToken,
216+
[StartupInfoEnvVar] = startupInfoPath,
217+
};
218+
219+
this.process.Run(startCmd, args, directoriy, env);
220+
221+
// Wait for Electron to write the startup-info file (or for the process to die / timeout).
222+
var waitTask = WaitForStartupInfoAsync(startupInfoPath, cts.Token);
223+
var completed = await Task.WhenAny(waitTask, tcs.Task).ConfigureAwait(false);
224+
225+
int port = 0;
226+
if (completed == waitTask && waitTask.Status == TaskStatus.RanToCompletion)
227+
{
228+
port = waitTask.Result;
229+
}
172230

173231
Console.Error.WriteLine("[StartInternal]: after run:");
174232

@@ -178,11 +236,18 @@ private async Task StartInternal(string startCmd, string args, string directoriy
178236
Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput);
179237

180238
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
181-
182-
throw new Exception("Failed to launch the Electron process.");
183239
}
184-
185-
this.TransitionState(LifetimeState.Ready);
240+
else if (port > 0)
241+
{
242+
ElectronNetRuntime.ElectronAuthToken = authToken;
243+
ElectronNetRuntime.ElectronSocketPort = port;
244+
this.TransitionState(LifetimeState.Ready);
245+
}
246+
else
247+
{
248+
Console.Error.WriteLine("[StartInternal]: Did not receive Electron startup info before process exit/timeout.");
249+
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
250+
}
186251
}
187252
catch (Exception ex)
188253
{
@@ -191,6 +256,63 @@ private async Task StartInternal(string startCmd, string args, string directoriy
191256
Console.Error.WriteLine("[StartInternal]: Exception: " + ex);
192257
throw;
193258
}
259+
finally
260+
{
261+
try
262+
{
263+
if (File.Exists(startupInfoPath))
264+
{
265+
File.Delete(startupInfoPath);
266+
}
267+
}
268+
catch
269+
{
270+
// best effort cleanup
271+
}
272+
}
273+
}
274+
275+
private static async Task<int> WaitForStartupInfoAsync(string startupInfoPath, CancellationToken cancellationToken)
276+
{
277+
while (!cancellationToken.IsCancellationRequested)
278+
{
279+
try
280+
{
281+
if (File.Exists(startupInfoPath))
282+
{
283+
var json = await File.ReadAllTextAsync(startupInfoPath, cancellationToken).ConfigureAwait(false);
284+
if (!string.IsNullOrWhiteSpace(json))
285+
{
286+
using var doc = JsonDocument.Parse(json);
287+
if (doc.RootElement.TryGetProperty("port", out var portElement) &&
288+
portElement.TryGetInt32(out var port) &&
289+
port > 0)
290+
{
291+
return port;
292+
}
293+
}
294+
}
295+
}
296+
catch (JsonException)
297+
{
298+
// File may be partially written / racing with the writer - retry.
299+
}
300+
catch (IOException)
301+
{
302+
// Same - transient races on file access; retry.
303+
}
304+
305+
try
306+
{
307+
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
308+
}
309+
catch (TaskCanceledException)
310+
{
311+
break;
312+
}
313+
}
314+
315+
return 0;
194316
}
195317

196318
private void Process_Exited(object sender, EventArgs e)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.