Program.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. using System;
  2. using System.IO;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Net.Http;
  6. using System.Threading.Tasks;
  7. using Microsoft.AspNetCore.Builder;
  8. using Microsoft.AspNetCore.Http;
  9. using Microsoft.Extensions.Logging;
  10. using Microsoft.Extensions.Hosting;
  11. using Newtonsoft.Json;
  12. using Newtonsoft.Json.Linq;
  13. using SteamAuth;
  14. using SteamKit2;
  15. using SteamKit2.Authentication;
  16. var builder = WebApplication.CreateBuilder(args);
  17. var app = builder.Build();
  18. app.MapPost("/login", async (HttpContext http) =>
  19. {
  20. var req = await GetRequestBodyAsync(http);
  21. if (req == null)
  22. {
  23. http.Response.StatusCode = 400;
  24. await http.Response.WriteAsync("invalid request");
  25. return;
  26. }
  27. if (!File.Exists(req.MaFilePath))
  28. {
  29. http.Response.StatusCode = 400;
  30. await http.Response.WriteAsync("maFile not found");
  31. return;
  32. }
  33. SteamGuardAccount? account;
  34. try
  35. {
  36. var contents = await File.ReadAllTextAsync(req.MaFilePath);
  37. account = JsonConvert.DeserializeObject<SteamGuardAccount>(contents);
  38. }
  39. catch (Exception ex)
  40. {
  41. http.Response.StatusCode = 500;
  42. await http.Response.WriteAsync("failed to read maFile: " + ex.Message);
  43. return;
  44. }
  45. try
  46. {
  47. // Start SteamClient and authenticate using the maFile-provided SteamGuardAccount
  48. SteamClient steamClient = CreateSteamClient(req.ProxyUrl, app.Logger, "login");
  49. steamClient.Connect();
  50. var timeoutMs = 15000;
  51. var waited = 0;
  52. while (!steamClient.IsConnected && waited < timeoutMs)
  53. {
  54. await Task.Delay(200);
  55. waited += 200;
  56. }
  57. if (!steamClient.IsConnected)
  58. {
  59. http.Response.StatusCode = 500;
  60. await http.Response.WriteAsync("failed to connect to steam network");
  61. return;
  62. }
  63. CredentialsAuthSession authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
  64. {
  65. Username = req.Username,
  66. Password = req.Password,
  67. IsPersistentSession = false,
  68. // PlatformType and ClientOSType are optional; omit to avoid enum symbol differences across SteamKit2 versions.
  69. Authenticator = new FileAuthenticator(account),
  70. });
  71. var pollResponse = await authSession.PollingWaitForResultAsync();
  72. var sessionData = new SessionData()
  73. {
  74. SteamID = authSession.SteamID.ConvertToUInt64(),
  75. AccessToken = pollResponse.AccessToken,
  76. RefreshToken = pollResponse.RefreshToken,
  77. };
  78. // Serialize cookies
  79. var uri = new Uri("https://steamcommunity.com");
  80. var cookies = sessionData.GetCookies().GetCookies(uri).Cast<Cookie>().Select(c => new
  81. {
  82. name = c.Name,
  83. value = c.Value,
  84. domain = c.Domain,
  85. path = c.Path,
  86. expires = c.Expires == DateTime.MinValue ? null : (DateTime?)c.Expires
  87. }).ToArray();
  88. var resp = new
  89. {
  90. session = new
  91. {
  92. steamId = sessionData.SteamID,
  93. accessToken = sessionData.AccessToken,
  94. refreshToken = sessionData.RefreshToken,
  95. sessionId = sessionData.SessionID
  96. },
  97. cookies = cookies
  98. };
  99. http.Response.ContentType = "application/json";
  100. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp, Formatting.Indented));
  101. return;
  102. }
  103. catch (Exception ex)
  104. {
  105. http.Response.StatusCode = 500;
  106. await http.Response.WriteAsync("login failed: " + ex.Message);
  107. return;
  108. }
  109. });
  110. app.MapPost("/wallet-balance", async (HttpContext http) =>
  111. {
  112. var req = await GetWalletBalanceRequestBodyAsync(http);
  113. if (req == null || string.IsNullOrWhiteSpace(req.SteamLoginSecure))
  114. {
  115. http.Response.StatusCode = 400;
  116. await http.Response.WriteAsync("invalid request");
  117. return;
  118. }
  119. try
  120. {
  121. var handler = CreateHttpClientHandler(req.ProxyUrl, app.Logger, "wallet-balance");
  122. using var client = new HttpClient(handler, disposeHandler: true)
  123. {
  124. Timeout = TimeSpan.FromSeconds(20)
  125. };
  126. var request = new HttpRequestMessage(HttpMethod.Get, "https://store.steampowered.com/api/getfundwalletinfo/?cc=us&l=english");
  127. request.Headers.Add("Cookie", $"steamLoginSecure={req.SteamLoginSecure}");
  128. var response = await client.SendAsync(request);
  129. var body = await response.Content.ReadAsStringAsync();
  130. if (!response.IsSuccessStatusCode)
  131. {
  132. http.Response.StatusCode = 500;
  133. await http.Response.WriteAsync($"wallet request failed: {(int)response.StatusCode}");
  134. return;
  135. }
  136. var json = JsonConvert.DeserializeObject<JObject>(body);
  137. if (json == null || json["success"]?.Value<int>() != 1 || json["user_wallet"] is not JObject wallet)
  138. {
  139. http.Response.StatusCode = 500;
  140. await http.Response.WriteAsync("wallet response invalid");
  141. return;
  142. }
  143. var resp = new
  144. {
  145. amount = wallet["amount"]?.Value<string>() ?? "0",
  146. currency = wallet["currency"]?.Value<string>() ?? "USD"
  147. };
  148. http.Response.ContentType = "application/json";
  149. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp));
  150. }
  151. catch (Exception ex)
  152. {
  153. http.Response.StatusCode = 500;
  154. await http.Response.WriteAsync("wallet query failed: " + ex.Message);
  155. }
  156. });
  157. app.Run();
  158. async Task<LoginRequest?> GetRequestBodyAsync(HttpContext http)
  159. {
  160. try
  161. {
  162. using var sr = new StreamReader(http.Request.Body);
  163. var body = await sr.ReadToEndAsync();
  164. if (string.IsNullOrWhiteSpace(body)) return null;
  165. return JsonConvert.DeserializeObject<LoginRequest>(body);
  166. }
  167. catch
  168. {
  169. return null;
  170. }
  171. }
  172. async Task<WalletBalanceRequest?> GetWalletBalanceRequestBodyAsync(HttpContext http)
  173. {
  174. try
  175. {
  176. using var sr = new StreamReader(http.Request.Body);
  177. var body = await sr.ReadToEndAsync();
  178. if (string.IsNullOrWhiteSpace(body)) return null;
  179. return JsonConvert.DeserializeObject<WalletBalanceRequest>(body);
  180. }
  181. catch
  182. {
  183. return null;
  184. }
  185. }
  186. SteamClient CreateSteamClient(string? requestProxyUrl, ILogger logger, string endpointName)
  187. {
  188. var (proxyUrl, source) = ResolveProxyUrl(requestProxyUrl);
  189. logger.LogInformation(
  190. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  191. endpointName,
  192. source,
  193. MaskProxyUrlForLog(proxyUrl)
  194. );
  195. if (string.IsNullOrWhiteSpace(proxyUrl))
  196. {
  197. logger.LogInformation("[{Endpoint}] Proxy disabled for this request.", endpointName);
  198. return new SteamClient();
  199. }
  200. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var proxyUri))
  201. {
  202. logger.LogWarning(
  203. "[{Endpoint}] Proxy url invalid. raw={RawProxy}",
  204. endpointName,
  205. MaskProxyUrlForLog(proxyUrl)
  206. );
  207. throw new ArgumentException("invalid proxy url, expected format like http://127.0.0.1:7890");
  208. }
  209. var config = SteamConfiguration.Create(builder =>
  210. {
  211. // Force WebSocket so proxy can be applied through HttpClient.
  212. builder.WithProtocolTypes(ProtocolTypes.WebSocket);
  213. builder.WithHttpClientFactory(() =>
  214. {
  215. var handler = new HttpClientHandler
  216. {
  217. Proxy = new WebProxy(proxyUri),
  218. UseProxy = true
  219. };
  220. return new HttpClient(handler, disposeHandler: true);
  221. });
  222. });
  223. return new SteamClient(config);
  224. }
  225. HttpClientHandler CreateHttpClientHandler(string? requestProxyUrl, ILogger logger, string endpointName)
  226. {
  227. var (proxyUrl, source) = ResolveProxyUrl(requestProxyUrl);
  228. logger.LogInformation(
  229. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  230. endpointName,
  231. source,
  232. MaskProxyUrlForLog(proxyUrl)
  233. );
  234. var handler = new HttpClientHandler();
  235. if (string.IsNullOrWhiteSpace(proxyUrl))
  236. {
  237. logger.LogInformation("[{Endpoint}] Proxy disabled for this request.", endpointName);
  238. return handler;
  239. }
  240. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var proxyUri))
  241. {
  242. logger.LogWarning(
  243. "[{Endpoint}] Proxy url invalid. raw={RawProxy}",
  244. endpointName,
  245. MaskProxyUrlForLog(proxyUrl)
  246. );
  247. throw new ArgumentException("invalid proxy url, expected format like http://127.0.0.1:7890");
  248. }
  249. handler.Proxy = new WebProxy(proxyUri);
  250. handler.UseProxy = true;
  251. return handler;
  252. }
  253. (string? ProxyUrl, string Source) ResolveProxyUrl(string? requestProxyUrl)
  254. {
  255. if (!string.IsNullOrWhiteSpace(requestProxyUrl))
  256. {
  257. return (requestProxyUrl, "request.ProxyUrl");
  258. }
  259. var envProxy = Environment.GetEnvironmentVariable("STEAM_PROXY_URL");
  260. if (!string.IsNullOrWhiteSpace(envProxy))
  261. {
  262. return (envProxy, "env.STEAM_PROXY_URL");
  263. }
  264. return (null, "none");
  265. }
  266. string MaskProxyUrlForLog(string? proxyUrl)
  267. {
  268. if (string.IsNullOrWhiteSpace(proxyUrl))
  269. {
  270. return "<none>";
  271. }
  272. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var uri))
  273. {
  274. var trimmed = proxyUrl.Trim();
  275. if (trimmed.Length > 64)
  276. {
  277. trimmed = trimmed.Substring(0, 64) + "...";
  278. }
  279. return $"<invalid:{trimmed}>";
  280. }
  281. var hostPort = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}";
  282. return $"{uri.Scheme}://{hostPort}";
  283. }
  284. record LoginRequest(string Username, string Password, string MaFilePath, string? ProxyUrl = null);
  285. record WalletBalanceRequest(string SteamLoginSecure, string? ProxyUrl = null);
  286. internal class FileAuthenticator : IAuthenticator
  287. {
  288. private readonly SteamGuardAccount? account;
  289. private int deviceCodesGenerated = 0;
  290. public FileAuthenticator(SteamGuardAccount? account)
  291. {
  292. this.account = account;
  293. }
  294. public Task<bool> AcceptDeviceConfirmationAsync()
  295. {
  296. return Task.FromResult(false);
  297. }
  298. public async Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect)
  299. {
  300. if (previousCodeWasIncorrect)
  301. {
  302. if (deviceCodesGenerated > 2)
  303. throw new Exception("failed to generate device code from maFile");
  304. await Task.Delay(30000);
  305. }
  306. if (account == null) return null;
  307. deviceCodesGenerated++;
  308. var code = await account.GenerateSteamGuardCodeAsync();
  309. return code ?? "";
  310. }
  311. public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect)
  312. {
  313. // This implementation does not support interactive email codes.
  314. return Task.FromResult(string.Empty);
  315. }
  316. }