Skip to content

Commit d3b895e

Browse files
committed
Implemented socket authentication and improved port selection
1 parent c1bf6d9 commit d3b895e

19 files changed

+367
-112
lines changed

src/ElectronNET.API/Bridge/SocketIOFacade.cs

Lines 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 SocketIoFacade : IDisposable
1214
{
1315
private readonly SocketIO _socket;
1416
private readonly object _lockObj = new object();
1517
private bool _isDisposed;
1618

17-
public SocketIoFacade(string uri)
19+
public SocketIoFacade(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.

src/ElectronNET.API/Common/ProcessRunner.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class ProcessRunner : IDisposable
2626
private readonly StringBuilder stdOut = new StringBuilder(4 * 1024);
2727
private readonly StringBuilder stdErr = new StringBuilder(4 * 1024);
2828

29+
public event EventHandler<string> LineReceived;
30+
2931
private volatile ManualResetEvent stdOutEvent;
3032
private volatile ManualResetEvent stdErrEvent;
3133
private volatile Stopwatch stopwatch;
@@ -571,6 +573,7 @@ private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
571573
if (e.Data != null)
572574
{
573575
Console.WriteLine("|| " + e.Data);
576+
LineReceived?.Invoke(this, e.Data);
574577
}
575578
else
576579
{

src/ElectronNET.API/ElectronNetRuntime.cs

Lines 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; }

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

Lines 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();

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

Lines 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 SocketIoFacade 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();

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
{
33
using System;
44
using System.ComponentModel;
5+
using System.Diagnostics;
56
using System.IO;
67
using System.Linq;
78
using System.Runtime.InteropServices;
9+
using System.Text.RegularExpressions;
810
using System.Threading.Tasks;
911
using ElectronNET.Common;
1012
using ElectronNET.Runtime.Data;
@@ -15,6 +17,7 @@
1517
[Localizable(false)]
1618
internal class ElectronProcessActive : ElectronProcessBase
1719
{
20+
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$");
1821
private readonly bool isUnpackaged;
1922
private readonly string electronBinaryName;
2023
private readonly string extraArguments;
@@ -157,18 +160,36 @@ protected override Task StopCore()
157160

158161
private async Task StartInternal(string startCmd, string args, string directoriy)
159162
{
160-
try
163+
var tcs = new TaskCompletionSource();
164+
165+
void Read_SocketIO_Parameters(object sender, string line)
161166
{
162-
await Task.Delay(10.ms()).ConfigureAwait(false);
167+
// Look for "Electron Socket: listening on port %s at ..."
168+
var match = extractor.Match(line);
163169

170+
if (match?.Success ?? false)
171+
{
172+
var port = int.Parse(match.Groups[1].Value);
173+
var token = match.Groups[2].Value;
174+
175+
this.process.LineReceived -= Read_SocketIO_Parameters;
176+
ElectronNetRuntime.ElectronAuthToken = token;
177+
ElectronNetRuntime.ElectronSocketPort = port;
178+
tcs.SetResult();
179+
}
180+
}
181+
182+
try
183+
{
164184
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
165185
Console.Error.WriteLine("[StartInternal]: args: {0}", args);
166186

167187
this.process = new ProcessRunner("ElectronRunner");
168188
this.process.ProcessExited += this.Process_Exited;
189+
this.process.LineReceived += Read_SocketIO_Parameters;
169190
this.process.Run(startCmd, args, directoriy);
170191

171-
await Task.Delay(500.ms()).ConfigureAwait(false);
192+
await tcs.Task.ConfigureAwait(false);
172193

173194
Console.Error.WriteLine("[StartInternal]: after run:");
174195

src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
internal class SocketBridgeService : LifetimeServiceBase
99
{
1010
private readonly int socketPort;
11+
private readonly string authorization;
1112
private readonly string socketUrl;
1213
private SocketIoFacade socket;
1314

14-
public SocketBridgeService(int socketPort)
15+
public SocketBridgeService(int socketPort, string authorization)
1516
{
1617
this.socketPort = socketPort;
18+
this.authorization = authorization;
1719
this.socketUrl = $"http://localhost:{this.socketPort}";
1820
}
1921

@@ -23,7 +25,7 @@ public SocketBridgeService(int socketPort)
2325

2426
protected override Task StartCore()
2527
{
26-
this.socket = new SocketIoFacade(this.socketUrl);
28+
this.socket = new SocketIoFacade(this.socketUrl, this.authorization);
2729
this.socket.BridgeConnected += this.Socket_BridgeConnected;
2830
this.socket.BridgeDisconnected += this.Socket_BridgeDisconnected;
2931
Task.Run(this.Connect);

src/ElectronNET.API/Runtime/StartupManager.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ private void CollectProcessData()
106106
Console.WriteLine("Electron Process ID: " + result);
107107
}
108108
}
109+
110+
var authTokenArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronAuthTokenArgumentName, StringComparison.OrdinalIgnoreCase));
111+
112+
if (authTokenArg != null)
113+
{
114+
var parts = authTokenArg.Split('=', StringSplitOptions.TrimEntries);
115+
116+
if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]))
117+
{
118+
var result = parts[1];
119+
ElectronNetRuntime.ElectronAuthToken = result;
120+
Console.WriteLine("Use Auth Token: " + result);
121+
}
122+
}
109123
}
110124

111125
private void SetElectronExecutable()

src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace ElectronNET.API
22
{
33
using System;
4+
using System.Diagnostics;
45
using System.IO;
56
using System.Threading.Tasks;
67
using ElectronNET.AspNet;
@@ -10,6 +11,7 @@
1011
using ElectronNET.Runtime.Helpers;
1112
using Microsoft.AspNetCore.Hosting;
1213
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Hosting;
1315

1416
/// <summary>
1517
/// Provides extension methods for <see cref="IWebHostBuilder"/> to enable Electron.NET
@@ -66,23 +68,26 @@ public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[]
6668
// work as expected, see issue #952
6769
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", null);
6870

69-
var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
70-
ElectronNetRuntime.AspNetWebPort = webPort;
71+
var webPort = ElectronNetRuntime.AspNetWebPort ?? 0;
7172

7273
// check for the content folder if its exists in base director otherwise no need to include
7374
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
7475
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.
76+
77+
// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
78+
var host = webPort == 0? "127.0.0.1" : "localhost";
79+
7580
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
7681
{
7782
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
78-
.UseUrls("http://localhost:" + webPort);
83+
.UseUrls($"http://{host}:{webPort}");
7984
}
8085
else
8186
{
82-
builder = builder.UseUrls("http://localhost:" + webPort);
87+
builder = builder.UseUrls($"http://{host}:{webPort}");
8388
}
8489

85-
builder = builder.ConfigureServices(services =>
90+
builder = builder.ConfigureServices((context, services) =>
8691
{
8792
services.AddTransient<IStartupFilter, ServerReadyStartupFilter>();
8893
services.AddSingleton<AspNetLifetimeAdapter>();
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
namespace ElectronNET.AspNet.Middleware
2+
{
3+
using System;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.Extensions.Logging;
7+
using ElectronNET.AspNet.Services;
8+
9+
/// <summary>
10+
/// Middleware that validates authentication for all Electron requests.
11+
/// Checks for authentication cookie or token query parameter on first request.
12+
/// Sets HttpOnly cookie for subsequent requests.
13+
///
14+
/// Security Model:
15+
/// - First request includes token as query parameter (?token=guid)
16+
/// - Middleware validates token and sets secure HttpOnly cookie
17+
/// - Subsequent requests use cookie (no token in URL)
18+
/// - HTTP endpoints protected
19+
/// </summary>
20+
public class ElectronAuthenticationMiddleware
21+
{
22+
private readonly RequestDelegate _next;
23+
private readonly IElectronAuthenticationService _authService;
24+
private readonly ILogger<ElectronAuthenticationMiddleware> _logger;
25+
private const string AuthCookieName = "ElectronAuth";
26+
27+
public ElectronAuthenticationMiddleware(
28+
RequestDelegate next,
29+
IElectronAuthenticationService authService,
30+
ILogger<ElectronAuthenticationMiddleware> logger)
31+
{
32+
_next = next;
33+
_authService = authService;
34+
_logger = logger;
35+
}
36+
37+
public async Task InvokeAsync(HttpContext context)
38+
{
39+
var path = context.Request.Path.Value;
40+
41+
// Check if authentication cookie exists
42+
var authCookie = context.Request.Cookies[AuthCookieName];
43+
44+
if (!string.IsNullOrEmpty(authCookie))
45+
{
46+
// Cookie present - validate it
47+
if (_authService.ValidateToken(authCookie))
48+
{
49+
await _next(context);
50+
return;
51+
}
52+
else
53+
{
54+
// Invalid cookie - reject
55+
_logger.LogWarning("Authentication failed: Invalid cookie for path {Path} from {RemoteIp}", path, context.Connection.RemoteIpAddress);
56+
context.Response.StatusCode = 401;
57+
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
58+
return;
59+
}
60+
}
61+
62+
// No cookie - check for token in query string (first-time authentication)
63+
var token = context.Request.Query["token"].ToString();
64+
65+
if (!string.IsNullOrEmpty(token))
66+
{
67+
if (_authService.ValidateToken(token))
68+
{
69+
// Valid token - set cookie for future requests
70+
_logger.LogInformation("Authentication successful: Setting cookie for path {Path}", path);
71+
72+
context.Response.Cookies.Append(AuthCookieName, token, new CookieOptions
73+
{
74+
HttpOnly = true, // Prevent JavaScript access (XSS protection)
75+
SameSite = SameSiteMode.Strict, // CSRF protection
76+
Path = "/", // Valid for all routes
77+
Secure = false, // False because localhost is HTTP
78+
IsEssential = true // Required for app to function
79+
});
80+
81+
await _next(context);
82+
return;
83+
}
84+
else
85+
{
86+
// Invalid token - reject
87+
_logger.LogWarning("Authentication failed: Invalid token (prefix: {TokenPrefix}...) for path {Path} from {RemoteIp}", token.Length > 8 ? token.Substring(0, 8) : token, path, context.Connection.RemoteIpAddress);
88+
context.Response.StatusCode = 401;
89+
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
90+
return;
91+
}
92+
}
93+
94+
// Neither cookie nor valid token present - reject
95+
_logger.LogWarning("Authentication failed: No cookie or token provided for path {Path} from {RemoteIp}", path, context.Connection.RemoteIpAddress);
96+
context.Response.StatusCode = 401;
97+
await context.Response.WriteAsync("Unauthorized: Authentication required");
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)