mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 06:00:46 +00:00
Add support for full OpenID procedure against ArchiNet
This commit is contained in:
@@ -37,7 +37,7 @@ using ArchiSteamFarm.Web.Responses;
|
|||||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||||
|
|
||||||
internal static class Backend {
|
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);
|
ArgumentNullException.ThrowIfNull(bot);
|
||||||
|
|
||||||
if ((inventory == null) || (inventory.Count == 0)) {
|
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);
|
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 await bot.ArchiWebHandler.WebBrowser.UrlPost(request, data: data, requestOptions: WebBrowser.ERequestOptions.ReturnRedirections | WebBrowser.ERequestOptions.ReturnClientErrors).ConfigureAwait(false);
|
||||||
|
|
||||||
return response?.StatusCode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static async Task<(HttpStatusCode StatusCode, ImmutableHashSet<ListedUser> Users)?> GetListedUsersForMatching(Guid licenseID, Bot bot, IReadOnlyCollection<Asset> inventory, IReadOnlyCollection<Asset.EType> acceptedMatchableTypes, string tradeToken) {
|
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.Cards;
|
||||||
using ArchiSteamFarm.Steam.Data;
|
using ArchiSteamFarm.Steam.Data;
|
||||||
using ArchiSteamFarm.Steam.Exchange;
|
using ArchiSteamFarm.Steam.Exchange;
|
||||||
|
using ArchiSteamFarm.Steam.Integration;
|
||||||
using ArchiSteamFarm.Steam.Security;
|
using ArchiSteamFarm.Steam.Security;
|
||||||
using ArchiSteamFarm.Steam.Storage;
|
using ArchiSteamFarm.Steam.Storage;
|
||||||
using ArchiSteamFarm.Storage;
|
using ArchiSteamFarm.Storage;
|
||||||
|
using ArchiSteamFarm.Web.Responses;
|
||||||
|
|
||||||
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
namespace ArchiSteamFarm.OfficialPlugins.ItemsMatcher;
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||||||
private DateTime LastPersonaStateRequest;
|
private DateTime LastPersonaStateRequest;
|
||||||
private bool ShouldSendAnnouncementEarlier;
|
private bool ShouldSendAnnouncementEarlier;
|
||||||
private bool ShouldSendHeartBeats;
|
private bool ShouldSendHeartBeats;
|
||||||
|
private bool SignedInWithSteam;
|
||||||
|
|
||||||
internal RemoteCommunication(Bot bot) {
|
internal RemoteCommunication(Bot bot) {
|
||||||
ArgumentNullException.ThrowIfNull(bot);
|
ArgumentNullException.ThrowIfNull(bot);
|
||||||
@@ -202,34 +205,66 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||||||
return;
|
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));
|
Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Localization.Strings.ListingAnnouncing, Bot.SteamID, nickname, inventory.Count));
|
||||||
|
|
||||||
// ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework
|
// 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
|
// This is actually a network failure, so we'll stop sending heartbeats but not record it as valid check
|
||||||
ShouldSendHeartBeats = false;
|
ShouldSendHeartBeats = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've got the response, regardless what happened, we've succeeded in a valid check
|
if (response.StatusCode.IsRedirectionCode()) {
|
||||||
LastAnnouncement = DateTime.UtcNow;
|
ShouldSendHeartBeats = false;
|
||||||
ShouldSendAnnouncementEarlier = 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
|
// ArchiNet told us that we've sent a bad request, so the process should restart from the beginning at later time
|
||||||
ShouldSendHeartBeats = false;
|
ShouldSendHeartBeats = false;
|
||||||
|
|
||||||
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response));
|
Bot.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, response));
|
||||||
|
|
||||||
switch (response) {
|
switch (response.StatusCode) {
|
||||||
case HttpStatusCode.Forbidden:
|
case HttpStatusCode.Forbidden:
|
||||||
// ArchiNet told us to stop submitting data for now
|
// ArchiNet told us to stop submitting data for now
|
||||||
LastAnnouncement = DateTime.UtcNow.AddYears(1);
|
LastAnnouncement = DateTime.UtcNow.AddYears(1);
|
||||||
|
|
||||||
break;
|
return;
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
case (HttpStatusCode) 429:
|
case (HttpStatusCode) 429:
|
||||||
#else
|
#else
|
||||||
@@ -239,13 +274,17 @@ internal sealed class RemoteCommunication : IAsyncDisposable, IDisposable {
|
|||||||
// ArchiNet told us to try again later
|
// ArchiNet told us to try again later
|
||||||
LastAnnouncement = DateTime.UtcNow.AddDays(1);
|
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;
|
ShouldSendHeartBeats = true;
|
||||||
|
|
||||||
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
|
Bot.ArchiLogger.LogGenericInfo(Strings.Success);
|
||||||
|
|||||||
@@ -23,9 +23,15 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AngleSharp.Dom;
|
||||||
using ArchiSteamFarm.Helpers;
|
using ArchiSteamFarm.Helpers;
|
||||||
using ArchiSteamFarm.IPC.Responses;
|
using ArchiSteamFarm.IPC.Responses;
|
||||||
|
using ArchiSteamFarm.Steam;
|
||||||
|
using ArchiSteamFarm.Steam.Integration;
|
||||||
|
using ArchiSteamFarm.Web;
|
||||||
using ArchiSteamFarm.Web.Responses;
|
using ArchiSteamFarm.Web.Responses;
|
||||||
using SteamKit2;
|
using SteamKit2;
|
||||||
|
|
||||||
@@ -68,6 +74,97 @@ internal static class ArchiNet {
|
|||||||
return badBots?.Contains(steamID);
|
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() {
|
private static async Task<(bool Success, IReadOnlyCollection<ulong>? Result)> ResolveCachedBadBots() {
|
||||||
if (ASF.GlobalDatabase == null) {
|
if (ASF.GlobalDatabase == null) {
|
||||||
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
throw new InvalidOperationException(nameof(ASF.WebBrowser));
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace ArchiSteamFarm.IPC.Responses;
|
namespace ArchiSteamFarm.IPC.Responses;
|
||||||
|
|
||||||
public sealed class GenericResponse<T> : GenericResponse where T : class {
|
public sealed class GenericResponse<T> : GenericResponse {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The actual result of the request, if available.
|
/// The actual result of the request, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,7 +35,7 @@ public sealed class GenericResponse<T> : GenericResponse where T : class {
|
|||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public T? Result { get; private set; }
|
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, string? message) : base(success, message) { }
|
||||||
public GenericResponse(bool success, T? result) : base(success) => Result = result;
|
public GenericResponse(bool success, T? result) : base(success) => Result = result;
|
||||||
public GenericResponse(bool success, string? message, T? result) : base(success, message) => Result = result;
|
public GenericResponse(bool success, string? message, T? result) : base(success, message) => Result = result;
|
||||||
|
|||||||
Reference in New Issue
Block a user