mirror of
https://github.com/JustArchiNET/ArchiSteamFarm
synced 2024-11-10 07:04:27 +00:00
Add support for full OpenID procedure against ArchiNet
This commit is contained in:
parent
abc9a9ef04
commit
defc1bf80f
4 changed files with 152 additions and 18 deletions
|
@ -37,7 +37,7 @@ using ArchiSteamFarm.Web.Responses;
|
|||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
internal static class Backend {
|
||||
internal static async Task<HttpStatusCode?> AnnounceForListing(Bot bot, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
internal static async Task<BasicResponse?> AnnounceForListing(Bot bot, IReadOnlyList<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken, string? nickname = null, string? avatarHash = null) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if ((inventory == null) || (inventory.Count == 0)) {
|
||||
|
@ -60,9 +60,7 @@ internal static class Backend {
|
|||
|
||||
AnnouncementRequest data = new(ASF.GlobalDatabase?.Identifier ?? Guid.NewGuid(), bot.SteamID, tradeToken, inventory, acceptedMatchableTypes, bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything), ASF.GlobalConfig?.MaxTradeHoldDuration ?? GlobalConfig.DefaultMaxTradeHoldDuration, nickname, avatarHash);
|
||||
|
||||
BasicResponse? response = await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
|
||||
return response?.StatusCode;
|
||||
return await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken) {
|
||||
|
|
|
@ -35,9 +35,11 @@ using ArchiSteamFarm.Steam;
|
|||
using ArchiSteamFarm.Steam.Cards;
|
||||
using ArchiSteamFarm.Steam.Data;
|
||||
using ArchiSteamFarm.Steam.Exchange;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Steam.Security;
|
||||
using ArchiSteamFarm.Steam.Storage;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
|
||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||
|
||||
|
@ -66,6 +68,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||
private DateTime LastPersonaStateRequest;
|
||||
private bool ShouldSendAnnouncementEarlier;
|
||||
private bool ShouldSendHeartBeats;
|
||||
private bool SignedInWithSteam;
|
||||
|
||||
internal RemoteCommunication(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
@ -202,34 +205,66 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!SignedInWithSteam) {
|
||||
HttpStatusCode? signInWithSteam = await ArchiNet.SignInWithSteam(Bot).ConfigureAwait(false);
|
||||
|
||||
if (signInWithSteam == null) {
|
||||
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signInWithSteam.Value.IsSuccessCode()) {
|
||||
// SignIn procedure failed and it wasn't a network error, hold off with future tries at least for a full day
|
||||
LastAnnouncement = DateTime.UtcNow.AddDays(1);
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SignedInWithSteam = true;
|
||||
}
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count));
|
||||
|
||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
||||
HttpStatusCode? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
|
||||
BasicResponse? response = await Backend.AnnounceForListing(Bot, inventory, acceptedMatchableTypes, tradeToken!, nickname, avatarHash).ConfigureAwait(false);
|
||||
|
||||
if (!response.HasValue) {
|
||||
if (response == null) {
|
||||
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We've got the response, regardless what happened, we've succeeded in a valid check
|
||||
LastAnnouncement = DateTime.UtcNow;
|
||||
ShouldSendAnnouncementEarlier = false;
|
||||
if (response.StatusCode.IsRedirectionCode()) {
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
if (response.Value.IsClientErrorCode()) {
|
||||
if (response.FinalUri.Host != ArchiWebHandler.SteamCommunityURL.Host) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(response.FinalUri), response.FinalUri));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We've expected the result, not the redirection to the sign in, we need to authenticate again
|
||||
SignedInWithSteam = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.StatusCode.IsClientErrorCode()) {
|
||||
// ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time
|
||||
ShouldSendHeartBeats = false;
|
||||
|
||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response));
|
||||
|
||||
switch (response) {
|
||||
switch (response.StatusCode) {
|
||||
case HttpStatusCode.Forbidden:
|
||||
// ArchiNet told us to stop submitting data for now
|
||||
LastAnnouncement = DateTime.UtcNow.AddYears(1);
|
||||
|
||||
break;
|
||||
return;
|
||||
#if NETFRAMEWORK
|
||||
case (HttpStatusCode) 429:
|
||||
#else
|
||||
|
@ -239,13 +274,17 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||
// ArchiNet told us to try again later
|
||||
LastAnnouncement = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
break;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
// There is something wrong with our payload or the server, we shouldn't retry for at least several hours
|
||||
LastAnnouncement = DateTime.UtcNow.AddHours(6);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LastHeartBeat = DateTime.UtcNow;
|
||||
LastAnnouncement = LastHeartBeat = DateTime.UtcNow;
|
||||
ShouldSendAnnouncementEarlier = false;
|
||||
ShouldSendHeartBeats = true;
|
||||
|
||||
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
|
||||
|
|
|
@ -23,9 +23,15 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Dom;
|
||||
using ArchiSteamFarm.Helpers;
|
||||
using ArchiSteamFarm.IPC.Responses;
|
||||
using ArchiSteamFarm.Steam;
|
||||
using ArchiSteamFarm.Steam.Integration;
|
||||
using ArchiSteamFarm.Web;
|
||||
using ArchiSteamFarm.Web.Responses;
|
||||
using SteamKit2;
|
||||
|
||||
|
@ -68,6 +74,97 @@ internal static class ArchiNet {
|
|||
return badBots?.Contains(steamID);
|
||||
}
|
||||
|
||||
internal static async Task<HttpStatusCode?> SignInWithSteam(Bot bot) {
|
||||
ArgumentNullException.ThrowIfNull(bot);
|
||||
|
||||
if (!bot.IsConnectedAndLoggedOn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We expect data or redirection to Steam OpenID
|
||||
Uri authenticateRequest = new(URL, $"/Api/Steam/Authenticate?steamID={bot.SteamID}");
|
||||
|
||||
ObjectResponse<GenericResponse<ulong>>? authenticateResponse = await bot.ArchiWebHandler.WebBrowser.UrlGetToJsonObject<GenericResponse<ulong>>(authenticateRequest, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
||||
|
||||
if (authenticateResponse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authenticateResponse.StatusCode.IsClientErrorCode()) {
|
||||
return authenticateResponse.StatusCode;
|
||||
}
|
||||
|
||||
if (authenticateResponse.StatusCode.IsSuccessCode()) {
|
||||
return authenticateResponse.Content?.Result == bot.SteamID ? HttpStatusCode.OK : HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
// We've got a redirection, initiate OpenID procedure by following it
|
||||
using HtmlDocumentResponse? challengeResponse = await bot.ArchiWebHandler.UrlGetToHtmlDocumentWithSession(authenticateResponse.FinalUri).ConfigureAwait(false);
|
||||
|
||||
if (challengeResponse?.Content == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
IAttr? paramsNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='openidparams']/@value");
|
||||
|
||||
if (paramsNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(paramsNode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string paramsValue = paramsNode.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(paramsValue)) {
|
||||
ASF.ArchiLogger.LogNullError(paramsValue);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
IAttr? nonceNode = challengeResponse.Content.SelectSingleNode<IAttr>("//input[@name='nonce']/@value");
|
||||
|
||||
if (nonceNode == null) {
|
||||
ASF.ArchiLogger.LogNullError(nonceNode);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string nonceValue = nonceNode.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(nonceValue)) {
|
||||
ASF.ArchiLogger.LogNullError(nonceValue);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri loginRequest = new(ArchiWebHandler.SteamCommunityURL, "/openid/login");
|
||||
|
||||
using StringContent actionContent = new("steam_openid_login");
|
||||
using StringContent modeContent = new("checkid_setup");
|
||||
using StringContent paramsContent = new(paramsValue);
|
||||
using StringContent nonceContent = new(nonceValue);
|
||||
|
||||
using MultipartFormDataContent data = new();
|
||||
|
||||
data.Add(actionContent, "action");
|
||||
data.Add(modeContent, "openid.mode");
|
||||
data.Add(paramsContent, "openidparams");
|
||||
data.Add(nonceContent, "nonce");
|
||||
|
||||
// Accept OpenID request presented and follow redirection back to the data we initially expected
|
||||
authenticateResponse = await bot.ArchiWebHandler.WebBrowser.UrlPostToJsonObject<GenericResponse<ulong>, MultipartFormDataContent>(loginRequest, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false);
|
||||
|
||||
if (authenticateResponse == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authenticateResponse.StatusCode.IsClientErrorCode()) {
|
||||
return authenticateResponse.StatusCode;
|
||||
}
|
||||
|
||||
return authenticateResponse.Content?.Result == bot.SteamID ? HttpStatusCode.OK : HttpStatusCode.Unauthorized;
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, IReadOnlyCollection<ulong>? Result)> ResolveCachedBadBots() {
|
||||
if (ASF.GlobalDatabase == null) {
|
||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||
|
|
|
@ -25,7 +25,7 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace ArchiSteamFarm.IPC.Responses;
|
||||
|
||||
public sealed class GenericResponse<T> : GenericResponse where T : class {
|
||||
public sealed class GenericResponse<T> : GenericResponse {
|
||||
/// <summary>
|
||||
/// The actual result of the request, if available.
|
||||
/// </summary>
|
||||
|
@ -35,7 +35,7 @@ public sealed class GenericResponse<T> : GenericResponse where T : class {
|
|||
[JsonProperty]
|
||||
public T? Result { get; private set; }
|
||||
|
||||
public GenericResponse(T? result) : base(result != null) => Result = result;
|
||||
public GenericResponse(T? result) : base(result is not null) => Result = result;
|
||||
public GenericResponse(bool success, string? message) : base(success, message) { }
|
||||
public GenericResponse(bool success, T? result) : base(success) => Result = result;
|
||||
public GenericResponse(bool success, string? message, T? result) : base(success, message) => Result = result;
|
||||
|
|
Loading…
Reference in a new issue