using System.Net; using System.Net.Sockets; using Microsoft.Extensions.Options; using Prospect.Server.Api.Config; using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.CloudScript.Models; namespace Prospect.Server.Api.Services.CloudScript.Functions; [CloudScriptFunction("GetSignalRConnection")] public class GetSignalRConnection : ICloudScriptFunction { // LAN address of the homelab host. Clients on the local network get the SignalR URL on // this IP (which their cert already validates via the IP/DNS SAN), while external clients // keep the public domain from configuration. This matters because the real-time WebSocket // (libwebsockets, incl. under Proton/Wine) can't reach the public domain from inside the // LAN (hairpin NAT), which surfaced as "error code 5" right after the intro. private const string LanHost = "192.168.1.136"; private readonly PlayFabSettings _settings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor, ILogger logger) { _settings = settings.Value; _httpContextAccessor = httpContextAccessor; _logger = logger; } public Task ExecuteAsync(FYGetSignalRConnection request) { // The game client connects to SignalR only over HTTPS. var url = _settings.SignalRURL; var remote = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; var lan = remote != null && IsLan(remote); if (lan) { try { url = new UriBuilder(url) { Host = LanHost }.Uri.AbsoluteUri; } catch { /* keep the configured URL on any parsing issue */ } } // Tag the connection with the player id so the hub can map connection -> user and // target server messages (deploy signal) instead of broadcasting to everyone. var userId = _httpContextAccessor.HttpContext?.User.FindAuthUserId(); if (!string.IsNullOrEmpty(userId)) url += (url.Contains('?') ? "&" : "?") + "uid=" + Uri.EscapeDataString(userId); _logger.LogInformation("GetSignalRConnection: remoteIp={Remote} lan={Lan} uid={Uid} -> {Url}", remote, lan, userId, url); return Task.FromResult(new FYGetSignalRConnectionResult { Url = url, AccessToken = _settings.SignalRAccessToken }); } private static bool IsLan(IPAddress ip) { if (IPAddress.IsLoopback(ip)) return true; if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4(); if (ip.AddressFamily != AddressFamily.InterNetwork) return false; var b = ip.GetAddressBytes(); return b[0] == 10 || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || (b[0] == 192 && b[1] == 168); } }