Program.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  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. // ── 限制每个主机的最大并发连接数,防止连接耗尽 ──
  18. System.Net.ServicePointManager.DefaultConnectionLimit = 200;
  19. var app = builder.Build();
  20. // ── 全局异常处理:防止未捕获的异常导致进程崩溃 ──
  21. app.UseExceptionHandler(errorApp =>
  22. {
  23. errorApp.Run(async context =>
  24. {
  25. context.Response.StatusCode = 500;
  26. context.Response.ContentType = "application/json; charset=utf-8";
  27. var errorId = Guid.NewGuid().ToString("N")[..8];
  28. Console.Error.WriteLine($"[UNHANDLED] error_id={errorId} path={context.Request.Path}");
  29. await context.Response.WriteAsync($"{{\"error\":\"Internal server error\",\"error_id\":\"{errorId}\"}}");
  30. });
  31. });
  32. // ── 请求超时中间件:防止慢请求永久占用线程 ──
  33. app.Use(async (context, next) =>
  34. {
  35. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
  36. try
  37. {
  38. await next.Invoke();
  39. }
  40. catch (OperationCanceledException)
  41. {
  42. context.Response.StatusCode = 504;
  43. context.Response.ContentType = "application/json";
  44. await context.Response.WriteAsync("{\"error\":\"Request timeout\"}");
  45. }
  46. });
  47. app.MapPost("/login", async (HttpContext http) =>
  48. {
  49. var req = await GetRequestBodyAsync(http);
  50. if (req == null)
  51. {
  52. http.Response.StatusCode = 400;
  53. await http.Response.WriteAsync("invalid request");
  54. return;
  55. }
  56. if (!File.Exists(req.MaFilePath))
  57. {
  58. http.Response.StatusCode = 400;
  59. await http.Response.WriteAsync("maFile not found");
  60. return;
  61. }
  62. SteamGuardAccount? account;
  63. try
  64. {
  65. var contents = await File.ReadAllTextAsync(req.MaFilePath);
  66. account = JsonConvert.DeserializeObject<SteamGuardAccount>(contents);
  67. }
  68. catch (Exception ex)
  69. {
  70. http.Response.StatusCode = 500;
  71. await http.Response.WriteAsync("failed to read maFile: " + ex.Message);
  72. return;
  73. }
  74. try
  75. {
  76. // Start SteamClient and authenticate using the maFile-provided SteamGuardAccount
  77. SteamClient steamClient = CreateSteamClient(req.ProxyUrl, app.Logger, "login");
  78. steamClient.Connect();
  79. var timeoutMs = 15000;
  80. var waited = 0;
  81. while (!steamClient.IsConnected && waited < timeoutMs)
  82. {
  83. await Task.Delay(200);
  84. waited += 200;
  85. }
  86. if (!steamClient.IsConnected)
  87. {
  88. http.Response.StatusCode = 500;
  89. await http.Response.WriteAsync("failed to connect to steam network");
  90. return;
  91. }
  92. CredentialsAuthSession authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync(new AuthSessionDetails
  93. {
  94. Username = req.Username,
  95. Password = req.Password,
  96. IsPersistentSession = false,
  97. // PlatformType and ClientOSType are optional; omit to avoid enum symbol differences across SteamKit2 versions.
  98. Authenticator = new FileAuthenticator(account),
  99. });
  100. var pollResponse = await authSession.PollingWaitForResultAsync();
  101. var sessionData = new SessionData()
  102. {
  103. SteamID = authSession.SteamID.ConvertToUInt64(),
  104. AccessToken = pollResponse.AccessToken,
  105. RefreshToken = pollResponse.RefreshToken,
  106. };
  107. // Serialize cookies
  108. var uri = new Uri("https://steamcommunity.com");
  109. var cookies = sessionData.GetCookies().GetCookies(uri).Cast<Cookie>().Select(c => new
  110. {
  111. name = c.Name,
  112. value = c.Value,
  113. domain = c.Domain,
  114. path = c.Path,
  115. expires = c.Expires == DateTime.MinValue ? null : (DateTime?)c.Expires
  116. }).ToArray();
  117. var resp = new
  118. {
  119. session = new
  120. {
  121. steamId = sessionData.SteamID,
  122. accessToken = sessionData.AccessToken,
  123. refreshToken = sessionData.RefreshToken,
  124. sessionId = sessionData.SessionID
  125. },
  126. cookies = cookies
  127. };
  128. http.Response.ContentType = "application/json";
  129. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp, Formatting.Indented));
  130. return;
  131. }
  132. catch (Exception ex)
  133. {
  134. http.Response.StatusCode = 500;
  135. await http.Response.WriteAsync("login failed: " + ex.Message);
  136. return;
  137. }
  138. });
  139. app.MapPost("/wallet-balance", async (HttpContext http) =>
  140. {
  141. var req = await GetWalletBalanceRequestBodyAsync(http);
  142. if (req == null || string.IsNullOrWhiteSpace(req.SteamLoginSecure))
  143. {
  144. http.Response.StatusCode = 400;
  145. await http.Response.WriteAsync("invalid request");
  146. return;
  147. }
  148. try
  149. {
  150. var handler = CreateHttpClientHandler(req.ProxyUrl, app.Logger, "wallet-balance");
  151. using var client = new HttpClient(handler, disposeHandler: true)
  152. {
  153. Timeout = TimeSpan.FromSeconds(20)
  154. };
  155. var request = new HttpRequestMessage(HttpMethod.Get, "https://store.steampowered.com/api/getfundwalletinfo/?cc=us&l=english");
  156. request.Headers.Add("Cookie", $"steamLoginSecure={req.SteamLoginSecure}");
  157. var response = await client.SendAsync(request);
  158. var body = await response.Content.ReadAsStringAsync();
  159. if (!response.IsSuccessStatusCode)
  160. {
  161. http.Response.StatusCode = 500;
  162. await http.Response.WriteAsync($"wallet request failed: {(int)response.StatusCode}");
  163. return;
  164. }
  165. var json = JsonConvert.DeserializeObject<JObject>(body);
  166. if (json == null || json["success"]?.Value<int>() != 1 || json["user_wallet"] is not JObject wallet)
  167. {
  168. http.Response.StatusCode = 500;
  169. await http.Response.WriteAsync("wallet response invalid");
  170. return;
  171. }
  172. var resp = new
  173. {
  174. amount = wallet["amount"]?.Value<string>() ?? "0",
  175. currency = wallet["currency"]?.Value<string>() ?? "USD"
  176. };
  177. http.Response.ContentType = "application/json";
  178. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp));
  179. }
  180. catch (Exception ex)
  181. {
  182. http.Response.StatusCode = 500;
  183. await http.Response.WriteAsync("wallet query failed: " + ex.Message);
  184. }
  185. });
  186. app.MapPost("/trade-confirmations/list", async (HttpContext http) =>
  187. {
  188. var req = await GetTradeConfirmationsListRequestBodyAsync(http);
  189. if (req == null || string.IsNullOrWhiteSpace(req.MaFilePath) || req.Session == null)
  190. {
  191. http.Response.StatusCode = 400;
  192. await http.Response.WriteAsync("invalid request");
  193. return;
  194. }
  195. if (!File.Exists(req.MaFilePath))
  196. {
  197. http.Response.StatusCode = 400;
  198. await http.Response.WriteAsync("maFile not found");
  199. return;
  200. }
  201. try
  202. {
  203. var (proxyUrl, source) = ResolveProxyUrl(req.ProxyUrl);
  204. app.Logger.LogInformation(
  205. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  206. "trade-confirmations/list",
  207. source,
  208. MaskProxyUrlForLog(proxyUrl)
  209. );
  210. app.Logger.LogInformation(
  211. "[{Endpoint}] Calling Steam confirmation list. mode={Mode}, proxy={Proxy}",
  212. "trade-confirmations/list",
  213. string.IsNullOrWhiteSpace(proxyUrl) ? "direct" : "proxy",
  214. MaskProxyUrlForLog(proxyUrl)
  215. );
  216. var account = await BuildAccountWithSessionAsync(req.MaFilePath, req.Session);
  217. if (account == null)
  218. {
  219. http.Response.StatusCode = 500;
  220. await http.Response.WriteAsync("failed to build steam account session");
  221. return;
  222. }
  223. if (account.Session.IsRefreshTokenExpired())
  224. {
  225. http.Response.StatusCode = 401;
  226. await http.Response.WriteAsync("session refresh token expired");
  227. return;
  228. }
  229. var accessTokenRefreshed = false;
  230. if (account.Session.IsAccessTokenExpired())
  231. {
  232. await account.Session.RefreshAccessToken();
  233. accessTokenRefreshed = true;
  234. }
  235. var confirmations = await account.FetchConfirmationsAsync(proxyUrl);
  236. var resp = new
  237. {
  238. session = new
  239. {
  240. steamId = account.Session.SteamID,
  241. accessToken = account.Session.AccessToken,
  242. refreshToken = account.Session.RefreshToken,
  243. sessionId = account.Session.SessionID,
  244. accessTokenRefreshed
  245. },
  246. confirmations = confirmations.Select(c => new
  247. {
  248. id = c.ID,
  249. key = c.Key,
  250. creatorId = c.Creator,
  251. type = c.ConfType.ToString(),
  252. headline = c.Headline,
  253. summary = c.Summary,
  254. accept = c.Accept,
  255. cancel = c.Cancel,
  256. icon = c.Icon
  257. }).ToArray()
  258. };
  259. http.Response.ContentType = "application/json";
  260. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp));
  261. }
  262. catch (Exception ex)
  263. {
  264. http.Response.StatusCode = 500;
  265. await http.Response.WriteAsync("fetch confirmations failed: " + ex.Message);
  266. }
  267. });
  268. app.MapPost("/trade-confirmations/operate", async (HttpContext http) =>
  269. {
  270. var req = await GetTradeConfirmationOperateRequestBodyAsync(http);
  271. if (req == null
  272. || string.IsNullOrWhiteSpace(req.MaFilePath)
  273. || req.Session == null
  274. || req.ConfirmationId == 0
  275. || string.IsNullOrWhiteSpace(req.Operation))
  276. {
  277. http.Response.StatusCode = 400;
  278. await http.Response.WriteAsync("invalid request");
  279. return;
  280. }
  281. if (!File.Exists(req.MaFilePath))
  282. {
  283. http.Response.StatusCode = 400;
  284. await http.Response.WriteAsync("maFile not found");
  285. return;
  286. }
  287. var op = req.Operation.Trim().ToLowerInvariant();
  288. if (op != "accept" && op != "cancel")
  289. {
  290. http.Response.StatusCode = 400;
  291. await http.Response.WriteAsync("invalid operation, expected accept or cancel");
  292. return;
  293. }
  294. try
  295. {
  296. var (proxyUrl, source) = ResolveProxyUrl(req.ProxyUrl);
  297. app.Logger.LogInformation(
  298. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  299. "trade-confirmations/operate",
  300. source,
  301. MaskProxyUrlForLog(proxyUrl)
  302. );
  303. app.Logger.LogInformation(
  304. "[{Endpoint}] Calling Steam confirmation operation. op={Op}, mode={Mode}, proxy={Proxy}",
  305. "trade-confirmations/operate",
  306. op,
  307. string.IsNullOrWhiteSpace(proxyUrl) ? "direct" : "proxy",
  308. MaskProxyUrlForLog(proxyUrl)
  309. );
  310. var account = await BuildAccountWithSessionAsync(req.MaFilePath, req.Session);
  311. if (account == null)
  312. {
  313. http.Response.StatusCode = 500;
  314. await http.Response.WriteAsync("failed to build steam account session");
  315. return;
  316. }
  317. if (account.Session.IsRefreshTokenExpired())
  318. {
  319. http.Response.StatusCode = 401;
  320. await http.Response.WriteAsync("session refresh token expired");
  321. return;
  322. }
  323. var accessTokenRefreshed = false;
  324. if (account.Session.IsAccessTokenExpired())
  325. {
  326. await account.Session.RefreshAccessToken();
  327. accessTokenRefreshed = true;
  328. }
  329. ulong confirmationKey = req.ConfirmationKey ?? 0;
  330. ulong effectiveConfirmationId = req.ConfirmationId;
  331. var keyResolvedFromList = false;
  332. var idResolvedFromList = false;
  333. var matchedBy = "request";
  334. if (confirmationKey == 0)
  335. {
  336. var confirmations = await account.FetchConfirmationsAsync(proxyUrl);
  337. var matched = confirmations.FirstOrDefault(c => c.ID == req.ConfirmationId);
  338. if (matched != null)
  339. {
  340. matchedBy = "id";
  341. }
  342. else
  343. {
  344. // buylisting 返回的 confirmation_id 在部分场景为 creator_id,
  345. // 这里回退按 creator_id 匹配,便于调用方仅传一个确认订单号即可完成确认。
  346. matched = confirmations.FirstOrDefault(c => c.Creator == req.ConfirmationId);
  347. if (matched != null)
  348. {
  349. matchedBy = "creator_id";
  350. }
  351. }
  352. if (matched == null || matched.Key == 0)
  353. {
  354. http.Response.StatusCode = 404;
  355. await http.Response.WriteAsync("confirmation not found or key unavailable");
  356. return;
  357. }
  358. effectiveConfirmationId = matched.ID;
  359. confirmationKey = matched.Key;
  360. keyResolvedFromList = true;
  361. idResolvedFromList = true;
  362. }
  363. var confirmation = new Confirmation
  364. {
  365. ID = effectiveConfirmationId,
  366. Key = confirmationKey
  367. };
  368. var success = op == "accept"
  369. ? await account.AcceptConfirmation(confirmation, proxyUrl)
  370. : await account.DenyConfirmation(confirmation, proxyUrl);
  371. var resp = new
  372. {
  373. success,
  374. operation = op,
  375. requestedConfirmationId = req.ConfirmationId,
  376. confirmationId = effectiveConfirmationId,
  377. confirmationKey,
  378. keyResolvedFromList,
  379. idResolvedFromList,
  380. matchedBy,
  381. session = new
  382. {
  383. steamId = account.Session.SteamID,
  384. accessToken = account.Session.AccessToken,
  385. refreshToken = account.Session.RefreshToken,
  386. sessionId = account.Session.SessionID,
  387. accessTokenRefreshed
  388. }
  389. };
  390. http.Response.ContentType = "application/json";
  391. await http.Response.WriteAsync(JsonConvert.SerializeObject(resp));
  392. }
  393. catch (Exception ex)
  394. {
  395. http.Response.StatusCode = 500;
  396. await http.Response.WriteAsync("operate confirmation failed: " + ex.Message);
  397. }
  398. });
  399. app.Run();
  400. async Task<LoginRequest?> GetRequestBodyAsync(HttpContext http)
  401. {
  402. try
  403. {
  404. using var sr = new StreamReader(http.Request.Body);
  405. var body = await sr.ReadToEndAsync();
  406. if (string.IsNullOrWhiteSpace(body)) return null;
  407. return JsonConvert.DeserializeObject<LoginRequest>(body);
  408. }
  409. catch
  410. {
  411. return null;
  412. }
  413. }
  414. async Task<WalletBalanceRequest?> GetWalletBalanceRequestBodyAsync(HttpContext http)
  415. {
  416. try
  417. {
  418. using var sr = new StreamReader(http.Request.Body);
  419. var body = await sr.ReadToEndAsync();
  420. if (string.IsNullOrWhiteSpace(body)) return null;
  421. return JsonConvert.DeserializeObject<WalletBalanceRequest>(body);
  422. }
  423. catch
  424. {
  425. return null;
  426. }
  427. }
  428. async Task<TradeConfirmationsListRequest?> GetTradeConfirmationsListRequestBodyAsync(HttpContext http)
  429. {
  430. try
  431. {
  432. using var sr = new StreamReader(http.Request.Body);
  433. var body = await sr.ReadToEndAsync();
  434. if (string.IsNullOrWhiteSpace(body)) return null;
  435. return JsonConvert.DeserializeObject<TradeConfirmationsListRequest>(body);
  436. }
  437. catch
  438. {
  439. return null;
  440. }
  441. }
  442. async Task<TradeConfirmationOperateRequest?> GetTradeConfirmationOperateRequestBodyAsync(HttpContext http)
  443. {
  444. try
  445. {
  446. using var sr = new StreamReader(http.Request.Body);
  447. var body = await sr.ReadToEndAsync();
  448. if (string.IsNullOrWhiteSpace(body)) return null;
  449. return JsonConvert.DeserializeObject<TradeConfirmationOperateRequest>(body);
  450. }
  451. catch
  452. {
  453. return null;
  454. }
  455. }
  456. async Task<SteamGuardAccount?> BuildAccountWithSessionAsync(string maFilePath, SessionPayload sessionPayload)
  457. {
  458. var contents = await File.ReadAllTextAsync(maFilePath);
  459. var account = JsonConvert.DeserializeObject<SteamGuardAccount>(contents);
  460. if (account == null)
  461. {
  462. return null;
  463. }
  464. if (string.IsNullOrWhiteSpace(account.DeviceID))
  465. {
  466. throw new Exception("maFile missing device_id");
  467. }
  468. account.Session = new SessionData
  469. {
  470. SteamID = sessionPayload.SteamId,
  471. AccessToken = sessionPayload.AccessToken,
  472. RefreshToken = sessionPayload.RefreshToken,
  473. SessionID = sessionPayload.SessionId
  474. };
  475. return account;
  476. }
  477. SteamClient CreateSteamClient(string? requestProxyUrl, ILogger logger, string endpointName)
  478. {
  479. var (proxyUrl, source) = ResolveProxyUrl(requestProxyUrl);
  480. logger.LogInformation(
  481. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  482. endpointName,
  483. source,
  484. MaskProxyUrlForLog(proxyUrl)
  485. );
  486. if (string.IsNullOrWhiteSpace(proxyUrl))
  487. {
  488. logger.LogInformation("[{Endpoint}] Proxy disabled for this request.", endpointName);
  489. return new SteamClient();
  490. }
  491. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var proxyUri))
  492. {
  493. logger.LogWarning(
  494. "[{Endpoint}] Proxy url invalid. raw={RawProxy}",
  495. endpointName,
  496. MaskProxyUrlForLog(proxyUrl)
  497. );
  498. throw new ArgumentException("invalid proxy url, expected format like http://127.0.0.1:7890");
  499. }
  500. var config = SteamConfiguration.Create(builder =>
  501. {
  502. // Force WebSocket so proxy can be applied through HttpClient.
  503. builder.WithProtocolTypes(ProtocolTypes.WebSocket);
  504. builder.WithHttpClientFactory(() =>
  505. {
  506. var handler = new HttpClientHandler
  507. {
  508. Proxy = new WebProxy(proxyUri),
  509. UseProxy = true
  510. };
  511. return new HttpClient(handler, disposeHandler: true);
  512. });
  513. });
  514. return new SteamClient(config);
  515. }
  516. HttpClientHandler CreateHttpClientHandler(string? requestProxyUrl, ILogger logger, string endpointName)
  517. {
  518. var (proxyUrl, source) = ResolveProxyUrl(requestProxyUrl);
  519. logger.LogInformation(
  520. "[{Endpoint}] Proxy resolved. source={Source}, value={Proxy}",
  521. endpointName,
  522. source,
  523. MaskProxyUrlForLog(proxyUrl)
  524. );
  525. var handler = new HttpClientHandler();
  526. if (string.IsNullOrWhiteSpace(proxyUrl))
  527. {
  528. logger.LogInformation("[{Endpoint}] Proxy disabled for this request.", endpointName);
  529. return handler;
  530. }
  531. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var proxyUri))
  532. {
  533. logger.LogWarning(
  534. "[{Endpoint}] Proxy url invalid. raw={RawProxy}",
  535. endpointName,
  536. MaskProxyUrlForLog(proxyUrl)
  537. );
  538. throw new ArgumentException("invalid proxy url, expected format like http://127.0.0.1:7890");
  539. }
  540. handler.Proxy = new WebProxy(proxyUri);
  541. handler.UseProxy = true;
  542. return handler;
  543. }
  544. (string? ProxyUrl, string Source) ResolveProxyUrl(string? requestProxyUrl)
  545. {
  546. if (!string.IsNullOrWhiteSpace(requestProxyUrl))
  547. {
  548. return (requestProxyUrl, "request.ProxyUrl");
  549. }
  550. var envProxy = Environment.GetEnvironmentVariable("STEAM_PROXY_URL");
  551. if (!string.IsNullOrWhiteSpace(envProxy))
  552. {
  553. return (envProxy, "env.STEAM_PROXY_URL");
  554. }
  555. return (null, "none");
  556. }
  557. string MaskProxyUrlForLog(string? proxyUrl)
  558. {
  559. if (string.IsNullOrWhiteSpace(proxyUrl))
  560. {
  561. return "<none>";
  562. }
  563. if (!Uri.TryCreate(proxyUrl, UriKind.Absolute, out var uri))
  564. {
  565. var trimmed = proxyUrl.Trim();
  566. if (trimmed.Length > 64)
  567. {
  568. trimmed = trimmed.Substring(0, 64) + "...";
  569. }
  570. return $"<invalid:{trimmed}>";
  571. }
  572. var hostPort = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}";
  573. return $"{uri.Scheme}://{hostPort}";
  574. }
  575. record LoginRequest(string Username, string Password, string MaFilePath, string? ProxyUrl = null);
  576. record WalletBalanceRequest(string SteamLoginSecure, string? ProxyUrl = null);
  577. record TradeConfirmationsListRequest(string MaFilePath, SessionPayload Session, string? ProxyUrl = null);
  578. record TradeConfirmationOperateRequest(
  579. string MaFilePath,
  580. SessionPayload Session,
  581. ulong ConfirmationId,
  582. ulong? ConfirmationKey,
  583. string Operation,
  584. string? ProxyUrl = null
  585. );
  586. record SessionPayload(ulong SteamId, string AccessToken, string RefreshToken, string? SessionId = null);
  587. internal class FileAuthenticator : IAuthenticator
  588. {
  589. private readonly SteamGuardAccount? account;
  590. private int deviceCodesGenerated = 0;
  591. public FileAuthenticator(SteamGuardAccount? account)
  592. {
  593. this.account = account;
  594. }
  595. public Task<bool> AcceptDeviceConfirmationAsync()
  596. {
  597. return Task.FromResult(false);
  598. }
  599. public async Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect)
  600. {
  601. if (previousCodeWasIncorrect)
  602. {
  603. if (deviceCodesGenerated > 2)
  604. throw new Exception("failed to generate device code from maFile");
  605. await Task.Delay(30000);
  606. }
  607. if (account == null) return null;
  608. deviceCodesGenerated++;
  609. var code = await account.GenerateSteamGuardCodeAsync();
  610. return code ?? "";
  611. }
  612. public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect)
  613. {
  614. // This implementation does not support interactive email codes.
  615. return Task.FromResult(string.Empty);
  616. }
  617. }