Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/ElectronNET.API/Bridge/SocketIOConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@
namespace ElectronNET.API;

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ElectronNET.API.Serialization;
using SocketIO.Serializer.SystemTextJson;
using SocketIO = SocketIOClient.SocketIO;
using SocketIOOptions = SocketIOClient.SocketIOOptions;

internal class SocketIOConnection : ISocketConnection
{
private readonly SocketIO _socket;
private readonly object _lockObj = new object();
private bool _isDisposed;

public SocketIOConnection(string uri)
public SocketIOConnection(string uri, string authorization)
{
_socket = new SocketIO(uri);
var opts = string.IsNullOrEmpty(authorization) ? new SocketIOOptions() : new SocketIOOptions
{
ExtraHeaders = new Dictionary<string, string>
{
["authorization"] = authorization
},
};
_socket = new SocketIO(uri, opts);
_socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options);
// Use default System.Text.Json serializer from SocketIOClient.
// Outgoing args are normalized to camelCase via SerializeArg in Emit.
Expand Down
3 changes: 3 additions & 0 deletions src/ElectronNET.API/Common/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ProcessRunner : IDisposable
private readonly StringBuilder stdOut = new StringBuilder(4 * 1024);
private readonly StringBuilder stdErr = new StringBuilder(4 * 1024);

public event EventHandler<string> LineReceived;

private volatile ManualResetEvent stdOutEvent;
private volatile ManualResetEvent stdErrEvent;
private volatile Stopwatch stopwatch;
Expand Down Expand Up @@ -571,6 +573,7 @@ private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
if (e.Data != null)
{
Console.WriteLine("|| " + e.Data);
LineReceived?.Invoke(this, e.Data);
}
else
{
Expand Down
3 changes: 3 additions & 0 deletions src/ElectronNET.API/ElectronNetRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class ElectronNetRuntime
internal const int DefaultWebPort = 8001;
internal const string ElectronPortArgumentName = "electronPort";
internal const string ElectronPidArgumentName = "electronPID";
internal const string ElectronAuthTokenArgumentName = "electronAuthToken";

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

public static string ElectronExtraArguments { get; set; }

public static string ElectronAuthToken { get; internal set; }

public static int? ElectronSocketPort { get; internal set; }

public static int? AspNetWebPort { get; internal set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ internal class RuntimeControllerDotNetFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;

public RuntimeControllerDotNetFirst()
{
Expand Down Expand Up @@ -41,19 +40,13 @@ protected override Task StartCore()
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
var args = string.Format("{0} {1}", ElectronNetRuntime.ElectronExtraArguments, Environment.CommandLine).Trim();
this.port = ElectronNetRuntime.ElectronSocketPort;

if (!this.port.HasValue)
{
this.port = PortHelper.GetFreePort(ElectronNetRuntime.DefaultSocketPort);
ElectronNetRuntime.ElectronSocketPort = this.port;
}
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;

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

this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value);
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, port);
this.electronProcess.Ready += this.ElectronProcess_Ready;
this.electronProcess.Stopped += this.ElectronProcess_Stopped;

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

private void ElectronProcess_Ready(object sender, EventArgs e)
{
var port = ElectronNetRuntime.ElectronSocketPort.Value;
var token = ElectronNetRuntime.ElectronAuthToken;
this.TransitionState(LifetimeState.Started);
this.socketBridge = new SocketBridgeService(this.port!.Value);
this.socketBridge = new SocketBridgeService(port, token);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal class RuntimeControllerElectronFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;

public RuntimeControllerElectronFirst()
{
Expand All @@ -36,20 +35,16 @@ internal override ISocketConnection Socket

protected override Task StartCore()
{
this.port = ElectronNetRuntime.ElectronSocketPort;

if (!this.port.HasValue)
{
throw new Exception("No port has been specified by Electron!");
}
var port = ElectronNetRuntime.ElectronSocketPort.Value;
var token = ElectronNetRuntime.ElectronAuthToken;

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

this.TransitionState(LifetimeState.Starting);
this.socketBridge = new SocketBridgeService(this.port!.Value);
this.socketBridge = new SocketBridgeService(port, token);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
Expand All @@ -15,6 +17,7 @@
[Localizable(false)]
internal class ElectronProcessActive : ElectronProcessBase
{
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$");
private readonly bool isUnpackaged;
private readonly string electronBinaryName;
private readonly string extraArguments;
Expand Down Expand Up @@ -157,18 +160,36 @@ protected override Task StopCore()

private async Task StartInternal(string startCmd, string args, string directoriy)
{
try
var tcs = new TaskCompletionSource();

void Read_SocketIO_Parameters(object sender, string line)
{
await Task.Delay(10.ms()).ConfigureAwait(false);
// Look for "Electron Socket: listening on port %s at ..."
var match = extractor.Match(line);

if (match?.Success ?? false)
{
var port = int.Parse(match.Groups[1].Value);
var token = match.Groups[2].Value;

this.process.LineReceived -= Read_SocketIO_Parameters;
ElectronNetRuntime.ElectronAuthToken = token;
ElectronNetRuntime.ElectronSocketPort = port;
tcs.SetResult();
}
}

try
{
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
Console.Error.WriteLine("[StartInternal]: args: {0}", args);

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

await Task.Delay(500.ms()).ConfigureAwait(false);
await tcs.Task.ConfigureAwait(false);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
internal class SocketBridgeService : LifetimeServiceBase
{
private readonly int socketPort;
private readonly string authorization;
private readonly string socketUrl;
private SocketIOConnection socket;

public SocketBridgeService(int socketPort)
public SocketBridgeService(int socketPort, string authorization)
{
this.socketPort = socketPort;
this.authorization = authorization;
this.socketUrl = $"http://localhost:{this.socketPort}";
}

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

protected override Task StartCore()
{
this.socket = new SocketIOConnection(this.socketUrl);
this.socket = new SocketIOConnection(this.socketUrl, this.authorization);
this.socket.BridgeConnected += this.Socket_BridgeConnected;
this.socket.BridgeDisconnected += this.Socket_BridgeDisconnected;
Task.Run(this.Connect);
Expand Down
14 changes: 14 additions & 0 deletions src/ElectronNET.API/Runtime/StartupManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ private void CollectProcessData()
Console.WriteLine("Electron Process ID: " + result);
}
}

var authTokenArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronAuthTokenArgumentName, StringComparison.OrdinalIgnoreCase));

if (authTokenArg != null)
{
var parts = authTokenArg.Split('=', StringSplitOptions.TrimEntries);

if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]))
{
var result = parts[1];
ElectronNetRuntime.ElectronAuthToken = result;
Console.WriteLine("Use Auth Token: " + result);
}
}
}

private void SetElectronExecutable()
Expand Down
15 changes: 10 additions & 5 deletions src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ElectronNET.API
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using ElectronNET.AspNet;
Expand All @@ -10,6 +11,7 @@
using ElectronNET.Runtime.Helpers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

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

var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
ElectronNetRuntime.AspNetWebPort = webPort;
var webPort = ElectronNetRuntime.AspNetWebPort ?? 0;

// check for the content folder if its exists in base director otherwise no need to include
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.

// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
var host = webPort == 0? "127.0.0.1" : "localhost";

if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
{
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseUrls("http://localhost:" + webPort);
.UseUrls($"http://{host}:{webPort}");
}
else
{
builder = builder.UseUrls("http://localhost:" + webPort);
builder = builder.UseUrls($"http://{host}:{webPort}");
}

builder = builder.ConfigureServices(services =>
builder = builder.ConfigureServices((context, services) =>
{
services.AddTransient<IStartupFilter, ServerReadyStartupFilter>();
services.AddSingleton<AspNetLifetimeAdapter>();
Expand Down
100 changes: 100 additions & 0 deletions src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
namespace ElectronNET.AspNet.Middleware
{
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using ElectronNET.AspNet.Services;

/// <summary>
/// Middleware that validates authentication for all Electron requests.
/// Checks for authentication cookie or token query parameter on first request.
/// Sets HttpOnly cookie for subsequent requests.
///
/// Security Model:
/// - First request includes token as query parameter (?token=guid)
/// - Middleware validates token and sets secure HttpOnly cookie
/// - Subsequent requests use cookie (no token in URL)
/// - HTTP endpoints protected
/// </summary>
public class ElectronAuthenticationMiddleware
{
private readonly RequestDelegate _next;
private readonly IElectronAuthenticationService _authService;
private readonly ILogger<ElectronAuthenticationMiddleware> _logger;
private const string AuthCookieName = "ElectronAuth";

public ElectronAuthenticationMiddleware(
RequestDelegate next,
IElectronAuthenticationService authService,
ILogger<ElectronAuthenticationMiddleware> logger)
{
_next = next;
_authService = authService;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value;

// Check if authentication cookie exists
var authCookie = context.Request.Cookies[AuthCookieName];

if (!string.IsNullOrEmpty(authCookie))
{
// Cookie present - validate it
if (_authService.ValidateToken(authCookie))
{
await _next(context);
return;
}
else
{
// Invalid cookie - reject
_logger.LogWarning("Authentication failed: Invalid cookie for path {Path} from {RemoteIp}", path, context.Connection.RemoteIpAddress);
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
return;
}
}

// No cookie - check for token in query string (first-time authentication)
var token = context.Request.Query["token"].ToString();

if (!string.IsNullOrEmpty(token))
{
if (_authService.ValidateToken(token))
{
// Valid token - set cookie for future requests
_logger.LogInformation("Authentication successful: Setting cookie for path {Path}", path);

context.Response.Cookies.Append(AuthCookieName, token, new CookieOptions
{
HttpOnly = true, // Prevent JavaScript access (XSS protection)
SameSite = SameSiteMode.Strict, // CSRF protection
Path = "/", // Valid for all routes
Secure = false, // False because localhost is HTTP
IsEssential = true // Required for app to function
});

await _next(context);
return;
}
else
{
// Invalid token - reject
_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);
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
return;
}
}

// Neither cookie nor valid token present - reject
_logger.LogWarning("Authentication failed: No cookie or token provided for path {Path} from {RemoteIp}", path, context.Connection.RemoteIpAddress);
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Authentication required");
}
}
}
Loading