Add project files.

This commit is contained in:
2026-03-08 20:45:01 +01:00
commit 053a4052dd
45 changed files with 2578 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace LeagueAPI.Utils;
public interface IPortTokenBehavior
{
bool TryGet(Process process, out string remotingAuthToken, out int appPort, [NotNullWhen(false)] out Exception? exception);
void MapArguments(IReadOnlyList<string> args, Dictionary<string, string> _args)
{
foreach (string arg in args)
{
if (arg.Length > 0 && arg.Contains('='))
{
string text = arg;
string[] array = text[2..].Split("=");
string key = array[0];
string value = array[1];
_args[key] = value;
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json;
namespace LeagueAPI.Utils;
public class LcuHttpClient : HttpClient
{
private static readonly Lazy<LcuHttpClient> LazyInstance = new(() => new LcuHttpClient(new LcuHttpClientHandler()));
internal static LcuHttpClient Instance => LazyInstance.Value;
private LcuHttpClientHandler Handler { get; }
public ProcessInfo? ProcessInfo
{
get
{
return Handler.ProcessInfo;
}
internal set
{
Handler.ProcessInfo = value;
}
}
public RiotAuthentication? RiotAuthentication => Handler.RiotAuthentication;
internal LcuHttpClient(LcuHttpClientHandler lcuHttpClientHandler)
: base(lcuHttpClientHandler)
{
Handler = lcuHttpClientHandler;
base.BaseAddress = new Uri("https://127.0.0.1");
}
public async Task<T?> GetContentAsync<T>(string requestUri) where T : class
{
HttpResponseMessage response = await GetAsync(requestUri);
if (!response.IsSuccessStatusCode)
{
return null;
}
string content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(content);
}
}

View File

@@ -0,0 +1,151 @@
namespace LeagueAPI.Utils;
internal class LcuHttpClientHandler : HttpClientHandler
{
private Lazy<bool> _isFirstRequest = new(() => true);
private Lazy<bool> _isFailing = new(() => false);
public ProcessInfo? ProcessInfo { get; internal set; }
public RiotAuthentication? RiotAuthentication
{
get
{
if (ProcessInfo is not null)
{
return new RiotAuthentication(ProcessInfo.RemotingAuthToken);
}
return null;
}
}
public string? BaseAddress
{
get
{
if (ProcessInfo != null)
{
return $"https://127.0.0.1:{ProcessInfo.AppPort}";
}
return null;
}
}
internal LcuHttpClientHandler()
{
base.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpResponseMessage result;
int num;
try
{
if (_isFirstRequest.Value)
{
_isFirstRequest = new Lazy<bool>(() => false);
SetProcessInfo();
}
if (_isFailing.Value)
{
_isFailing = new Lazy<bool>(() => false);
SetProcessInfo();
}
PrepareRequestMessage(request);
result = await base.SendAsync(request, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
return result;
}
catch (InvalidOperationException)
{
_isFailing = new Lazy<bool>(() => true);
throw;
}
catch (HttpRequestException)
{
num = 1;
}
if (num == 1)
{
try
{
SetProcessInfo();
PrepareRequestMessage(request);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
}
catch (InvalidOperationException)
{
_isFailing = new Lazy<bool>(() => true);
throw;
}
}
throw new Exception("Failed to send request");
}
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
try
{
if (_isFirstRequest.Value)
{
_isFirstRequest = new Lazy<bool>(() => false);
SetProcessInfo();
}
if (_isFailing.Value)
{
_isFailing = new Lazy<bool>(() => false);
SetProcessInfo();
}
PrepareRequestMessage(request);
return base.Send(request, cancellationToken);
}
catch (InvalidOperationException)
{
_isFailing = new Lazy<bool>(() => true);
throw;
}
catch (HttpRequestException)
{
try
{
SetProcessInfo();
PrepareRequestMessage(request);
return base.Send(request, cancellationToken);
}
catch (InvalidOperationException)
{
_isFailing = new Lazy<bool>(() => true);
throw;
}
}
}
private void SetProcessInfo()
{
ProcessInfo = ProcessFinder.GetProcessInfo();
if (!ProcessFinder.IsPortOpen(ProcessInfo))
{
throw new InvalidOperationException("Failed to connect to LCUx process port.");
}
}
private void PrepareRequestMessage(HttpRequestMessage request)
{
if (BaseAddress != null)
{
Uri requestUri = new(BaseAddress + (request.RequestUri?.PathAndQuery ?? "/"));
request.RequestUri = requestUri;
}
request.Headers.Authorization = RiotAuthentication?.ToAuthenticationHeaderValue();
}
}

View File

@@ -0,0 +1,43 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace LeagueAPI.Utils;
internal class PortTokenWithLockfile : IPortTokenBehavior
{
public bool TryGet(Process process, out string remotingAuthToken, out int appPort, [NotNullWhen(false)] out Exception? exception)
{
try
{
DirectoryInfo directory = new(Path.GetDirectoryName(process.MainModule?.FileName) ?? throw new FileNotFoundException("Lockfile not found.", "lockfile"));
FileInfo lockfile = new(Path.Join(directory.FullName, "lockfile"));
while (!lockfile.Exists)
{
if (directory.Root == directory)
{
break;
}
directory = directory.Parent ?? directory.Root;
lockfile = new(Path.Join(directory.FullName, "lockfile"));
}
using FileStream stream = lockfile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader streamReader = new(stream);
string[] array = streamReader.ReadToEnd().Split(":");
if (array is not [string clientName, string pid, string port, string secret, string protocol] || !int.TryParse(port, out appPort))
{
throw new Exception("Failed to parse lockfile.");
}
remotingAuthToken = secret;
exception = null;
return true;
}
catch (Exception ex)
{
remotingAuthToken = string.Empty;
appPort = 0;
exception = ex;
return false;
}
}
}

View File

@@ -0,0 +1,80 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace LeagueAPI.Utils;
internal partial class PortTokenWithProcessList : IPortTokenBehavior
{
[GeneratedRegex("--remoting-auth-token=([\\w-]*)")]
private static partial Regex AuthTokenRegex();
[GeneratedRegex("--app-port=([0-9]*)")]
private static partial Regex AppPortRegex();
public bool TryGet(Process process, out string remotingAuthToken, out int appPort, [NotNullWhen(false)] out Exception? exception)
{
try
{
ProcessStartInfo startInfo;
if (OperatingSystem.IsWindows())
{
startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = "Get-CimInstance Win32_Process -Filter \"\"\"name = 'LeagueClientUx.exe'\"\"\" | Select-Object -ExpandProperty CommandLine",
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
}
else
{
if (!OperatingSystem.IsMacOS())
{
throw new PlatformNotSupportedException("This behavior is only supported on Windows or MacOS.");
}
startInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = "-c \"ps x -o args | grep 'LeagueClientUx'\"",
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
}
using Process process2 = new()
{
StartInfo = startInfo
};
process2.Start();
process2.WaitForExit();
string text = process2.StandardOutput.ReadToEnd();
string text2 = process2.StandardError.ReadToEnd();
if (string.IsNullOrEmpty(text))
{
if (!string.IsNullOrEmpty(text2))
{
throw new Exception(text2);
}
throw new FormatException("The output string is empty.");
}
remotingAuthToken = AuthTokenRegex().Match(text).Groups[1].Value;
appPort = int.Parse(AppPortRegex().Match(text).Groups[1].Value);
exception = null;
return true;
}
catch (Exception ex)
{
remotingAuthToken = string.Empty;
appPort = 0;
exception = ex;
return false;
}
}
}

View File

@@ -0,0 +1,59 @@
using System.Diagnostics;
using System.Net.Sockets;
namespace LeagueAPI.Utils;
public static class ProcessFinder
{
public static Process GetProcess()
{
Process[] processesByName = Process.GetProcessesByName("LeagueClientUx");
return processesByName.FirstOrDefault(p => p.ProcessName == "LeagueClientUx")
?? throw new InvalidOperationException("Failed to find LCUx process.");
}
public static ProcessInfo GetProcessInfo()
{
return new ProcessInfo(GetProcess());
}
public static bool IsActive()
{
try
{
GetProcessInfo();
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
public static bool IsPortOpen()
{
try
{
return IsPortOpen(GetProcessInfo());
}
catch (InvalidOperationException)
{
return false;
}
}
public static bool IsPortOpen(ProcessInfo processInfo)
{
try
{
using TcpClient tcpClient = new();
tcpClient.Connect("127.0.0.1", processInfo.AppPort);
tcpClient.Close();
return true;
}
catch (SocketException)
{
return false;
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Diagnostics;
namespace LeagueAPI.Utils;
public class ProcessInfo
{
private static readonly List<IPortTokenBehavior> Behaviors =
[
new PortTokenWithLockfile(),
new PortTokenWithProcessList(),
];
public int AppPort { get; }
public string RemotingAuthToken { get; }
internal ProcessInfo(Process process)
{
List<Exception> exceptions = [];
foreach (IPortTokenBehavior behavior in Behaviors)
{
if (behavior.TryGet(process, out string remotingAuthToken, out int appPort, out Exception? exception))
{
RemotingAuthToken = remotingAuthToken;
AppPort = appPort;
return;
}
exceptions.Add(exception);
}
throw new AggregateException("Unable to obtain process information.", exceptions);
}
}