Add support for full OpenID procedure against ArchiNet

This commit is contained in:
Archi 2022-12-17 17:23:20 +01:00
parent abc9a9ef04
commit defc1bf80f
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
4 changed files with 152 additions and 18 deletions

View file

@ -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) {

View file

@ -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);

View file

@ -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));

View file

@ -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;