diff --git a/Core/SteamLoginCdp.cs b/Core/SteamLoginCdp.cs new file mode 100644 index 0000000..7e27492 --- /dev/null +++ b/Core/SteamLoginCdp.cs @@ -0,0 +1,239 @@ +using System; +using System.IO; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace SAM.Core +{ + public enum CdpLoginResult + { + Success, + NeedCode, + NoDebugPort, + Failed + } + + // Drives Steam's CEF login page over the Chrome DevTools Protocol. + // Replaces the old UI Automation approach + public static class SteamLoginCdp + { + private const string Endpoint = "http://127.0.0.1:8080"; + private const string FlagFileName = ".cef-enable-remote-debugging"; + + public static void EnsureRemoteDebugging(string steamPath) + { + try + { + if (string.IsNullOrEmpty(steamPath)) return; + string flag = Path.Combine(steamPath, FlagFileName); + if (!File.Exists(flag)) File.Create(flag).Dispose(); + } + catch (Exception e) { Console.WriteLine("EnsureRemoteDebugging: " + e.Message); } + } + + public static CdpLoginResult Login(string user, string password, string sharedSecret, int timeoutSeconds = 120) + { + try { return LoginAsync(user, password, sharedSecret, timeoutSeconds).GetAwaiter().GetResult(); } + catch (Exception e) { Console.WriteLine("CDP Login: " + e.Message); return CdpLoginResult.Failed; } + } + + private static async Task LoginAsync(string user, string password, string sharedSecret, int timeoutSeconds) + { + string ws = await WaitForLoginTarget(30); + if (ws == null) return CdpLoginResult.NoDebugPort; + + using (var cdp = new Cdp()) + { + await cdp.Connect(ws); + + DateTime deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds); + while (DateTime.UtcNow < deadline) + { + if (WindowUtils.GetMainSteamClientWindow("Steam").IsValid) + return CdpLoginResult.Success; + + string state = await SafeEval(cdp, JsState); + if (state == null) + { + // Endpoint dropped during the post-login handoff; the client window is the source of truth. + if (WindowUtils.GetMainSteamClientWindow("Steam").IsValid) return CdpLoginResult.Success; + await Task.Delay(500); + continue; + } + + switch (state) + { + case "login": + string fill = await SafeEval(cdp, JsFill, JsStr(user), JsStr(password)); + await Task.Delay(fill == "submitted" ? 1500 : 700); + break; + + case "selection": + await SafeEval(cdp, JsSelect); + await Task.Delay(700); + break; + + case "code": + if (string.IsNullOrEmpty(sharedSecret)) return CdpLoginResult.NeedCode; + string code = WindowUtils.Generate2FACode(sharedSecret); + await SafeEval(cdp, JsCode, JsStr(code)); + await Task.Delay(2500); + break; + + case "loading": + await Task.Delay(800); + break; + + default: + await Task.Delay(600); + break; + } + } + } + + return WindowUtils.GetMainSteamClientWindow("Steam").IsValid ? CdpLoginResult.Success : CdpLoginResult.Failed; + } + + private static async Task SafeEval(Cdp cdp, string fn, params string[] args) + { + try { return await cdp.Eval(fn, args); } catch { return null; } + } + + private static async Task WaitForLoginTarget(int seconds) + { + DateTime deadline = DateTime.UtcNow.AddSeconds(seconds); + while (DateTime.UtcNow < deadline) + { + try + { + string json = HttpGet(Endpoint + "/json"); + foreach (Match m in Regex.Matches(json, "\\{[^{}]*?\"webSocketDebuggerUrl\"[^{}]*?\\}", RegexOptions.Singleline)) + { + string blk = m.Value; + if (Regex.IsMatch(blk, "\"type\"\\s*:\\s*\"page\"") && + blk.IndexOf("Sign in to Steam", StringComparison.OrdinalIgnoreCase) >= 0) + return Regex.Match(blk, "\"webSocketDebuggerUrl\"\\s*:\\s*\"([^\"]*)\"").Groups[1].Value; + } + } + catch { } + await Task.Delay(500); + } + return null; + } + + private static string HttpGet(string url) + { + var req = (HttpWebRequest)WebRequest.Create(url); + req.Timeout = 4000; + using (var resp = (HttpWebResponse)req.GetResponse()) + using (var sr = new StreamReader(resp.GetResponseStream())) + return sr.ReadToEnd(); + } + + private static string JsStr(string s) + { + var sb = new StringBuilder("\""); + foreach (char c in s) + { + if (c == '"' || c == '\\') sb.Append('\\').Append(c); + else if (c == '\n') sb.Append("\\n"); + else if (c == '\r') sb.Append("\\r"); + else sb.Append(c); + } + return sb.Append('"').ToString(); + } + + // ---- injected page scripts (shadow-piercing, React-safe) ---- + + private const string JsFill = @"function(u,p){ + var a=[];(function d(r){var e=r.querySelectorAll('*');for(var i=0;i=0)btn=el;} + if(!ui||!pi)return 'no-fields'; + var s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; + function set(el,v){el.focus();s.call(el,v);el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));} + set(ui,u);set(pi,p); + if(!btn)return 'no-button'; + if(btn.disabled)return 'disabled'; + btn.click();return 'submitted';}"; + + private const string JsState = @"function(){ + var a=[];(function d(r){var e=r.querySelectorAll('*');for(var i=0;i=0)return 'loading'; + if(pw>0)return 'login'; + if(t.indexOf('enter the code')>=0||t.indexOf('authenticator')>=0||txt>=5||(txt>0&&t.indexOf('code')>=0))return 'code'; + if(t.indexOf('sign in as')>=0||t.indexOf('add an account')>=0)return 'selection'; + return 'other';}"; + + private const string JsCode = @"function(c){ + var a=[];(function d(r){var e=r.querySelectorAll('*');for(var i=0;i=c.length){for(var i=0;i=0||t.indexOf('different account')>=0)){el.click();return 'picked';}} + return 'no-add';}"; + + // ---- minimal CDP transport ---- + private class Cdp : IDisposable + { + private readonly ClientWebSocket _sock = new ClientWebSocket(); + private int _id; + + public async Task Connect(string ws) + { + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) + await _sock.ConnectAsync(new Uri(ws), cts.Token); + } + + public async Task Eval(string funcJs, params string[] jsonArgs) + { + string expr = "(" + funcJs + ")(" + string.Join(",", jsonArgs) + ")"; + string p = "{\"expression\":" + JsStr(expr) + ",\"returnByValue\":true,\"awaitPromise\":true}"; + string reply = await Call("Runtime.evaluate", p); + if (reply == null) return null; + Match m = Regex.Match(reply, "\"value\"\\s*:\\s*\"([^\"\\\\]*)\""); + return m.Success ? m.Groups[1].Value : null; + } + + private async Task Call(string method, string paramsJson) + { + int id = ++_id; + string msg = "{\"id\":" + id + ",\"method\":\"" + method + "\",\"params\":" + paramsJson + "}"; + await _sock.SendAsync(new ArraySegment(Encoding.UTF8.GetBytes(msg)), WebSocketMessageType.Text, true, CancellationToken.None); + + var buf = new byte[262144]; + for (int i = 0; i < 60; i++) + { + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) + using (var ms = new MemoryStream()) + { + WebSocketReceiveResult r; + do { r = await _sock.ReceiveAsync(new ArraySegment(buf), cts.Token); ms.Write(buf, 0, r.Count); } + while (!r.EndOfMessage); + string reply = Encoding.UTF8.GetString(ms.ToArray()); + if (Regex.IsMatch(reply, "\"id\"\\s*:\\s*" + id + "\\b")) return reply; + } + } + return null; + } + + public void Dispose() { try { _sock.Dispose(); } catch { } } + } + } +} diff --git a/SAM.csproj b/SAM.csproj index 202bf82..189afa4 100644 --- a/SAM.csproj +++ b/SAM.csproj @@ -95,6 +95,7 @@ + diff --git a/Views/AccountsWindow.xaml.cs b/Views/AccountsWindow.xaml.cs index f971711..976250e 100644 --- a/Views/AccountsWindow.xaml.cs +++ b/Views/AccountsWindow.xaml.cs @@ -1334,6 +1334,9 @@ private void Login(Account account, int tryCount) string startParams = parametersBuilder.ToString(); + // Enable Steam's CEF remote debugging before launch so we can drive the login page over CDP. + SteamLoginCdp.EnsureRemoteDebugging(settings.User.SteamPath); + // Start Steam process with the selected path. ProcessStartInfo startInfo = new ProcessStartInfo { @@ -1355,7 +1358,47 @@ private void Login(Account account, int tryCount) return; } - EnterCredentials(steamProcess, account, 0); + LoginViaCdp(account, tryCount); + } + + private void LoginViaCdp(Account account, int tryCount) + { + string password = StringCipher.Decrypt(account.Password, eKey); + string secret = StringCipher.Decrypt(account.SharedSecret, eKey); + + SetWindowTitle("Working"); + + CdpLoginResult result = SteamLoginCdp.Login(account.Name, password, secret); + + switch (result) + { + case CdpLoginResult.Success: + PostLogin(); + break; + + case CdpLoginResult.NeedCode: + MessageBox.Show("This account requires a Steam Guard code, but no shared secret is set for it.\n" + + "Add the account's shared secret, or enter the code manually in Steam.", + "Steam Guard", MessageBoxButton.OK, MessageBoxImage.Warning); + break; + + case CdpLoginResult.NoDebugPort: + MessageBox.Show("Could not connect to Steam's sign-in page (CEF remote debugging).\n\n" + + "Steam may need to be fully closed and reopened once so it picks up the debugging flag.", + "Login", MessageBoxButton.OK, MessageBoxImage.Error); + break; + + default: + if (tryCount + 1 < maxRetry) + { + Login(account, tryCount + 1); + } + else + { + MessageBox.Show("Login Failed! Please try again.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + break; + } } private void EnterCredentials(Process steamProcess, Account account, int tryCount)