Skip to content
Closed
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
14 changes: 9 additions & 5 deletions Deceive/ConfigProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ internal ConfigProxy(int chatPort)
{
ChatPort = chatPort;

// Find a free port.
// Bind to port 0 to get a free port from the OS. Keep the listener alive until
// the web server is ready to prevent another process from stealing the port.
var l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
var port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();

ConfigPort = port;

// Release the probe listener just before the web server binds the same port.
l.Stop();

// Start a web server that sends everything to ProxyAndRewriteResponse
var server = new WebServer(o => o
.WithUrlPrefix("http://127.0.0.1:" + port)
Expand Down Expand Up @@ -82,6 +85,7 @@ private async Task ProxyAndRewriteResponseAsync(IHttpContext ctx)
var url = ConfigUrl + ctx.Request.RawUrl;
Trace.WriteLine("Received client proxy request to URL: " + url);


using var message = new HttpRequestMessage(HttpMethod.Get, url);
// Cloudflare bitches at us without a user agent.
message.Headers.TryAddWithoutValidation("User-Agent", ctx.Request.Headers["user-agent"]);
Expand All @@ -97,7 +101,7 @@ private async Task ProxyAndRewriteResponseAsync(IHttpContext ctx)
Trace.WriteLine("Received response from clientconfig service with status code: " + result.StatusCode);
var content = await result.Content.ReadAsStringAsync();
var modifiedContent = content;
Trace.WriteLine("ORIGINAL CLIENTCONFIG: " + content);
Trace.WriteLine("Received CLIENTCONFIG response (content omitted from logs to protect player data).");

// sometimes riot yields an internal error with content that is definitely
// not json. we can just forward it to the riot client, which will retry
Expand Down Expand Up @@ -139,7 +143,7 @@ private async Task ProxyAndRewriteResponseAsync(IHttpContext ctx)
try
{
var pasJwt = await (await Client.SendAsync(pasRequest)).Content.ReadAsStringAsync();
Trace.WriteLine("PAS JWT:" + pasJwt);
Trace.WriteLine("Received PAS JWT (value omitted from logs).");
var pasJwtContent = pasJwt.Split('.')[1];
var validBase64 = pasJwtContent.PadRight((pasJwtContent.Length / 4 * 4) + (pasJwtContent.Length % 4 == 0 ? 0 : 4), '=');
var pasJwtString = Encoding.UTF8.GetString(Convert.FromBase64String(validBase64));
Expand All @@ -164,7 +168,7 @@ private async Task ProxyAndRewriteResponseAsync(IHttpContext ctx)
}

modifiedContent = JsonSerializer.Serialize(configObject);
Trace.WriteLine("MODIFIED CLIENTCONFIG: " + modifiedContent);
Trace.WriteLine("Rewrote CLIENTCONFIG chat endpoints to local proxy.");

if (riotChatHost is not null && riotChatPort != 0)
PatchedChatServer?.Invoke(this, new ChatServerEventArgs { ChatHost = riotChatHost, ChatPort = riotChatPort });
Expand Down
6 changes: 6 additions & 0 deletions Deceive/Persistence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ internal static LaunchGame GetDefaultLaunchGame()
}

internal static void SetCachedCertificate(byte[] certBytes) => File.WriteAllBytes(CachedCertPath, certBytes);

internal static void DeleteCachedCertificate()
{
if (File.Exists(CachedCertPath))
File.Delete(CachedCertPath);
}

// Startup status: "chat", "offline", "mobile", or "last" (remember last session).
internal static string GetStartupStatus() => File.Exists(StartupStatusPath) ? File.ReadAllText(StartupStatusPath) : "last";
Expand Down
11 changes: 10 additions & 1 deletion Deceive/StartupHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,16 @@ private static async Task StartDeceiveAsync(LaunchGame game, string gamePatchlin
startArgs.Arguments += $" --launch-product={launchProduct} --launch-patchline={gamePatchline}";

if (riotClientParams is not null)
startArgs.Arguments += $" {riotClientParams}";
{
if (riotClientParams.Contains("--client-config-url", StringComparison.OrdinalIgnoreCase))
{
Trace.WriteLine("Ignoring riotClientParams containing --client-config-url to prevent proxy bypass.");
}
else
{
startArgs.Arguments += $" {riotClientParams}";
}
}

if (gameParams is not null)
startArgs.Arguments += $" -- {gameParams}";
Expand Down
36 changes: 32 additions & 4 deletions Deceive/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ public static async Task CheckForUpdatesAsync()
);

if (result is DialogResult.OK)
// Open the url in the browser.
Process.Start(release?["html_url"]?.ToString()!);
{
var htmlUrl = release?["html_url"]?.ToString();
if (!string.IsNullOrEmpty(htmlUrl) && htmlUrl.StartsWith("https://github.com/", StringComparison.Ordinal))
Process.Start(htmlUrl);
}
}
catch
{
Expand Down Expand Up @@ -163,15 +166,25 @@ public static void KillProcesses()
}
}

private const string ExpectedCertDomain = "deceive-localhost.molenzwiebel.xyz";

// Returns a certificate for deceive-localhost.molenzwiebel.xyz, either from cache or by downloading
// the current one from the server. The returned certificate will be valid for at least 20 days.
public static async Task<X509Certificate2?> GetProxyCertificateAsync()
{
var cachedCert = Persistence.GetCachedCertificate();
if (cachedCert is not null && cachedCert.NotAfter > DateTime.Now.AddDays(20))
{
Trace.WriteLine($"Cached certificate is valid until {cachedCert.NotAfter}, using cached certificate.");
return cachedCert;
if (!ValidateProxyCertificate(cachedCert))
{
Trace.WriteLine("Cached certificate failed domain validation, discarding.");
Persistence.DeleteCachedCertificate();
}
else
{
Trace.WriteLine($"Cached certificate is valid until {cachedCert.NotAfter}, using cached certificate.");
return cachedCert;
}
}

try
Expand All @@ -184,6 +197,13 @@ public static void KillProcesses()
response.EnsureSuccessStatusCode();
var certBytes = await response.Content.ReadAsByteArrayAsync();
var cert = new X509Certificate2(certBytes);

if (!ValidateProxyCertificate(cert))
{
Trace.WriteLine("Downloaded certificate failed domain validation, refusing to use it.");
return null;
}

Persistence.SetCachedCertificate(certBytes);
return cert;
}
Expand All @@ -195,6 +215,14 @@ public static void KillProcesses()
}
}

private static bool ValidateProxyCertificate(X509Certificate2 cert)
{
var san = cert.Extensions["2.5.29.17"]?.Format(false) ?? string.Empty;
var cn = cert.GetNameInfo(X509NameType.DnsName, false);
return cn.Equals(ExpectedCertDomain, StringComparison.OrdinalIgnoreCase) ||
san.Contains(ExpectedCertDomain, StringComparison.OrdinalIgnoreCase);
}

private static bool DeceiveLocalhostResolves()
{
try
Expand Down