mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-20 16:28:38 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4ed24704 | ||
|
|
015c6b7bdf | ||
|
|
a17c1fc35a | ||
|
|
19e46ce78d | ||
|
|
cf6ee3b60d | ||
|
|
488003993f | ||
|
|
c64a6fabbc | ||
|
|
eb2751861d | ||
|
|
48a1cf1189 | ||
|
|
98e1d51a48 | ||
|
|
5e22c832a2 | ||
|
|
6523b30f10 | ||
|
|
8fefa7c5af | ||
|
|
b6a5b8942c | ||
|
|
cd622989d6 | ||
|
|
8251274bf8 | ||
|
|
dd63c97ced | ||
|
|
b17960096e | ||
|
|
83934af041 | ||
|
|
0421da30cd | ||
|
|
bf084a1ffb | ||
|
|
f3868b9acc | ||
|
|
7c3d195fd4 | ||
|
|
4d7294feac | ||
|
|
6877abed6e | ||
|
|
5353d755a0 | ||
|
|
d2dff449d1 | ||
|
|
d526c6c1ce | ||
|
|
49fc908f0f | ||
|
|
b3162ce390 | ||
|
|
9a2a37f2fc | ||
|
|
a90b3afcdd | ||
|
|
6c903b2433 | ||
|
|
00558441fe | ||
|
|
7e10e6ba81 | ||
|
|
3995b3a083 | ||
|
|
e6c9c3f0a9 | ||
|
|
c8fc25111e | ||
|
|
6b3cf5e788 | ||
|
|
afbd8d12b4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,9 +2,10 @@
|
||||
## ArchiSteamFarm
|
||||
#################
|
||||
|
||||
# Ignore all config files, apart from example.xml
|
||||
# Ignore all config files, apart from ones we want to include
|
||||
ArchiSteamFarm/config/*
|
||||
!ArchiSteamFarm/config/example.xml
|
||||
!ArchiSteamFarm/config/minimal.xml
|
||||
|
||||
#################
|
||||
## Eclipse
|
||||
|
||||
@@ -5,6 +5,8 @@ VisualStudioVersion = 14.0.23107.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArchiSteamFarm", "ArchiSteamFarm\ArchiSteamFarm.csproj", "{35AF7887-08B9-40E8-A5EA-797D8B60B30C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth\SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -15,6 +17,10 @@ Global
|
||||
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{35AF7887-08B9-40E8-A5EA-797D8B60B30C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2"/>
|
||||
</startup>
|
||||
</configuration>
|
||||
</configuration>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
using SteamKit2;
|
||||
using SteamKit2.Internal;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
@@ -151,5 +152,60 @@ namespace ArchiSteamFarm {
|
||||
Client.PostCallback(new NotificationCallback(notification));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Please remove me entirely once https://github.com/SteamRE/SteamKit/pull/217 gets merged
|
||||
internal void HackedLogOn(uint id, SteamUser.LogOnDetails details) {
|
||||
if (details == null) {
|
||||
throw new ArgumentNullException("details");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(details.Username) || (string.IsNullOrEmpty(details.Password) && string.IsNullOrEmpty(details.LoginKey))) {
|
||||
throw new ArgumentException("LogOn requires a username and password to be set in 'details'.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(details.LoginKey) && !details.ShouldRememberPassword) {
|
||||
// Prevent consumers from screwing this up.
|
||||
// If should_remember_password is false, the login_key is ignored server-side.
|
||||
// The inverse is not applicable (you can log in with should_remember_password and no login_key).
|
||||
throw new ArgumentException("ShouldRememberPassword is required to be set to true in order to use LoginKey.");
|
||||
}
|
||||
|
||||
if (!Client.IsConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
var logon = new ClientMsgProtobuf<CMsgClientLogon>(EMsg.ClientLogon);
|
||||
|
||||
SteamID steamId = new SteamID(details.AccountID, details.AccountInstance, Client.ConnectedUniverse, EAccountType.Individual);
|
||||
|
||||
logon.ProtoHeader.client_sessionid = 0;
|
||||
logon.ProtoHeader.steamid = steamId.ConvertToUInt64();
|
||||
|
||||
logon.Body.obfustucated_private_ip = id;
|
||||
|
||||
logon.Body.account_name = details.Username;
|
||||
logon.Body.password = details.Password;
|
||||
logon.Body.should_remember_password = details.ShouldRememberPassword;
|
||||
|
||||
logon.Body.protocol_version = MsgClientLogon.CurrentProtocol;
|
||||
logon.Body.client_os_type = (uint) details.ClientOSType;
|
||||
logon.Body.client_language = details.ClientLanguage;
|
||||
logon.Body.cell_id = details.CellID;
|
||||
|
||||
logon.Body.steam2_ticket_request = details.RequestSteam2Ticket;
|
||||
|
||||
logon.Body.client_package_version = 1771;
|
||||
|
||||
// steam guard
|
||||
logon.Body.auth_code = details.AuthCode;
|
||||
logon.Body.two_factor_code = details.TwoFactorCode;
|
||||
|
||||
logon.Body.login_key = details.LoginKey;
|
||||
|
||||
logon.Body.sha_sentryfile = details.SentryFileHash;
|
||||
logon.Body.eresult_sentryfile = (int) (details.SentryFileHash != null ? EResult.OK : EResult.FileNotFound);
|
||||
|
||||
Client.Send(logon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>ArchiSteamFarm</RootNamespace>
|
||||
<AssemblyName>ArchiSteamFarm</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
@@ -26,6 +26,7 @@
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<UseApplicationTrust>false</UseApplicationTrust>
|
||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -122,6 +123,15 @@
|
||||
<Content Include="config\example.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="config\minimal.xml">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SteamAuth\SteamAuth.csproj">
|
||||
<Project>{5ad0934e-f6c4-4ae5-83af-c788313b2a87}</Project>
|
||||
<Name>SteamAuth</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
@@ -132,6 +142,7 @@
|
||||
<PostBuildEvent Condition=" '$(OS)' != 'Unix' ">if $(ConfigurationName) == Release (
|
||||
mkdir "$(TargetDir)out" "$(TargetDir)out\config"
|
||||
copy "$(TargetDir)config\example.xml" "$(TargetDir)out\config"
|
||||
copy "$(TargetDir)config\minimal.xml" "$(TargetDir)out\config"
|
||||
"$(SolutionDir)tools\ILMerge.exe" /out:"$(TargetDir)out\ASF.exe" "$(TargetDir)$(TargetName).exe" "$(TargetDir)*.dll" /target:exe /targetplatform:v4,C:\Windows\Microsoft.NET\Framework64\v4.0.30319 /wildcards
|
||||
del "$(TargetDir)out\ASF.pdb"
|
||||
)</PostBuildEvent>
|
||||
|
||||
@@ -148,8 +148,31 @@ namespace ArchiSteamFarm {
|
||||
Logging.LogGenericInfo(Bot.BotName, "Failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bot.Trading.CheckTrades();
|
||||
internal async Task<bool?> IsLoggedIn() {
|
||||
if (SteamID == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HtmlDocument htmlDocument = await WebBrowser.UrlGetToHtmlDocument("http://steamcommunity.com/my/profile", SteamCookieDictionary).ConfigureAwait(false);
|
||||
if (htmlDocument == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='account_pulldown']");
|
||||
return htmlNode != null;
|
||||
}
|
||||
|
||||
internal async Task<bool> ReconnectIfNeeded() {
|
||||
bool? isLoggedIn = await IsLoggedIn().ConfigureAwait(false);
|
||||
if (isLoggedIn.HasValue && !isLoggedIn.Value) {
|
||||
Logging.LogGenericInfo(Bot.BotName, "Reconnecting because our sessionID expired!");
|
||||
Bot.SteamClient.Disconnect(); // Bot will handle reconnect
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal List<SteamTradeOffer> GetTradeOffers() {
|
||||
@@ -192,7 +215,9 @@ namespace ArchiSteamFarm {
|
||||
is_our_offer = trade["is_our_offer"].AsBoolean(),
|
||||
time_created = trade["time_created"].AsInteger(),
|
||||
time_updated = trade["time_updated"].AsInteger(),
|
||||
from_real_time_trade = trade["from_real_time_trade"].AsBoolean()
|
||||
from_real_time_trade = trade["from_real_time_trade"].AsBoolean(),
|
||||
escrow_end_date = trade["escrow_end_date"].AsInteger(),
|
||||
confirmation_method = (SteamTradeOffer.ETradeOfferConfirmationMethod) trade["confirmation_method"].AsInteger()
|
||||
};
|
||||
foreach (KeyValue item in trade["items_to_give"].Children) {
|
||||
tradeOffer.items_to_give.Add(new SteamItem {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
*/
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using SteamAuth;
|
||||
using SteamKit2;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -37,12 +39,13 @@ namespace ArchiSteamFarm {
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Bot> Bots = new ConcurrentDictionary<string, Bot>();
|
||||
|
||||
private readonly string ConfigFile, SentryFile;
|
||||
private readonly string ConfigFile, LoginKeyFile, MobileAuthenticatorFile, SentryFile;
|
||||
|
||||
internal readonly string BotName;
|
||||
|
||||
private bool LoggedInElsewhere = false;
|
||||
private bool IsRunning = false;
|
||||
private string AuthCode, TwoFactorAuth;
|
||||
private string AuthCode, LoginKey, TwoFactorAuth;
|
||||
|
||||
internal ArchiHandler ArchiHandler { get; private set; }
|
||||
internal ArchiWebHandler ArchiWebHandler { get; private set; }
|
||||
@@ -50,6 +53,7 @@ namespace ArchiSteamFarm {
|
||||
internal CardsFarmer CardsFarmer { get; private set; }
|
||||
internal SteamClient SteamClient { get; private set; }
|
||||
internal SteamFriends SteamFriends { get; private set; }
|
||||
internal SteamGuardAccount SteamGuardAccount { get; private set; }
|
||||
internal SteamUser SteamUser { get; private set; }
|
||||
internal Trading Trading { get; private set; }
|
||||
|
||||
@@ -63,8 +67,9 @@ namespace ArchiSteamFarm {
|
||||
internal ulong SteamMasterID { get; private set; } = 0;
|
||||
internal ulong SteamMasterClanID { get; private set; } = 0;
|
||||
internal bool CardDropsRestricted { get; private set; } = false;
|
||||
internal bool UseAsfAsMobileAuthenticator { get; private set; } = false;
|
||||
internal bool ShutdownOnFarmingFinished { get; private set; } = false;
|
||||
internal HashSet<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020 };
|
||||
internal HashSet<uint> Blacklist { get; private set; } = new HashSet<uint> { 303700, 335590, 368020, 425280 };
|
||||
internal bool Statistics { get; private set; } = true;
|
||||
|
||||
private static bool IsValidCdKey(string key) {
|
||||
@@ -105,6 +110,8 @@ namespace ArchiSteamFarm {
|
||||
BotName = botName;
|
||||
|
||||
ConfigFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".xml");
|
||||
LoginKeyFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".key");
|
||||
MobileAuthenticatorFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".auth");
|
||||
SentryFile = Path.Combine(Program.ConfigDirectoryPath, BotName + ".bin");
|
||||
|
||||
if (!ReadConfig()) {
|
||||
@@ -131,10 +138,15 @@ namespace ArchiSteamFarm {
|
||||
CallbackManager.Subscribe<SteamFriends.FriendsListCallback>(OnFriendsList);
|
||||
CallbackManager.Subscribe<SteamFriends.FriendMsgCallback>(OnFriendMsg);
|
||||
|
||||
if (UseAsfAsMobileAuthenticator && File.Exists(MobileAuthenticatorFile)) {
|
||||
SteamGuardAccount = JsonConvert.DeserializeObject<SteamGuardAccount>(File.ReadAllText(MobileAuthenticatorFile));
|
||||
}
|
||||
|
||||
SteamUser = SteamClient.GetHandler<SteamUser>();
|
||||
CallbackManager.Subscribe<SteamUser.AccountInfoCallback>(OnAccountInfo);
|
||||
CallbackManager.Subscribe<SteamUser.LoggedOffCallback>(OnLoggedOff);
|
||||
CallbackManager.Subscribe<SteamUser.LoggedOnCallback>(OnLoggedOn);
|
||||
CallbackManager.Subscribe<SteamUser.LoginKeyCallback>(OnLoginKey);
|
||||
CallbackManager.Subscribe<SteamUser.UpdateMachineAuthCallback>(OnMachineAuth);
|
||||
|
||||
CallbackManager.Subscribe<ArchiHandler.NotificationCallback>(OnNotification);
|
||||
@@ -148,6 +160,94 @@ namespace ArchiSteamFarm {
|
||||
var fireAndForget = Task.Run(async () => await Start().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
internal async Task AcceptAllConfirmations() {
|
||||
if (SteamGuardAccount == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await SteamGuardAccount.RefreshSessionAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
foreach (Confirmation confirmation in await SteamGuardAccount.FetchConfirmationsAsync().ConfigureAwait(false)) {
|
||||
if (SteamGuardAccount.AcceptConfirmation(confirmation)) {
|
||||
Logging.LogGenericInfo(BotName, "Accepting confirmation: Success!");
|
||||
} else {
|
||||
Logging.LogGenericWarning(BotName, "Accepting confirmation: Failed!");
|
||||
}
|
||||
}
|
||||
} catch (SteamGuardAccount.WGTokenInvalidException) {
|
||||
Logging.LogGenericWarning(BotName, "Accepting confirmation: Failed!");
|
||||
Logging.LogGenericWarning(BotName, "Confirmation could not be accepted because of invalid token exception");
|
||||
Logging.LogGenericWarning(BotName, "If issue persists, consider removing and readding ASF 2FA");
|
||||
}
|
||||
}
|
||||
|
||||
private bool LinkMobileAuthenticator() {
|
||||
if (SteamGuardAccount != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logging.LogGenericNotice(BotName, "Linking new ASF MobileAuthenticator...");
|
||||
UserLogin userLogin = new UserLogin(SteamLogin, SteamPassword);
|
||||
LoginResult loginResult;
|
||||
while ((loginResult = userLogin.DoLogin()) != LoginResult.LoginOkay) {
|
||||
switch (loginResult) {
|
||||
case LoginResult.NeedEmail:
|
||||
userLogin.EmailCode = Program.GetUserInput(BotName, Program.EUserInputType.SteamGuard);
|
||||
break;
|
||||
default:
|
||||
Logging.LogGenericError(BotName, "Unhandled situation: " + loginResult);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticatorLinker authenticatorLinker = new AuthenticatorLinker(userLogin.Session);
|
||||
|
||||
AuthenticatorLinker.LinkResult linkResult;
|
||||
while ((linkResult = authenticatorLinker.AddAuthenticator()) != AuthenticatorLinker.LinkResult.AwaitingFinalization) {
|
||||
switch (linkResult) {
|
||||
case AuthenticatorLinker.LinkResult.MustProvidePhoneNumber:
|
||||
authenticatorLinker.PhoneNumber = Program.GetUserInput(BotName, Program.EUserInputType.PhoneNumber);
|
||||
break;
|
||||
default:
|
||||
Logging.LogGenericError(BotName, "Unhandled situation: " + linkResult);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SteamGuardAccount = authenticatorLinker.LinkedAccount;
|
||||
|
||||
try {
|
||||
File.WriteAllText(MobileAuthenticatorFile, JsonConvert.SerializeObject(SteamGuardAccount));
|
||||
} catch (Exception e) {
|
||||
Logging.LogGenericException(BotName, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticatorLinker.FinalizeResult finalizeResult = authenticatorLinker.FinalizeAddAuthenticator(Program.GetUserInput(BotName, Program.EUserInputType.SMS));
|
||||
if (finalizeResult != AuthenticatorLinker.FinalizeResult.Success) {
|
||||
Logging.LogGenericError(BotName, "Unhandled situation: " + finalizeResult);
|
||||
DelinkMobileAuthenticator();
|
||||
return false;
|
||||
}
|
||||
|
||||
Logging.LogGenericInfo(BotName, "Successfully linked ASF as new mobile authenticator for this account!");
|
||||
Program.GetUserInput(BotName, Program.EUserInputType.RevocationCode, SteamGuardAccount.RevocationCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DelinkMobileAuthenticator() {
|
||||
if (SteamGuardAccount == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = SteamGuardAccount.DeactivateAuthenticator();
|
||||
SteamGuardAccount = null;
|
||||
File.Delete(MobileAuthenticatorFile);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ReadConfig() {
|
||||
if (!File.Exists(ConfigFile)) {
|
||||
return false;
|
||||
@@ -195,6 +295,9 @@ namespace ArchiSteamFarm {
|
||||
case "SteamMasterClanID":
|
||||
SteamMasterClanID = ulong.Parse(value);
|
||||
break;
|
||||
case "UseAsfAsMobileAuthenticator":
|
||||
UseAsfAsMobileAuthenticator = bool.Parse(value);
|
||||
break;
|
||||
case "CardDropsRestricted":
|
||||
CardDropsRestricted = bool.Parse(value);
|
||||
break;
|
||||
@@ -233,7 +336,12 @@ namespace ArchiSteamFarm {
|
||||
IsRunning = true;
|
||||
|
||||
Logging.LogGenericInfo(BotName, "Starting...");
|
||||
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
|
||||
|
||||
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
|
||||
if (TwoFactorAuth == null) {
|
||||
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
SteamClient.Connect();
|
||||
|
||||
var fireAndForget = Task.Run(() => HandleCallbacks());
|
||||
@@ -249,23 +357,24 @@ namespace ArchiSteamFarm {
|
||||
SteamClient.Disconnect();
|
||||
}
|
||||
|
||||
private async Task<bool> Shutdown(string botNameToShutdown) {
|
||||
Bot botToShutdown;
|
||||
if (!Bots.TryGetValue(botNameToShutdown, out botToShutdown)) {
|
||||
return false;
|
||||
internal async Task<bool> Shutdown(string botName = null) {
|
||||
Bot bot;
|
||||
|
||||
if (string.IsNullOrEmpty(botName)) {
|
||||
bot = this;
|
||||
} else {
|
||||
if (!Bots.TryGetValue(botName, out bot)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await botToShutdown.Stop().ConfigureAwait(false);
|
||||
Bots.TryRemove(botNameToShutdown, out botToShutdown);
|
||||
await bot.Stop().ConfigureAwait(false);
|
||||
Bots.TryRemove(bot.BotName, out bot);
|
||||
|
||||
Program.OnBotShutdown(botToShutdown);
|
||||
Program.OnBotShutdown();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal async Task<bool> Shutdown() {
|
||||
return await Shutdown(BotName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task OnFarmingFinished() {
|
||||
if (ShutdownOnFarmingFinished) {
|
||||
await Shutdown().ConfigureAwait(false);
|
||||
@@ -310,35 +419,88 @@ namespace ArchiSteamFarm {
|
||||
SendMessageToUser(steamID, "Currently " + Bots.Count + " bots are running");
|
||||
}
|
||||
|
||||
private void ResponseStart(ulong steamID, string botNameToStart) {
|
||||
if (steamID == 0 || string.IsNullOrEmpty(botNameToStart)) {
|
||||
private void Response2FA(ulong steamID, string botName = null) {
|
||||
if (steamID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Bots.ContainsKey(botNameToStart)) {
|
||||
Bot bot;
|
||||
|
||||
if (string.IsNullOrEmpty(botName)) {
|
||||
bot = this;
|
||||
} else {
|
||||
if (!Bots.TryGetValue(botName, out bot)) {
|
||||
SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (bot.SteamGuardAccount == null) {
|
||||
SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!");
|
||||
return;
|
||||
}
|
||||
|
||||
long timeLeft = 30 - TimeAligner.GetSteamTime() % 30;
|
||||
SendMessageToUser(steamID, "2FA Token: " + bot.SteamGuardAccount.GenerateSteamGuardCode() + " (expires in " + timeLeft + " seconds)");
|
||||
}
|
||||
|
||||
private void Response2FAOff(ulong steamID, string botName = null) {
|
||||
if (steamID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bot bot;
|
||||
|
||||
if (string.IsNullOrEmpty(botName)) {
|
||||
bot = this;
|
||||
} else {
|
||||
if (!Bots.TryGetValue(botName, out bot)) {
|
||||
SendMessageToUser(steamID, "Couldn't find any bot named " + botName + "!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (bot.SteamGuardAccount == null) {
|
||||
SendMessageToUser(steamID, "That bot doesn't have ASF 2FA enabled!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bot.DelinkMobileAuthenticator()) {
|
||||
SendMessageToUser(steamID, "Done! Bot is no longer using ASF 2FA");
|
||||
} else {
|
||||
SendMessageToUser(steamID, "Something went wrong!");
|
||||
}
|
||||
}
|
||||
|
||||
private void ResponseStart(ulong steamID, string botName) {
|
||||
if (steamID == 0 || string.IsNullOrEmpty(botName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Bots.ContainsKey(botName)) {
|
||||
SendMessageToUser(steamID, "That bot instance is already running!");
|
||||
return;
|
||||
}
|
||||
|
||||
new Bot(botNameToStart);
|
||||
if (Bots.ContainsKey(botNameToStart)) {
|
||||
new Bot(botName);
|
||||
if (Bots.ContainsKey(botName)) {
|
||||
SendMessageToUser(steamID, "Done!");
|
||||
} else {
|
||||
SendMessageToUser(steamID, "That bot instance failed to start, make sure that " + botNameToStart + ".xml config exists and bot is active!");
|
||||
SendMessageToUser(steamID, "That bot instance failed to start, make sure that XML config exists and bot is active!");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResponseStop(ulong steamID, string botNameToShutdown) {
|
||||
if (steamID == 0 || string.IsNullOrEmpty(botNameToShutdown)) {
|
||||
private async Task ResponseStop(ulong steamID, string botName) {
|
||||
if (steamID == 0 || string.IsNullOrEmpty(botName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Bots.ContainsKey(botNameToShutdown)) {
|
||||
if (!Bots.ContainsKey(botName)) {
|
||||
SendMessageToUser(steamID, "That bot instance is already inactive!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await Shutdown(botNameToShutdown).ConfigureAwait(false)) {
|
||||
if (await Shutdown(botName).ConfigureAwait(false)) {
|
||||
SendMessageToUser(steamID, "Done!");
|
||||
} else {
|
||||
SendMessageToUser(steamID, "That bot instance failed to shutdown!");
|
||||
@@ -361,6 +523,10 @@ namespace ArchiSteamFarm {
|
||||
|
||||
Logging.LogGenericInfo(BotName, "Connected to Steam!");
|
||||
|
||||
if (File.Exists(LoginKeyFile)) {
|
||||
LoginKey = File.ReadAllText(LoginKeyFile);
|
||||
}
|
||||
|
||||
byte[] sentryHash = null;
|
||||
if (File.Exists(SentryFile)) {
|
||||
byte[] sentryFileContent = File.ReadAllBytes(SentryFile);
|
||||
@@ -371,16 +537,19 @@ namespace ArchiSteamFarm {
|
||||
SteamLogin = Program.GetUserInput(BotName, Program.EUserInputType.Login);
|
||||
}
|
||||
|
||||
if (SteamPassword.Equals("null")) {
|
||||
if (SteamPassword.Equals("null") && string.IsNullOrEmpty(LoginKey)) {
|
||||
SteamPassword = Program.GetUserInput(BotName, Program.EUserInputType.Password);
|
||||
}
|
||||
|
||||
SteamUser.LogOn(new SteamUser.LogOnDetails {
|
||||
// TODO: We should use SteamUser.LogOn with proper LoginID once https://github.com/SteamRE/SteamKit/pull/217 gets merged
|
||||
ArchiHandler.HackedLogOn(Program.UniqueID, new SteamUser.LogOnDetails {
|
||||
Username = SteamLogin,
|
||||
Password = SteamPassword,
|
||||
AuthCode = AuthCode,
|
||||
LoginKey = LoginKey,
|
||||
TwoFactorCode = TwoFactorAuth,
|
||||
SentryFileHash = sentryHash
|
||||
SentryFileHash = sentryHash,
|
||||
ShouldRememberPassword = true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -397,8 +566,21 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
await CardsFarmer.StopFarming().ConfigureAwait(false);
|
||||
|
||||
Logging.LogGenericWarning(BotName, "Disconnected from Steam, reconnecting...");
|
||||
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
|
||||
|
||||
// 2FA tokens are expiring soon, use limiter only when we don't have any pending
|
||||
if (TwoFactorAuth == null) {
|
||||
await Program.LimitSteamRequestsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (LoggedInElsewhere) {
|
||||
LoggedInElsewhere = false;
|
||||
Logging.LogGenericWarning(BotName, "Account is being used elsewhere, will try reconnecting in 5 minutes...");
|
||||
await Utilities.SleepAsync(5 * 60 * 1000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
SteamClient.Connect();
|
||||
}
|
||||
|
||||
@@ -415,13 +597,11 @@ namespace ArchiSteamFarm {
|
||||
SteamID steamID = friend.SteamID;
|
||||
switch (steamID.AccountType) {
|
||||
case EAccountType.Clan:
|
||||
ArchiHandler.DeclineClanInvite(steamID);
|
||||
// TODO: Accept clan invites from master?
|
||||
break;
|
||||
default:
|
||||
if (steamID == SteamMasterID) {
|
||||
SteamFriends.AddFriend(steamID);
|
||||
} else {
|
||||
SteamFriends.RemoveFriend(steamID);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -458,6 +638,12 @@ namespace ArchiSteamFarm {
|
||||
|
||||
if (!message.Contains(" ")) {
|
||||
switch (message) {
|
||||
case "!2fa":
|
||||
Response2FA(steamID);
|
||||
break;
|
||||
case "!2faoff":
|
||||
Response2FAOff(steamID);
|
||||
break;
|
||||
case "!exit":
|
||||
await ShutdownAllBots().ConfigureAwait(false);
|
||||
break;
|
||||
@@ -479,6 +665,12 @@ namespace ArchiSteamFarm {
|
||||
} else {
|
||||
string[] args = message.Split(' ');
|
||||
switch (args[0]) {
|
||||
case "!2fa":
|
||||
Response2FA(steamID, args[1]);
|
||||
break;
|
||||
case "!2faoff":
|
||||
Response2FAOff(steamID, args[1]);
|
||||
break;
|
||||
case "!redeem":
|
||||
ArchiHandler.RedeemKey(args[1]);
|
||||
break;
|
||||
@@ -509,6 +701,14 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
Logging.LogGenericInfo(BotName, "Logged off of Steam: " + callback.Result);
|
||||
|
||||
switch (callback.Result) {
|
||||
case EResult.AlreadyLoggedInElsewhere:
|
||||
case EResult.LoggedInElsewhere:
|
||||
case EResult.LogonSessionReplaced:
|
||||
LoggedInElsewhere = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
|
||||
@@ -522,17 +722,40 @@ namespace ArchiSteamFarm {
|
||||
AuthCode = Program.GetUserInput(SteamLogin, Program.EUserInputType.SteamGuard);
|
||||
break;
|
||||
case EResult.AccountLoginDeniedNeedTwoFactor:
|
||||
TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
|
||||
if (SteamGuardAccount == null) {
|
||||
TwoFactorAuth = Program.GetUserInput(SteamLogin, Program.EUserInputType.TwoFactorAuthentication);
|
||||
} else {
|
||||
TwoFactorAuth = SteamGuardAccount.GenerateSteamGuardCode();
|
||||
}
|
||||
break;
|
||||
case EResult.InvalidPassword:
|
||||
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", will retry after a longer while");
|
||||
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result);
|
||||
await Stop().ConfigureAwait(false);
|
||||
await Utilities.SleepAsync(25 * 1000).ConfigureAwait(false); // Steam removes requirement of captcha after around 20 minutes
|
||||
|
||||
// InvalidPassword means usually that login key has expired, if we used it
|
||||
if (!string.IsNullOrEmpty(LoginKey)) {
|
||||
LoginKey = null;
|
||||
File.Delete(LoginKeyFile);
|
||||
Logging.LogGenericInfo(BotName, "Removed expired login key, reconnecting...");
|
||||
} else { // If we didn't use login key, InvalidPassword usually means we got captcha or other network-based throttling
|
||||
Logging.LogGenericInfo(BotName, "Will retry after 25 minutes...");
|
||||
await Utilities.SleepAsync(25 * 60 * 1000).ConfigureAwait(false); // Captcha disappears after around 20 minutes, so we make it 25
|
||||
}
|
||||
|
||||
// After all of that, try again
|
||||
await Start().ConfigureAwait(false);
|
||||
break;
|
||||
case EResult.OK:
|
||||
Logging.LogGenericInfo(BotName, "Successfully logged on!");
|
||||
|
||||
if (UseAsfAsMobileAuthenticator && TwoFactorAuth == null && SteamGuardAccount == null) {
|
||||
LinkMobileAuthenticator();
|
||||
}
|
||||
|
||||
// Reset one-time-only access tokens
|
||||
AuthCode = null;
|
||||
TwoFactorAuth = null;
|
||||
|
||||
if (!SteamNickname.Equals("null")) {
|
||||
SteamFriends.SetPersonaName(SteamNickname);
|
||||
}
|
||||
@@ -543,18 +766,21 @@ namespace ArchiSteamFarm {
|
||||
|
||||
await ArchiWebHandler.Init(SteamClient, callback.WebAPIUserNonce, callback.VanityURL, SteamParentalPIN).ConfigureAwait(false);
|
||||
|
||||
ulong clanID = SteamMasterClanID;
|
||||
if (clanID != 0) {
|
||||
SteamFriends.JoinChat(clanID);
|
||||
if (SteamMasterClanID != 0) {
|
||||
await ArchiWebHandler.JoinClan(SteamMasterClanID).ConfigureAwait(false);
|
||||
SteamFriends.JoinChat(SteamMasterClanID);
|
||||
}
|
||||
|
||||
if (Statistics) {
|
||||
SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
|
||||
await ArchiWebHandler.JoinClan(Program.ArchiSCFarmGroup).ConfigureAwait(false);
|
||||
SteamFriends.JoinChat(Program.ArchiSCFarmGroup);
|
||||
}
|
||||
|
||||
Trading.CheckTrades();
|
||||
|
||||
await CardsFarmer.StartFarming().ConfigureAwait(false);
|
||||
break;
|
||||
case EResult.ServiceUnavailable:
|
||||
case EResult.Timeout:
|
||||
case EResult.TryAnotherCM:
|
||||
Logging.LogGenericWarning(BotName, "Unable to login to Steam: " + result + ", retrying...");
|
||||
@@ -568,6 +794,15 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoginKey(SteamUser.LoginKeyCallback callback) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(LoginKeyFile, callback.LoginKey);
|
||||
SteamUser.AcceptNewLoginKey(callback);
|
||||
}
|
||||
|
||||
private void OnMachineAuth(SteamUser.UpdateMachineAuthCallback callback) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
using HtmlAgilityPack;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -37,7 +38,9 @@ namespace ArchiSteamFarm {
|
||||
|
||||
private readonly ManualResetEvent FarmResetEvent = new ManualResetEvent(false);
|
||||
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1);
|
||||
|
||||
private readonly Bot Bot;
|
||||
private readonly Timer Timer;
|
||||
|
||||
internal readonly ConcurrentDictionary<uint, double> GamesToFarm = new ConcurrentDictionary<uint, double>();
|
||||
internal readonly List<uint> CurrentGamesFarming = new List<uint>();
|
||||
@@ -46,6 +49,13 @@ namespace ArchiSteamFarm {
|
||||
|
||||
internal CardsFarmer(Bot bot) {
|
||||
Bot = bot;
|
||||
|
||||
Timer = new Timer(
|
||||
async e => await CheckGamesForFarming().ConfigureAwait(false),
|
||||
null,
|
||||
TimeSpan.FromMinutes(15), // Delay
|
||||
TimeSpan.FromMinutes(15) // Period
|
||||
);
|
||||
}
|
||||
|
||||
internal static List<uint> GetGamesToFarmSolo(ConcurrentDictionary<uint, double> gamesToFarm) {
|
||||
@@ -76,7 +86,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
internal bool FarmMultiple() {
|
||||
if (GamesToFarm == null || GamesToFarm.Count == 0) {
|
||||
if (GamesToFarm.Count == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,6 +143,28 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if farming is possible
|
||||
Logging.LogGenericInfo(Bot.BotName, "Checking possibility to farm...");
|
||||
NowFarming = true;
|
||||
Semaphore.Release();
|
||||
Bot.ArchiHandler.PlayGames(1337);
|
||||
|
||||
// We'll now either receive OnLoggedOff() with LoggedInElsewhere, or nothing happens
|
||||
if (await Task.Run(() => FarmResetEvent.WaitOne(5000)).ConfigureAwait(false)) { // If LoggedInElsewhere happens in 5 seconds from now, abort farming
|
||||
NowFarming = false;
|
||||
return;
|
||||
}
|
||||
|
||||
NowFarming = false;
|
||||
Logging.LogGenericInfo(Bot.BotName, "Farming is possible!");
|
||||
|
||||
await Semaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
if (await Bot.ArchiWebHandler.ReconnectIfNeeded().ConfigureAwait(false)) {
|
||||
Semaphore.Release();
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.LogGenericInfo(Bot.BotName, "Checking badges...");
|
||||
|
||||
// Find the number of badge pages
|
||||
@@ -279,7 +311,7 @@ namespace ArchiSteamFarm {
|
||||
|
||||
Logging.LogGenericInfo(Bot.BotName, "Sending signal to stop farming");
|
||||
FarmResetEvent.Set();
|
||||
while (NowFarming) {
|
||||
for (var i = 0; i < 5 && NowFarming; i++) {
|
||||
Logging.LogGenericInfo(Bot.BotName, "Waiting for reaction...");
|
||||
await Utilities.SleepAsync(1000).ConfigureAwait(false);
|
||||
}
|
||||
@@ -288,16 +320,31 @@ namespace ArchiSteamFarm {
|
||||
Semaphore.Release();
|
||||
}
|
||||
|
||||
private async Task<bool?> ShouldFarm(ulong appID) {
|
||||
bool? result = null;
|
||||
HtmlDocument gamePageDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
|
||||
if (gamePageDocument != null) {
|
||||
HtmlNode gamePageNode = gamePageDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']");
|
||||
if (gamePageNode != null) {
|
||||
result = !gamePageNode.InnerText.Contains("No card drops");
|
||||
}
|
||||
private async Task CheckGamesForFarming() {
|
||||
if (NowFarming || GamesToFarm.Count > 0) {
|
||||
return;
|
||||
}
|
||||
return result;
|
||||
|
||||
await StartFarming().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<bool?> ShouldFarm(ulong appID) {
|
||||
if (appID == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
|
||||
if (htmlDocument == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']");
|
||||
if (htmlNode == null) {
|
||||
await Bot.ArchiWebHandler.ReconnectIfNeeded().ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return !htmlNode.InnerText.Contains("No card drops");
|
||||
}
|
||||
|
||||
private async Task<bool> Farm(uint appID) {
|
||||
|
||||
@@ -29,9 +29,11 @@ using System.Runtime.CompilerServices;
|
||||
namespace ArchiSteamFarm {
|
||||
internal static class Logging {
|
||||
private static void Log(string message) {
|
||||
lock (Program.ConsoleLock) {
|
||||
Console.WriteLine(DateTime.Now + " " + message);
|
||||
if (Program.ConsoleIsBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine(DateTime.Now + " " + message);
|
||||
}
|
||||
|
||||
internal static void LogGenericError(string botName, string message, [CallerMemberName] string previousMethodName = "") {
|
||||
|
||||
@@ -34,8 +34,11 @@ namespace ArchiSteamFarm {
|
||||
internal enum EUserInputType {
|
||||
Login,
|
||||
Password,
|
||||
PhoneNumber,
|
||||
SMS,
|
||||
SteamGuard,
|
||||
SteamParentalPIN,
|
||||
RevocationCode,
|
||||
TwoFactorAuthentication,
|
||||
}
|
||||
|
||||
@@ -49,10 +52,13 @@ namespace ArchiSteamFarm {
|
||||
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
|
||||
private static readonly string ExecutablePath = Assembly.Location;
|
||||
private static readonly AssemblyName AssemblyName = Assembly.GetName();
|
||||
private static readonly object ConsoleLock = new object();
|
||||
//private static readonly string ExeName = AssemblyName.Name + ".exe";
|
||||
|
||||
internal static readonly uint UniqueID = (uint) Utilities.Random.Next();
|
||||
internal static readonly string Version = AssemblyName.Version.ToString();
|
||||
internal static readonly object ConsoleLock = new object();
|
||||
|
||||
internal static bool ConsoleIsBusy = false;
|
||||
|
||||
private static async Task CheckForUpdate() {
|
||||
JObject response = await WebBrowser.UrlGetToJObject(LatestGithubReleaseURL).ConfigureAwait(false);
|
||||
@@ -98,9 +104,10 @@ namespace ArchiSteamFarm {
|
||||
SteamSemaphore.Release();
|
||||
}
|
||||
|
||||
internal static string GetUserInput(string botLogin, EUserInputType userInputType) {
|
||||
internal static string GetUserInput(string botLogin, EUserInputType userInputType, string extraInformation = null) {
|
||||
string result;
|
||||
lock (ConsoleLock) {
|
||||
ConsoleIsBusy = true;
|
||||
switch (userInputType) {
|
||||
case EUserInputType.Login:
|
||||
Console.Write("<" + botLogin + "> Please enter your login: ");
|
||||
@@ -108,24 +115,36 @@ namespace ArchiSteamFarm {
|
||||
case EUserInputType.Password:
|
||||
Console.Write("<" + botLogin + "> Please enter your password: ");
|
||||
break;
|
||||
case EUserInputType.PhoneNumber:
|
||||
Console.Write("<" + botLogin + "> Please enter your full phone number (e.g. +1234567890): ");
|
||||
break;
|
||||
case EUserInputType.SMS:
|
||||
Console.Write("<" + botLogin + "> Please enter SMS code sent on your mobile: ");
|
||||
break;
|
||||
case EUserInputType.SteamGuard:
|
||||
Console.Write("<" + botLogin + "> Please enter the auth code sent to your email: ");
|
||||
break;
|
||||
case EUserInputType.SteamParentalPIN:
|
||||
Console.Write("<" + botLogin + "> Please enter steam parental PIN: ");
|
||||
break;
|
||||
case EUserInputType.RevocationCode:
|
||||
Console.WriteLine("<" + botLogin + "> PLEASE WRITE DOWN YOUR REVOCATION CODE: " + extraInformation);
|
||||
Console.WriteLine("<" + botLogin + "> THIS IS THE ONLY WAY TO NOT GET LOCKED OUT OF YOUR ACCOUNT!");
|
||||
Console.Write("<" + botLogin + "> Hit enter once ready...");
|
||||
break;
|
||||
case EUserInputType.TwoFactorAuthentication:
|
||||
Console.Write("<" + botLogin + "> Please enter your 2 factor auth code from your authenticator app: ");
|
||||
break;
|
||||
}
|
||||
result = Console.ReadLine();
|
||||
Console.Clear(); // For security purposes
|
||||
ConsoleIsBusy = false;
|
||||
}
|
||||
result = result.Trim(); // Get rid of all whitespace characters
|
||||
return result;
|
||||
|
||||
return result.Trim(); // Get rid of all whitespace characters
|
||||
}
|
||||
|
||||
internal static async void OnBotShutdown(Bot bot) {
|
||||
internal static async void OnBotShutdown() {
|
||||
if (Bot.GetRunningBotsCount() == 0) {
|
||||
Logging.LogGenericInfo("Main", "No bots are running, exiting");
|
||||
await Utilities.SleepAsync(5000).ConfigureAwait(false); // This might be the only message user gets, consider giving him some time
|
||||
@@ -169,7 +188,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
// Check if we got any bots running
|
||||
OnBotShutdown(null);
|
||||
OnBotShutdown();
|
||||
|
||||
ShutdownResetEvent.WaitOne();
|
||||
}
|
||||
|
||||
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("0.8.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.8.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
@@ -39,7 +39,14 @@ namespace ArchiSteamFarm {
|
||||
Declined,
|
||||
InvalidItems,
|
||||
EmailPending,
|
||||
EmailCanceled
|
||||
EmailCanceled,
|
||||
OnHold
|
||||
}
|
||||
|
||||
internal enum ETradeOfferConfirmationMethod {
|
||||
Invalid,
|
||||
Email,
|
||||
MobileApp
|
||||
}
|
||||
|
||||
internal string tradeofferid { get; set; }
|
||||
@@ -53,6 +60,8 @@ namespace ArchiSteamFarm {
|
||||
internal int time_created { get; set; }
|
||||
internal int time_updated { get; set; }
|
||||
internal bool from_real_time_trade { get; set; }
|
||||
internal int escrow_end_date { get; set; }
|
||||
internal ETradeOfferConfirmationMethod confirmation_method { get; set; }
|
||||
|
||||
// Extra
|
||||
private ulong _OtherSteamID64 = 0;
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace ArchiSteamFarm {
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
await Bot.AcceptAllConfirmations().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ParseTrade(SteamTradeOffer tradeOffer) {
|
||||
@@ -74,19 +75,11 @@ namespace ArchiSteamFarm {
|
||||
return;
|
||||
}
|
||||
|
||||
ulong otherSteamID = tradeOffer.OtherSteamID64;
|
||||
bool success = false;
|
||||
bool tradeAccepted = false;
|
||||
|
||||
if (tradeOffer.items_to_give.Count == 0 || otherSteamID == Bot.SteamMasterID) {
|
||||
tradeAccepted = true;
|
||||
success = await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
|
||||
if (tradeOffer.items_to_give.Count == 0 || tradeOffer.OtherSteamID64 == Bot.SteamMasterID) {
|
||||
Logging.LogGenericInfo(Bot.BotName, "Accepting trade: " + tradeID);
|
||||
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
|
||||
} else {
|
||||
success = Bot.ArchiWebHandler.DeclineTradeOffer(tradeID);
|
||||
}
|
||||
|
||||
if (tradeAccepted && success) {
|
||||
// Do whatever we want with success
|
||||
Logging.LogGenericInfo(Bot.BotName, "Ignoring trade: " + tradeID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,12 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ArchiSteamFarm {
|
||||
internal static class Utilities {
|
||||
private static readonly Random Random = new Random();
|
||||
|
||||
internal static int RandomNumber(int min, int max) {
|
||||
return Random.Next(min, max + 1);
|
||||
}
|
||||
|
||||
internal static byte RandomDice() {
|
||||
return (byte) RandomNumber(1, 6);
|
||||
}
|
||||
internal static readonly Random Random = new Random();
|
||||
|
||||
internal static async Task SleepAsync(int miliseconds) {
|
||||
await Task.Delay(miliseconds).ConfigureAwait(false);
|
||||
@@ -48,16 +39,25 @@ namespace ArchiSteamFarm {
|
||||
return 0;
|
||||
}
|
||||
|
||||
string resultString;
|
||||
try {
|
||||
Regex regexObj = new Regex(@"[^\d]");
|
||||
resultString = regexObj.Replace(inputString, "");
|
||||
} catch (ArgumentException e) {
|
||||
Logging.LogGenericException("Utilities", e);
|
||||
string resultString = OnlyNumbersString(inputString);
|
||||
if (string.IsNullOrEmpty(resultString)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ulong.Parse(resultString, CultureInfo.InvariantCulture);
|
||||
ulong result;
|
||||
if (!ulong.TryParse(resultString, out result)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static string OnlyNumbersString(string text) {
|
||||
if (string.IsNullOrEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Regex.Replace(text, @"[^\d]", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
using HtmlAgilityPack;
|
||||
/*
|
||||
_ _ _ ____ _ _____
|
||||
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
|
||||
Copyright 2015 Łukasz "JustArchi" Domeradzki
|
||||
Contact: JustArchi@JustArchi.net
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
using HtmlAgilityPack;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -17,6 +41,13 @@ namespace ArchiSteamFarm {
|
||||
|
||||
internal static void Init() {
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("ArchiSteamFarm/" + Program.Version);
|
||||
|
||||
// Don't limit maximum number of allowed concurrent connections
|
||||
// It's application's responsibility to handle that stuff
|
||||
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
|
||||
|
||||
// Don't use Expect100Continue, we don't need to do that
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
}
|
||||
|
||||
private static async Task<HttpResponseMessage> UrlRequest(string request, HttpMethod httpMethod, Dictionary<string, string> data = null, Dictionary<string, string> cookies = null, string referer = null) {
|
||||
@@ -54,10 +85,6 @@ namespace ArchiSteamFarm {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<!-- Every bot should have it's own unique .xml configuration file, this is example on which you can base on -->
|
||||
<!-- This is full-fledged example config, you may be also interested in minimal.xml for bare minimum one -->
|
||||
<!-- Default values used in config match default ASF values when given config property is not found -->
|
||||
|
||||
<!-- Notice, if you use special characters reserved for XML, you should escape them -->
|
||||
<!-- Escape table: [& - &] | [" - "] | [' - '] | [< - <] | [> - >] -->
|
||||
<!-- So e.g. if your SteamPassword is "foo&" you should write value="foo&" -->
|
||||
|
||||
<!-- Type of the property is a tip for you that defines what values you can use -->
|
||||
<!-- bool - Boolean value that can be only "true" or "false" -->
|
||||
<!-- string - Any sequence of characters, unless stated otherwise (keep in mind escape table above), with special treatment of "null" value -->
|
||||
<!-- uint - 32-bit unsigned integer, used mostly for steam appID -->
|
||||
<!-- ulong - 64-bit unsigned (long) integer, used mostly for representing steamID64 -->
|
||||
<!-- HashSet(uint) - Comma-separated list of unique 32-bit unsigned integers -->
|
||||
|
||||
<!-- Master switch to turn account on and off, set to "true" after you're done -->
|
||||
<!-- TIP: This bot instance won't run unless below switch is set to "true" -->
|
||||
<Enabled type="bool" value="false"/>
|
||||
@@ -30,23 +38,29 @@
|
||||
<!-- TIP: Most likely you don't want to change it. You can use "null" if you wish to enter PIN on every startup, 0 means there is no PIN -->
|
||||
<SteamParentalPIN type="string" value="0"/>
|
||||
|
||||
<!-- This is steamID64 of the bot-master - you, [Example: 76561198006963719] -->
|
||||
<!-- This is steamID64 of the bot-master - you, for example "76561198006963719" -->
|
||||
<!-- You can get one e.g. by logging in to http://steamrep.com/ -->
|
||||
<!-- TIP: You can use "0", but bot won't accept steam cd-keys or trades from anybody" -->
|
||||
<SteamMasterID type="ulong" value="0"/>
|
||||
|
||||
<!-- This defines clan of the master, bot will join chatroom of that clan automatically after logging in if set -->
|
||||
<!-- SteamMasterClanID could be found by visiting your group-page -->
|
||||
<!-- [Example: http://steamcommunity.com/groups/hellokitty/] -->
|
||||
<!-- Adding "memberslistxml/?xml=1" to the end of this url so it looks like -->
|
||||
<!-- [Example: http://steamcommunity.com/groups/hellokitty/memberslistxml/?xml=1] -->
|
||||
<!-- Finally replace the SteamMasterClanID value with groupID64 you got from your groups xml-file -->
|
||||
<!-- TIP: Most likely you don't want to change it -->
|
||||
<!-- This is steamID64 of the master clan (group). If defined, bot will join the group and the chat automatically after logging in -->
|
||||
<!-- The easiest way to get one is to check groups on your profile: http://steamcommunity.com/my/profile" -->
|
||||
<!-- Then copying the URL of "Leave group" link, it will look like this: javascript:leaveGroupPrompt('103582791440160998','Archi\'s SC Farm') -->
|
||||
<!-- The steamID64 we're looking for is there, in above example: "103582791440160998" -->
|
||||
<!-- TIP: If you don't have your own farming group, most likely you don't want to change it, 0 means there is no master group defined -->
|
||||
<SteamMasterClanID type="ulong" value="0"/>
|
||||
|
||||
<!-- This switch defines if you want to use built-in ASF two-factor-authentication (ASF 2FA) for this account -->
|
||||
<!-- The one and only purpose for this function is automating steam logins and steam trades, so you won't need to enter 2FA codes all the time -->
|
||||
<!-- This however defeats the whole purpose of two-factor-auth, and should be used on alt accounts ONLY -->
|
||||
<!-- You should read full documentation, along with explanation here: https://github.com/JustArchi/ArchiSteamFarm/wiki/Escrow -->
|
||||
<!-- WARNING, this option can potentially LOCK OUT YOUR ACCOUNT! If you don't fully understand this feature, DON'T USE IT! -->
|
||||
<UseAsfAsMobileAuthenticator type="bool" value="false"/>
|
||||
|
||||
<!-- This switch defines if the account has card drops restricted -->
|
||||
<!-- Restricted card drops means that the account doesn't receive any steam cards until it plays the game for at least 2 hours -->
|
||||
<!-- As there is no magical way to detect it by ASF, I made this option config-based switch -->
|
||||
<!-- TIP: Based on this parameter, ASF will try to choose the most optimized cards farming algorithm for this account -->
|
||||
<!-- TIP: Based on this parameter, ASF will try to choose the most optimal cards farming algorithm for this account -->
|
||||
<CardDropsRestricted type="bool" value="false"/>
|
||||
|
||||
<!-- This switch defines if bot should disconnect once farming is finished -->
|
||||
@@ -56,11 +70,13 @@
|
||||
<ShutdownOnFarmingFinished type="bool" value="false"/>
|
||||
|
||||
<!-- Comma-separated list of IDs that should not be considered for farming -->
|
||||
<!-- Default value includes appIDs that are wrongly appearing on the profile, e.g. Monster Summer Sale -->
|
||||
<!-- TIP: Most likely you don't want to change it -->
|
||||
<Blacklist type="HashSet(uint)" value="303700,335590,368020"/>
|
||||
<Blacklist type="HashSet(uint)" value="303700,335590,368020,425280"/>
|
||||
|
||||
<!-- This enables statistics for me - bot will join Archi's SC Farm group and chat -->
|
||||
<!-- This switch enables statistics for me - bot will join Archi's SC Farm group and chat -->
|
||||
<!-- Consider leaving it at "true", this way I can check how many running bots are active -->
|
||||
<!-- That directly affects my willings to work on the project, as I can see how many users are actually using it -->
|
||||
<!-- TIP: Group link is http://steamcommunity.com/groups/ascfarm -->
|
||||
<Statistics type="bool" value="true"/>
|
||||
</configuration>
|
||||
|
||||
8
ArchiSteamFarm/config/minimal.xml
Normal file
8
ArchiSteamFarm/config/minimal.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<!-- This is minimalistic config to "just make ASF work" -->
|
||||
<!-- For full-fledged config, please take a look at example.xml -->
|
||||
<Enabled type="bool" value="false"/>
|
||||
<SteamLogin type="string" value="null"/>
|
||||
<SteamPassword type="string" value="null"/>
|
||||
</configuration>
|
||||
43
README.md
43
README.md
@@ -3,36 +3,41 @@ ArchiSteamFarm
|
||||
|
||||
ASF is a C# application that allows you to farm steam cards using multiple steam accounts simultaneously. Unlike idle master which works only on one account at given time, requires steam client running in background, and launches additional processes imitiating "game playing" status, ASF doesn't require any steam client running in the background, doesn't launch any additional processes and is made to handle unlimited steam accounts at once. In addition to that, it's meant to be run on servers or other desktop-less machines, and features full Mono support, which makes it possible to launch on any Mono-supported operating system, such as Windows, Linux or OS X. ASF is based on, and possible, thanks to [SteamKit2](https://github.com/SteamRE/SteamKit).
|
||||
|
||||
ASF doesn't require and doesn't interfere in any way with Steam client. In addition to that, it no longer requires exclusive access to given account, which means that you can use your main account in Steam client, and use ASF for farming the same account at the same time. If you decide to launch a game, ASF will get disconnected, and resume farming once you finish playing your game, being as transparent as possible.
|
||||
|
||||
**Core features:**
|
||||
|
||||
- Automatically farm available games using any number of active accounts
|
||||
- Automatically accept friend requests sent from master
|
||||
- Automatically accept all trades coming from master
|
||||
- Automatically accept all steam cd-keys sent via chat from master
|
||||
- Possibility to choose the most efficient cards farming algorithm, based on given account
|
||||
- SteamGuard / SteamParental / 2FA support
|
||||
- Update notifications
|
||||
- Full Mono support, cross-OS compatibility
|
||||
- Automatically farm available games using any number of active accounts
|
||||
- Automatically accept friend requests sent from master
|
||||
- Automatically accept all trades coming from master
|
||||
- Automatically accept all steam cd-keys sent via chat from master
|
||||
- Possibility to choose the most efficient cards farming algorithm, based on given account
|
||||
- SteamGuard / SteamParental / 2FA support
|
||||
- Unique ASF 2FA mechanism allowing ASF to act as mobile authenticator (if needed)
|
||||
- ASF update notifications
|
||||
- Full Mono support, cross-OS compatibility
|
||||
|
||||
**Setting up:**
|
||||
|
||||
Each ASF bot is defined in it's own XML config file in `config` directory. ASF comes with included ```example.xml``` config file, on which you should base all of your bots. Simply copy ```example.xml``` to a new file, and edit properties inside. Don't forget to switch ```Enabled``` property to ```true``` once you're done, as this is the master switch which enables configured bot to launch.
|
||||
|
||||
ASF expects exclusive access to all steam accounts it's using, which means that you should not use the account which is currently being used by ASF at the same time. You may however ```!start``` and ```!stop``` bots during farming, as well as use config property such as ```ShutdownOnFarmingFinished``` which will automatically release account after farming is done.
|
||||
Each ASF bot is defined in it's own XML config file in `config` directory. ASF comes with included ```example.xml``` config file, on which you should base all of your bots. Simply copy ```example.xml``` to a new file, and edit properties inside. Don't forget to switch ```Enabled``` property to ```true``` once you're done, as this is the master switch which enables configured bot to launch. The most minimalistic setup to make ASF working is changing only ```Enabled```, ```SteamLogin``` and ```SteamPassword``` properties, everything else is more or less optional to enable additional features.
|
||||
|
||||
After you set up all your bots (their configs), you should launch ```ASF.exe```. If your accounts require additional steps to unlock, such as Steam guard code, you'll need to enter those too after ASF tries to launch given bot. If everything ended properly, you should notice in the console output, as well as on your Steam, that all of your bots automatically started cards farming.
|
||||
|
||||
ASF doesn't require and doesn't interfere in any way with Steam client, which means that you can be logged in to Steam client as your primary account with e.g. Idle Master in the background, and launch ASF for all other accounts (your alts) at the same time.
|
||||
ASF doesn't require and doesn't interfere in any way with Steam client, which means that you can be logged in to Steam client as your primary account, and launch ASF at the same time, for any number of accounts, including your main one (if needed).
|
||||
|
||||
**Current Commands:**
|
||||
|
||||
- `!exit` Stops whole ASF
|
||||
- `!farm` Restarts cards farming module. ASF automatically executes that if any cd-key is successfully claimed
|
||||
- `!redeem <KEY>` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat
|
||||
- `!start <BOT>` Starts given bot instance, after it was ```!stop```pped
|
||||
- `!status` Prints current status of ASF
|
||||
- `!stop` Stops current bot instance
|
||||
- `!stop <BOT>` Stops given bot instance
|
||||
- `!2fa` Generates temporary 2FA token for current bot instance
|
||||
- `!2fa <BOT>` Generates temporary 2FA token for given bot instance
|
||||
- `!2faoff` Deactivates 2FA for current bot instance
|
||||
- `!2faoff <BOT>` Deactivates 2FA for given bot instance
|
||||
- `!exit` Stops whole ASF
|
||||
- `!farm` Restarts cards farming module. ASF automatically executes that if any cd-key is successfully claimed
|
||||
- `!redeem <KEY>` Redeems cd-key on current bot instance. You can also paste cd-key directly to the chat
|
||||
- `!start <BOT>` Starts given bot instance
|
||||
- `!status` Prints current status of ASF
|
||||
- `!stop` Stops current bot instance
|
||||
- `!stop <BOT>` Stops given bot instance
|
||||
|
||||
> Commands can be executed via a private chat with your bot.
|
||||
> Remember that bot accepts commands only from ```SteamMasterID```. That property can be configured in the config.
|
||||
|
||||
12
SteamAuth/APIEndpoints.cs
Normal file
12
SteamAuth/APIEndpoints.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace SteamAuth
|
||||
{
|
||||
public static class APIEndpoints
|
||||
{
|
||||
public const string STEAMAPI_BASE = "https://api.steampowered.com";
|
||||
public const string COMMUNITY_BASE = "https://steamcommunity.com";
|
||||
public const string MOBILEAUTH_BASE = STEAMAPI_BASE + "/IMobileAuthService/%s/v0001";
|
||||
public static string MOBILEAUTH_GETWGTOKEN = MOBILEAUTH_BASE.Replace("%s", "GetWGToken");
|
||||
public const string TWO_FACTOR_BASE = STEAMAPI_BASE + "/ITwoFactorService/%s/v0001";
|
||||
public static string TWO_FACTOR_TIME_QUERY = TWO_FACTOR_BASE.Replace("%s", "QueryTime");
|
||||
}
|
||||
}
|
||||
277
SteamAuth/AuthenticatorLinker.cs
Normal file
277
SteamAuth/AuthenticatorLinker.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the linking process for a new mobile authenticator.
|
||||
/// </summary>
|
||||
public class AuthenticatorLinker
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Set to register a new phone number when linking. If a phone number is not set on the account, this must be set. If a phone number is set on the account, this must be null.
|
||||
/// </summary>
|
||||
public string PhoneNumber = null;
|
||||
|
||||
/// <summary>
|
||||
/// Randomly-generated device ID. Should only be generated once per linker.
|
||||
/// </summary>
|
||||
public string DeviceID { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// After the initial link step, if successful, this will be the SteamGuard data for the account. PLEASE save this somewhere after generating it; it's vital data.
|
||||
/// </summary>
|
||||
public SteamGuardAccount LinkedAccount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the authenticator has been fully finalized.
|
||||
/// </summary>
|
||||
public bool Finalized = false;
|
||||
|
||||
private SessionData _session;
|
||||
private CookieContainer _cookies;
|
||||
|
||||
public AuthenticatorLinker(SessionData session)
|
||||
{
|
||||
this._session = session;
|
||||
this.DeviceID = GenerateDeviceID();
|
||||
|
||||
this._cookies = new CookieContainer();
|
||||
session.AddCookies(_cookies);
|
||||
}
|
||||
|
||||
public LinkResult AddAuthenticator()
|
||||
{
|
||||
bool hasPhone = _hasPhoneAttached();
|
||||
if (hasPhone && PhoneNumber != null)
|
||||
return LinkResult.MustRemovePhoneNumber;
|
||||
if (!hasPhone && PhoneNumber == null)
|
||||
return LinkResult.MustProvidePhoneNumber;
|
||||
|
||||
if (!hasPhone)
|
||||
{
|
||||
if (!_addPhoneNumber())
|
||||
{
|
||||
return LinkResult.GeneralFailure;
|
||||
}
|
||||
}
|
||||
|
||||
var postData = new NameValueCollection();
|
||||
postData.Add("access_token", _session.OAuthToken);
|
||||
postData.Add("steamid", _session.SteamID.ToString());
|
||||
postData.Add("authenticator_type", "1");
|
||||
postData.Add("device_identifier", this.DeviceID);
|
||||
postData.Add("sms_phone_id", "1");
|
||||
|
||||
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/AddAuthenticator/v0001", "POST", postData);
|
||||
if (response == null) return LinkResult.GeneralFailure;
|
||||
|
||||
var addAuthenticatorResponse = JsonConvert.DeserializeObject<AddAuthenticatorResponse>(response);
|
||||
if (addAuthenticatorResponse == null || addAuthenticatorResponse.Response == null)
|
||||
{
|
||||
return LinkResult.GeneralFailure;
|
||||
}
|
||||
|
||||
if (addAuthenticatorResponse.Response.Status == 29)
|
||||
{
|
||||
return LinkResult.AuthenticatorPresent;
|
||||
}
|
||||
|
||||
if (addAuthenticatorResponse.Response.Status != 1)
|
||||
{
|
||||
return LinkResult.GeneralFailure;
|
||||
}
|
||||
|
||||
this.LinkedAccount = addAuthenticatorResponse.Response;
|
||||
LinkedAccount.Session = this._session;
|
||||
LinkedAccount.DeviceID = this.DeviceID;
|
||||
|
||||
return LinkResult.AwaitingFinalization;
|
||||
}
|
||||
|
||||
public FinalizeResult FinalizeAddAuthenticator(string smsCode)
|
||||
{
|
||||
bool smsCodeGood = false;
|
||||
|
||||
var postData = new NameValueCollection();
|
||||
postData.Add("steamid", _session.SteamID.ToString());
|
||||
postData.Add("access_token", _session.OAuthToken);
|
||||
postData.Add("activation_code", smsCode);
|
||||
postData.Add("authenticator_code", "");
|
||||
int tries = 0;
|
||||
while (tries <= 30)
|
||||
{
|
||||
postData.Set("authenticator_code", tries == 0 ? "" : LinkedAccount.GenerateSteamGuardCode());
|
||||
postData.Add("authenticator_time", TimeAligner.GetSteamTime().ToString());
|
||||
|
||||
if(smsCodeGood)
|
||||
postData.Set("activation_code", "");
|
||||
|
||||
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/FinalizeAddAuthenticator/v0001", "POST", postData);
|
||||
if (response == null) return FinalizeResult.GeneralFailure;
|
||||
|
||||
var finalizeResponse = JsonConvert.DeserializeObject<FinalizeAuthenticatorResponse>(response);
|
||||
|
||||
if (finalizeResponse == null || finalizeResponse.Response == null)
|
||||
{
|
||||
return FinalizeResult.GeneralFailure;
|
||||
}
|
||||
|
||||
if(finalizeResponse.Response.Status == 89)
|
||||
{
|
||||
return FinalizeResult.BadSMSCode;
|
||||
}
|
||||
|
||||
if(finalizeResponse.Response.Status == 88)
|
||||
{
|
||||
if(tries >= 30)
|
||||
{
|
||||
return FinalizeResult.UnableToGenerateCorrectCodes;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalizeResponse.Response.Success)
|
||||
{
|
||||
return FinalizeResult.GeneralFailure;
|
||||
}
|
||||
|
||||
if (finalizeResponse.Response.WantMore)
|
||||
{
|
||||
smsCodeGood = true;
|
||||
tries++;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.LinkedAccount.FullyEnrolled = true;
|
||||
return FinalizeResult.Success;
|
||||
}
|
||||
|
||||
return FinalizeResult.GeneralFailure;
|
||||
}
|
||||
|
||||
private bool _addPhoneNumber()
|
||||
{
|
||||
var postData = new NameValueCollection();
|
||||
postData.Add("op", "add_phone_number");
|
||||
postData.Add("arg", PhoneNumber);
|
||||
postData.Add("sessionid", _session.SessionID);
|
||||
|
||||
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
|
||||
if (response == null) return false;
|
||||
|
||||
var addPhoneNumberResponse = JsonConvert.DeserializeObject<AddPhoneResponse>(response);
|
||||
return addPhoneNumberResponse.Success;
|
||||
}
|
||||
|
||||
private bool _hasPhoneAttached()
|
||||
{
|
||||
var postData = new NameValueCollection();
|
||||
postData.Add("op", "has_phone");
|
||||
postData.Add("arg", "null");
|
||||
postData.Add("sessionid", _session.SessionID);
|
||||
|
||||
string response = SteamWeb.Request(APIEndpoints.COMMUNITY_BASE + "/steamguard/phoneajax", "POST", postData, _cookies);
|
||||
if (response == null) return false;
|
||||
|
||||
var hasPhoneResponse = JsonConvert.DeserializeObject<HasPhoneResponse>(response);
|
||||
return hasPhoneResponse.HasPhone;
|
||||
}
|
||||
|
||||
public enum LinkResult
|
||||
{
|
||||
MustProvidePhoneNumber, //No phone number on the account
|
||||
MustRemovePhoneNumber, //A phone number is already on the account
|
||||
AwaitingFinalization, //Must provide an SMS code
|
||||
GeneralFailure, //General failure (really now!)
|
||||
AuthenticatorPresent
|
||||
}
|
||||
|
||||
public enum FinalizeResult
|
||||
{
|
||||
BadSMSCode,
|
||||
UnableToGenerateCorrectCodes,
|
||||
Success,
|
||||
GeneralFailure
|
||||
}
|
||||
|
||||
private class AddAuthenticatorResponse
|
||||
{
|
||||
[JsonProperty("response")]
|
||||
public SteamGuardAccount Response { get; set; }
|
||||
}
|
||||
|
||||
private class FinalizeAuthenticatorResponse
|
||||
{
|
||||
[JsonProperty("response")]
|
||||
public FinalizeAuthenticatorInternalResponse Response { get; set; }
|
||||
|
||||
internal class FinalizeAuthenticatorInternalResponse
|
||||
{
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonProperty("server_time")]
|
||||
public long ServerTime { get; set; }
|
||||
|
||||
[JsonProperty("want_more")]
|
||||
public bool WantMore { get; set; }
|
||||
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class HasPhoneResponse
|
||||
{
|
||||
[JsonProperty("has_phone")]
|
||||
public bool HasPhone { get; set; }
|
||||
}
|
||||
|
||||
private class AddPhoneResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
public static string GenerateDeviceID()
|
||||
{
|
||||
using (var sha1 = new SHA1Managed())
|
||||
{
|
||||
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider();
|
||||
byte[] randomBytes = new byte[8];
|
||||
secureRandom.GetBytes(randomBytes);
|
||||
|
||||
byte[] hashedBytes = sha1.ComputeHash(randomBytes);
|
||||
string random32 = BitConverter.ToString(hashedBytes).Replace("-", "").Substring(0, 32).ToLower();
|
||||
|
||||
return "android:" + SplitOnRatios(random32, new[] { 8, 4, 4, 4, 12 }, "-");
|
||||
}
|
||||
}
|
||||
|
||||
private static string SplitOnRatios(string str, int[] ratios, string intermediate)
|
||||
{
|
||||
string result = "";
|
||||
|
||||
int pos = 0;
|
||||
for (int index = 0; index < ratios.Length; index++)
|
||||
{
|
||||
result += str.Substring(pos, ratios[index]);
|
||||
pos = ratios[index];
|
||||
|
||||
if (index < ratios.Length - 1)
|
||||
result += intermediate;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
SteamAuth/Confirmation.cs
Normal file
15
SteamAuth/Confirmation.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
public class Confirmation
|
||||
{
|
||||
public string ConfirmationID;
|
||||
public string ConfirmationKey;
|
||||
public string ConfirmationDescription;
|
||||
}
|
||||
}
|
||||
36
SteamAuth/Properties/AssemblyInfo.cs
Normal file
36
SteamAuth/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("SteamAuth")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("SteamAuth")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2015")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("5ad0934e-f6c4-4ae5-83af-c788313b2a87")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
40
SteamAuth/SessionData.cs
Normal file
40
SteamAuth/SessionData.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Net;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
public class SessionData
|
||||
{
|
||||
public string SessionID { get; set; }
|
||||
|
||||
public string SteamLogin { get; set; }
|
||||
|
||||
public string SteamLoginSecure { get; set; }
|
||||
|
||||
public string WebCookie { get; set; }
|
||||
|
||||
public string OAuthToken { get; set; }
|
||||
|
||||
public ulong SteamID { get; set; }
|
||||
|
||||
public void AddCookies(CookieContainer cookies)
|
||||
{
|
||||
cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com"));
|
||||
|
||||
cookies.Add(new Cookie("steamid", SteamID.ToString(), "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("steamLogin", SteamLogin, "/", ".steamcommunity.com")
|
||||
{
|
||||
HttpOnly = true
|
||||
});
|
||||
|
||||
cookies.Add(new Cookie("steamLoginSecure", SteamLoginSecure, "/", ".steamcommunity.com")
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("dob", "", "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("sessionid", this.SessionID, "/", ".steamcommunity.com"));
|
||||
}
|
||||
}
|
||||
}
|
||||
69
SteamAuth/SteamAuth.csproj
Normal file
69
SteamAuth/SteamAuth.csproj
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>SteamAuth</RootNamespace>
|
||||
<AssemblyName>SteamAuth</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.8.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="APIEndpoints.cs" />
|
||||
<Compile Include="AuthenticatorLinker.cs" />
|
||||
<Compile Include="Confirmation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="SessionData.cs" />
|
||||
<Compile Include="SteamGuardAccount.cs" />
|
||||
<Compile Include="SteamWeb.cs" />
|
||||
<Compile Include="TimeAligner.cs" />
|
||||
<Compile Include="UserLogin.cs" />
|
||||
<Compile Include="Util.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
28
SteamAuth/SteamAuth.sln
Normal file
28
SteamAuth/SteamAuth.sln
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.23107.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamAuth", "SteamAuth.csproj", "{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestBed", "..\TestBed\TestBed.csproj", "{8A732227-C090-4011-9F0A-51180CFE6271}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5AD0934E-F6C4-4AE5-83AF-C788313B2A87}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A732227-C090-4011-9F0A-51180CFE6271}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A732227-C090-4011-9F0A-51180CFE6271}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
419
SteamAuth/SteamGuardAccount.cs
Normal file
419
SteamAuth/SteamGuardAccount.cs
Normal file
@@ -0,0 +1,419 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
public class SteamGuardAccount
|
||||
{
|
||||
[JsonProperty("shared_secret")]
|
||||
public string SharedSecret { get; set; }
|
||||
|
||||
[JsonProperty("serial_number")]
|
||||
public string SerialNumber { get; set; }
|
||||
|
||||
[JsonProperty("revocation_code")]
|
||||
public string RevocationCode { get; set; }
|
||||
|
||||
[JsonProperty("uri")]
|
||||
public string URI { get; set; }
|
||||
|
||||
[JsonProperty("server_time")]
|
||||
public long ServerTime { get; set; }
|
||||
|
||||
[JsonProperty("account_name")]
|
||||
public string AccountName { get; set; }
|
||||
|
||||
[JsonProperty("token_gid")]
|
||||
public string TokenGID { get; set; }
|
||||
|
||||
[JsonProperty("identity_secret")]
|
||||
public string IdentitySecret { get; set; }
|
||||
|
||||
[JsonProperty("secret_1")]
|
||||
public string Secret1 { get; set; }
|
||||
|
||||
[JsonProperty("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
[JsonProperty("device_id")]
|
||||
public string DeviceID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the authenticator has actually been applied to the account.
|
||||
/// </summary>
|
||||
[JsonProperty("fully_enrolled")]
|
||||
public bool FullyEnrolled { get; set; }
|
||||
|
||||
public SessionData Session { get; set; }
|
||||
|
||||
private static byte[] steamGuardCodeTranslations = new byte[] { 50, 51, 52, 53, 54, 55, 56, 57, 66, 67, 68, 70, 71, 72, 74, 75, 77, 78, 80, 81, 82, 84, 86, 87, 88, 89 };
|
||||
|
||||
public bool DeactivateAuthenticator(int scheme = 2)
|
||||
{
|
||||
var postData = new NameValueCollection();
|
||||
postData.Add("steamid", this.Session.SteamID.ToString());
|
||||
postData.Add("steamguard_scheme", scheme.ToString());
|
||||
postData.Add("revocation_code", this.RevocationCode);
|
||||
postData.Add("access_token", this.Session.OAuthToken);
|
||||
|
||||
try
|
||||
{
|
||||
string response = SteamWeb.MobileLoginRequest(APIEndpoints.STEAMAPI_BASE + "/ITwoFactorService/RemoveAuthenticator/v0001", "POST", postData);
|
||||
var removeResponse = JsonConvert.DeserializeObject<RemoveAuthenticatorResponse>(response);
|
||||
|
||||
if (removeResponse == null || removeResponse.Response == null || !removeResponse.Response.Success) return false;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string GenerateSteamGuardCode()
|
||||
{
|
||||
return GenerateSteamGuardCodeForTime(TimeAligner.GetSteamTime());
|
||||
}
|
||||
|
||||
public string GenerateSteamGuardCodeForTime(long time)
|
||||
{
|
||||
if (this.SharedSecret == null || this.SharedSecret.Length == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
byte[] sharedSecretArray = Convert.FromBase64String(this.SharedSecret);
|
||||
byte[] timeArray = new byte[8];
|
||||
|
||||
time /= 30L;
|
||||
|
||||
for (int i = 8; i > 0; i--)
|
||||
{
|
||||
timeArray[i - 1] = (byte)time;
|
||||
time >>= 8;
|
||||
}
|
||||
|
||||
HMACSHA1 hmacGenerator = new HMACSHA1();
|
||||
hmacGenerator.Key = sharedSecretArray;
|
||||
byte[] hashedData = hmacGenerator.ComputeHash(timeArray);
|
||||
byte[] codeArray = new byte[5];
|
||||
try
|
||||
{
|
||||
byte b = (byte)(hashedData[19] & 0xF);
|
||||
int codePoint = (hashedData[b] & 0x7F) << 24 | (hashedData[b + 1] & 0xFF) << 16 | (hashedData[b + 2] & 0xFF) << 8 | (hashedData[b + 3] & 0xFF);
|
||||
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
codeArray[i] = steamGuardCodeTranslations[codePoint % steamGuardCodeTranslations.Length];
|
||||
codePoint /= steamGuardCodeTranslations.Length;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null; //Change later, catch-alls are bad!
|
||||
}
|
||||
return Encoding.UTF8.GetString(codeArray);
|
||||
}
|
||||
|
||||
public Confirmation[] FetchConfirmations()
|
||||
{
|
||||
string url = this.GenerateConfirmationURL();
|
||||
|
||||
CookieContainer cookies = new CookieContainer();
|
||||
this.Session.AddCookies(cookies);
|
||||
|
||||
string response = SteamWeb.Request(url, "GET", null, cookies);
|
||||
|
||||
/*So you're going to see this abomination and you're going to be upset.
|
||||
It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library.
|
||||
And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings.
|
||||
I'm sorry. */
|
||||
|
||||
Regex confIDRegex = new Regex("data-confid=\"(\\d+)\"");
|
||||
Regex confKeyRegex = new Regex("data-key=\"(\\d+)\"");
|
||||
Regex confDescRegex = new Regex("<div>((Confirm|Trade with|Sell -) .+)</div>");
|
||||
|
||||
if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response)))
|
||||
{
|
||||
if (response == null || !response.Contains("<div>Nothing to confirm</div>"))
|
||||
{
|
||||
throw new WGTokenInvalidException();
|
||||
}
|
||||
|
||||
return new Confirmation[0];
|
||||
}
|
||||
|
||||
MatchCollection confIDs = confIDRegex.Matches(response);
|
||||
MatchCollection confKeys = confKeyRegex.Matches(response);
|
||||
MatchCollection confDescs = confDescRegex.Matches(response);
|
||||
|
||||
List<Confirmation> ret = new List<Confirmation>();
|
||||
for (int i = 0; i < confIDs.Count; i++)
|
||||
{
|
||||
string confID = confIDs[i].Groups[1].Value;
|
||||
string confKey = confKeys[i].Groups[1].Value;
|
||||
string confDesc = confDescs[i].Groups[1].Value;
|
||||
Confirmation conf = new Confirmation()
|
||||
{
|
||||
ConfirmationDescription = confDesc,
|
||||
ConfirmationID = confID,
|
||||
ConfirmationKey = confKey
|
||||
};
|
||||
ret.Add(conf);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public async Task<Confirmation[]> FetchConfirmationsAsync()
|
||||
{
|
||||
string url = this.GenerateConfirmationURL();
|
||||
|
||||
CookieContainer cookies = new CookieContainer();
|
||||
this.Session.AddCookies(cookies);
|
||||
|
||||
string response = await SteamWeb.RequestAsync(url, "GET", null, cookies);
|
||||
|
||||
/*So you're going to see this abomination and you're going to be upset.
|
||||
It's understandable. But the thing is, regex for HTML -- while awful -- makes this way faster than parsing a DOM, plus we don't need another library.
|
||||
And because the data is always in the same place and same format... It's not as if we're trying to naturally understand HTML here. Just extract strings.
|
||||
I'm sorry. */
|
||||
|
||||
Regex confIDRegex = new Regex("data-confid=\"(\\d+)\"");
|
||||
Regex confKeyRegex = new Regex("data-key=\"(\\d+)\"");
|
||||
Regex confDescRegex = new Regex("<div>((Confirm|Trade with|Sell -) .+)</div>");
|
||||
|
||||
if (response == null || !(confIDRegex.IsMatch(response) && confKeyRegex.IsMatch(response) && confDescRegex.IsMatch(response)))
|
||||
{
|
||||
if (response == null || !response.Contains("<div>Nothing to confirm</div>"))
|
||||
{
|
||||
throw new WGTokenInvalidException();
|
||||
}
|
||||
|
||||
return new Confirmation[0];
|
||||
}
|
||||
|
||||
MatchCollection confIDs = confIDRegex.Matches(response);
|
||||
MatchCollection confKeys = confKeyRegex.Matches(response);
|
||||
MatchCollection confDescs = confDescRegex.Matches(response);
|
||||
|
||||
List<Confirmation> ret = new List<Confirmation>();
|
||||
for (int i = 0; i < confIDs.Count; i++)
|
||||
{
|
||||
string confID = confIDs[i].Groups[1].Value;
|
||||
string confKey = confKeys[i].Groups[1].Value;
|
||||
string confDesc = confDescs[i].Groups[1].Value;
|
||||
Confirmation conf = new Confirmation()
|
||||
{
|
||||
ConfirmationDescription = confDesc,
|
||||
ConfirmationID = confID,
|
||||
ConfirmationKey = confKey
|
||||
};
|
||||
ret.Add(conf);
|
||||
}
|
||||
|
||||
return ret.ToArray();
|
||||
}
|
||||
|
||||
public bool AcceptConfirmation(Confirmation conf)
|
||||
{
|
||||
return _sendConfirmationAjax(conf, "allow");
|
||||
}
|
||||
|
||||
public bool DenyConfirmation(Confirmation conf)
|
||||
{
|
||||
return _sendConfirmationAjax(conf, "cancel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool RefreshSession()
|
||||
{
|
||||
string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN;
|
||||
NameValueCollection postData = new NameValueCollection();
|
||||
postData.Add("access_token", this.Session.OAuthToken);
|
||||
|
||||
string response = SteamWeb.Request(url, "POST", postData);
|
||||
if (response == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var refreshResponse = JsonConvert.DeserializeObject<RefreshSessionDataResponse>(response);
|
||||
if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token))
|
||||
return false;
|
||||
|
||||
string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token;
|
||||
string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure;
|
||||
|
||||
this.Session.SteamLogin = token;
|
||||
this.Session.SteamLoginSecure = tokenSecure;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the Steam session. Necessary to perform confirmations if your session has expired or changed.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> RefreshSessionAsync()
|
||||
{
|
||||
string url = APIEndpoints.MOBILEAUTH_GETWGTOKEN;
|
||||
NameValueCollection postData = new NameValueCollection();
|
||||
postData.Add("access_token", this.Session.OAuthToken);
|
||||
|
||||
string response = await SteamWeb.RequestAsync(url, "POST", postData);
|
||||
if (response == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var refreshResponse = JsonConvert.DeserializeObject<RefreshSessionDataResponse>(response);
|
||||
if (refreshResponse == null || refreshResponse.Response == null || String.IsNullOrEmpty(refreshResponse.Response.Token))
|
||||
return false;
|
||||
|
||||
string token = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.Token;
|
||||
string tokenSecure = this.Session.SteamID + "%7C%7C" + refreshResponse.Response.TokenSecure;
|
||||
|
||||
this.Session.SteamLogin = token;
|
||||
this.Session.SteamLoginSecure = tokenSecure;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _sendConfirmationAjax(Confirmation conf, string op)
|
||||
{
|
||||
string url = APIEndpoints.COMMUNITY_BASE + "/mobileconf/ajaxop";
|
||||
string queryString = "?op=" + op + "&";
|
||||
queryString += GenerateConfirmationQueryParams(op);
|
||||
queryString += "&cid=" + conf.ConfirmationID + "&ck=" + conf.ConfirmationKey;
|
||||
url += queryString;
|
||||
|
||||
CookieContainer cookies = new CookieContainer();
|
||||
this.Session.AddCookies(cookies);
|
||||
string referer = GenerateConfirmationURL();
|
||||
|
||||
string response = SteamWeb.Request(url, "GET", null, cookies, null);
|
||||
if (response == null) return false;
|
||||
|
||||
SendConfirmationResponse confResponse = JsonConvert.DeserializeObject<SendConfirmationResponse>(response);
|
||||
return confResponse.Success;
|
||||
}
|
||||
|
||||
public string GenerateConfirmationURL(string tag = "conf")
|
||||
{
|
||||
string endpoint = APIEndpoints.COMMUNITY_BASE + "/mobileconf/conf?";
|
||||
string queryString = GenerateConfirmationQueryParams(tag);
|
||||
return endpoint + queryString;
|
||||
}
|
||||
|
||||
public string GenerateConfirmationQueryParams(string tag)
|
||||
{
|
||||
if (String.IsNullOrEmpty(DeviceID))
|
||||
DeviceID = AuthenticatorLinker.GenerateDeviceID();
|
||||
|
||||
long time = TimeAligner.GetSteamTime();
|
||||
return "p=" + this.DeviceID + "&a=" + this.Session.SteamID.ToString() + "&k=" + _generateConfirmationHashForTime(time, tag) + "&t=" + time + "&m=android&tag=" + tag;
|
||||
}
|
||||
|
||||
private string _generateConfirmationHashForTime(long time, string tag)
|
||||
{
|
||||
byte[] decode = Convert.FromBase64String(this.IdentitySecret);
|
||||
int n2 = 8;
|
||||
if (tag != null)
|
||||
{
|
||||
if (tag.Length > 32)
|
||||
{
|
||||
n2 = 8 + 32;
|
||||
}
|
||||
else
|
||||
{
|
||||
n2 = 8 + tag.Length;
|
||||
}
|
||||
}
|
||||
byte[] array = new byte[n2];
|
||||
int n3 = 8;
|
||||
while (true)
|
||||
{
|
||||
int n4 = n3 - 1;
|
||||
if (n3 <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
array[n4] = (byte)time;
|
||||
time >>= 8;
|
||||
n3 = n4;
|
||||
}
|
||||
if (tag != null)
|
||||
{
|
||||
Array.Copy(Encoding.UTF8.GetBytes(tag), 0, array, 8, n2 - 8);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HMACSHA1 hmacGenerator = new HMACSHA1();
|
||||
hmacGenerator.Key = decode;
|
||||
byte[] hashedData = hmacGenerator.ComputeHash(array);
|
||||
string encodedData = Convert.ToBase64String(hashedData, Base64FormattingOptions.None);
|
||||
string hash = WebUtility.UrlEncode(encodedData);
|
||||
return hash;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return null; //Fix soon: catch-all is BAD!
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Determine how to detect an invalid session.
|
||||
public class WGTokenInvalidException : Exception
|
||||
{
|
||||
}
|
||||
|
||||
private class RefreshSessionDataResponse
|
||||
{
|
||||
[JsonProperty("response")]
|
||||
public RefreshSessionDataInternalResponse Response { get; set; }
|
||||
internal class RefreshSessionDataInternalResponse
|
||||
{
|
||||
[JsonProperty("token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[JsonProperty("token_secure")]
|
||||
public string TokenSecure { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class RemoveAuthenticatorResponse
|
||||
{
|
||||
[JsonProperty("response")]
|
||||
public RemoveAuthenticatorInternalResponse Response { get; set; }
|
||||
|
||||
internal class RemoveAuthenticatorInternalResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class SendConfirmationResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
137
SteamAuth/SteamWeb.cs
Normal file
137
SteamAuth/SteamWeb.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
public class SteamWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// Perform a mobile login request
|
||||
/// </summary>
|
||||
/// <param name="url">API url</param>
|
||||
/// <param name="method">GET or POST</param>
|
||||
/// <param name="data">Name-data pairs</param>
|
||||
/// <param name="cookies">current cookie container</param>
|
||||
/// <returns>response body</returns>
|
||||
public static string MobileLoginRequest(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null)
|
||||
{
|
||||
return Request(url, method, data, cookies, headers, APIEndpoints.COMMUNITY_BASE + "/mobilelogin?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client");
|
||||
}
|
||||
|
||||
public static string Request(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE)
|
||||
{
|
||||
string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key])))));
|
||||
if (method == "GET")
|
||||
{
|
||||
url += (url.Contains("?") ? "&" : "?") + query;
|
||||
}
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.Method = method;
|
||||
request.Accept = "text/javascript, text/html, application/xml, text/xml, */*";
|
||||
request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
|
||||
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
|
||||
request.Referer = referer;
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
request.Headers.Add(headers);
|
||||
}
|
||||
|
||||
if (cookies != null)
|
||||
{
|
||||
request.CookieContainer = cookies;
|
||||
}
|
||||
|
||||
if (method == "POST")
|
||||
{
|
||||
request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||
request.ContentLength = query.Length;
|
||||
|
||||
StreamWriter requestStream = new StreamWriter(request.GetRequestStream());
|
||||
requestStream.Write(query);
|
||||
requestStream.Close();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
||||
{
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
|
||||
{
|
||||
string responseData = responseStream.ReadToEnd();
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<string> RequestAsync(string url, string method, NameValueCollection data = null, CookieContainer cookies = null, NameValueCollection headers = null, string referer = APIEndpoints.COMMUNITY_BASE)
|
||||
{
|
||||
string query = (data == null ? string.Empty : string.Join("&", Array.ConvertAll(data.AllKeys, key => String.Format("{0}={1}", WebUtility.UrlEncode(key), WebUtility.UrlEncode(data[key])))));
|
||||
if (method == "GET")
|
||||
{
|
||||
url += (url.Contains("?") ? "&" : "?") + query;
|
||||
}
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
|
||||
request.Method = method;
|
||||
request.Accept = "text/javascript, text/html, application/xml, text/xml, */*";
|
||||
request.UserAgent = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-us; Google Nexus 4 - 4.1.1 - API 16 - 768x1280 Build/JRO03S) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
|
||||
request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
|
||||
request.Referer = referer;
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
request.Headers.Add(headers);
|
||||
}
|
||||
|
||||
if (cookies != null)
|
||||
{
|
||||
request.CookieContainer = cookies;
|
||||
}
|
||||
|
||||
if (method == "POST")
|
||||
{
|
||||
request.ContentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
||||
request.ContentLength = query.Length;
|
||||
|
||||
StreamWriter requestStream = new StreamWriter(request.GetRequestStream());
|
||||
requestStream.Write(query);
|
||||
requestStream.Close();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HttpWebResponse response = (HttpWebResponse) await request.GetResponseAsync();
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
|
||||
{
|
||||
string responseData = responseStream.ReadToEnd();
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
SteamAuth/TimeAligner.cs
Normal file
84
SteamAuth/TimeAligner.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to help align system time with the Steam server time. Not super advanced; probably not taking some things into account that it should.
|
||||
/// Necessary to generate up-to-date codes. In general, this will have an error of less than a second, assuming Steam is operational.
|
||||
/// </summary>
|
||||
public class TimeAligner
|
||||
{
|
||||
private static bool _aligned = false;
|
||||
private static int _timeDifference = 0;
|
||||
|
||||
public static long GetSteamTime()
|
||||
{
|
||||
if (!TimeAligner._aligned)
|
||||
{
|
||||
TimeAligner.AlignTime();
|
||||
}
|
||||
return Util.GetSystemUnixTime() + _timeDifference;
|
||||
}
|
||||
|
||||
public static async Task<long> GetSteamTimeAsync()
|
||||
{
|
||||
if (!TimeAligner._aligned)
|
||||
{
|
||||
await TimeAligner.AlignTimeAsync();
|
||||
}
|
||||
return Util.GetSystemUnixTime() + _timeDifference;
|
||||
}
|
||||
|
||||
public static void AlignTime()
|
||||
{
|
||||
long currentTime = Util.GetSystemUnixTime();
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
string response = client.UploadString(APIEndpoints.TWO_FACTOR_TIME_QUERY, "steamid=0");
|
||||
TimeQuery query = JsonConvert.DeserializeObject<TimeQuery>(response);
|
||||
TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime);
|
||||
TimeAligner._aligned = true;
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task AlignTimeAsync()
|
||||
{
|
||||
long currentTime = Util.GetSystemUnixTime();
|
||||
WebClient client = new WebClient();
|
||||
try
|
||||
{
|
||||
string response = await client.UploadStringTaskAsync(new Uri(APIEndpoints.TWO_FACTOR_TIME_QUERY), "steamid=0");
|
||||
TimeQuery query = JsonConvert.DeserializeObject<TimeQuery>(response);
|
||||
TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime);
|
||||
TimeAligner._aligned = true;
|
||||
}
|
||||
catch (WebException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
internal class TimeQuery
|
||||
{
|
||||
[JsonProperty("response")]
|
||||
internal TimeQueryResponse Response { get; set; }
|
||||
|
||||
internal class TimeQueryResponse
|
||||
{
|
||||
[JsonProperty("server_time")]
|
||||
public long ServerTime { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
254
SteamAuth/UserLogin.cs
Normal file
254
SteamAuth/UserLogin.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles logging the user into the mobile Steam website. Necessary to generate OAuth token and session cookies.
|
||||
/// </summary>
|
||||
public class UserLogin
|
||||
{
|
||||
public string Username;
|
||||
public string Password;
|
||||
public ulong SteamID;
|
||||
|
||||
public bool RequiresCaptcha;
|
||||
public string CaptchaGID = null;
|
||||
public string CaptchaText = null;
|
||||
|
||||
public bool RequiresEmail;
|
||||
public string EmailDomain = null;
|
||||
public string EmailCode = null;
|
||||
|
||||
public bool Requires2FA;
|
||||
public string TwoFactorCode = null;
|
||||
|
||||
public SessionData Session = null;
|
||||
public bool LoggedIn = false;
|
||||
|
||||
private CookieContainer _cookies = new CookieContainer();
|
||||
|
||||
public UserLogin(string username, string password)
|
||||
{
|
||||
this.Username = username;
|
||||
this.Password = password;
|
||||
}
|
||||
|
||||
public LoginResult DoLogin()
|
||||
{
|
||||
var postData = new NameValueCollection();
|
||||
var cookies = _cookies;
|
||||
string response = null;
|
||||
|
||||
if (cookies.Count == 0)
|
||||
{
|
||||
//Generate a SessionID
|
||||
cookies.Add(new Cookie("mobileClientVersion", "0 (2.1.3)", "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("mobileClient", "android", "/", ".steamcommunity.com"));
|
||||
cookies.Add(new Cookie("Steam_Language", "english", "/", ".steamcommunity.com"));
|
||||
|
||||
NameValueCollection headers = new NameValueCollection();
|
||||
headers.Add("X-Requested-With", "com.valvesoftware.android.steam.community");
|
||||
|
||||
SteamWeb.MobileLoginRequest("https://steamcommunity.com/login?oauth_client_id=DE45CD61&oauth_scope=read_profile%20write_profile%20read_client%20write_client", "GET", null, cookies, headers);
|
||||
}
|
||||
|
||||
postData.Add("username", this.Username);
|
||||
response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/getrsakey", "POST", postData, cookies);
|
||||
if (response == null || response.Contains("<BODY>\nAn error occurred while processing your request.")) return LoginResult.GeneralFailure;
|
||||
|
||||
var rsaResponse = JsonConvert.DeserializeObject<RSAResponse>(response);
|
||||
|
||||
if (!rsaResponse.Success)
|
||||
{
|
||||
return LoginResult.BadRSA;
|
||||
}
|
||||
|
||||
RNGCryptoServiceProvider secureRandom = new RNGCryptoServiceProvider();
|
||||
byte[] encryptedPasswordBytes;
|
||||
using (var rsaEncryptor = new RSACryptoServiceProvider())
|
||||
{
|
||||
var passwordBytes = Encoding.ASCII.GetBytes(this.Password);
|
||||
var rsaParameters = rsaEncryptor.ExportParameters(false);
|
||||
rsaParameters.Exponent = Util.HexStringToByteArray(rsaResponse.Exponent);
|
||||
rsaParameters.Modulus = Util.HexStringToByteArray(rsaResponse.Modulus);
|
||||
rsaEncryptor.ImportParameters(rsaParameters);
|
||||
encryptedPasswordBytes = rsaEncryptor.Encrypt(passwordBytes, false);
|
||||
}
|
||||
|
||||
string encryptedPassword = Convert.ToBase64String(encryptedPasswordBytes);
|
||||
|
||||
postData.Clear();
|
||||
postData.Add("username", this.Username);
|
||||
postData.Add("password", encryptedPassword);
|
||||
|
||||
postData.Add("twofactorcode", this.TwoFactorCode ?? "");
|
||||
|
||||
postData.Add("captchagid", this.RequiresCaptcha ? this.CaptchaGID : "-1");
|
||||
postData.Add("captcha_text", this.RequiresCaptcha ? this.CaptchaText : "");
|
||||
|
||||
postData.Add("emailsteamid", (this.Requires2FA || this.RequiresEmail) ? this.SteamID.ToString() : "");
|
||||
postData.Add("emailauth", this.RequiresEmail ? this.EmailCode : "");
|
||||
|
||||
postData.Add("rsatimestamp", rsaResponse.Timestamp);
|
||||
postData.Add("remember_login", "false");
|
||||
postData.Add("oauth_client_id", "DE45CD61");
|
||||
postData.Add("oauth_scope", "read_profile write_profile read_client write_client");
|
||||
postData.Add("loginfriendlyname", "#login_emailauth_friendlyname_mobile");
|
||||
postData.Add("donotcache", Util.GetSystemUnixTime().ToString());
|
||||
|
||||
response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/dologin", "POST", postData, cookies);
|
||||
if (response == null) return LoginResult.GeneralFailure;
|
||||
|
||||
var loginResponse = JsonConvert.DeserializeObject<LoginResponse>(response);
|
||||
|
||||
if (loginResponse.Message != null && loginResponse.Message.Contains("Incorrect login"))
|
||||
{
|
||||
return LoginResult.BadCredentials;
|
||||
}
|
||||
|
||||
if (loginResponse.CaptchaNeeded)
|
||||
{
|
||||
this.RequiresCaptcha = true;
|
||||
this.CaptchaGID = loginResponse.CaptchaGID;
|
||||
return LoginResult.NeedCaptcha;
|
||||
}
|
||||
|
||||
if (loginResponse.EmailAuthNeeded)
|
||||
{
|
||||
this.RequiresEmail = true;
|
||||
this.SteamID = loginResponse.EmailSteamID;
|
||||
return LoginResult.NeedEmail;
|
||||
}
|
||||
|
||||
if (loginResponse.TwoFactorNeeded && !loginResponse.Success)
|
||||
{
|
||||
this.Requires2FA = true;
|
||||
return LoginResult.Need2FA;
|
||||
}
|
||||
|
||||
if (loginResponse.Message != null && loginResponse.Message.Contains("too many login failures"))
|
||||
{
|
||||
return LoginResult.TooManyFailedLogins;
|
||||
}
|
||||
|
||||
if (loginResponse.OAuthData == null || loginResponse.OAuthData.OAuthToken == null || loginResponse.OAuthData.OAuthToken.Length == 0)
|
||||
{
|
||||
return LoginResult.GeneralFailure;
|
||||
}
|
||||
|
||||
if (!loginResponse.LoginComplete)
|
||||
{
|
||||
return LoginResult.BadCredentials;
|
||||
}
|
||||
else
|
||||
{
|
||||
var readableCookies = cookies.GetCookies(new Uri("https://steamcommunity.com"));
|
||||
var oAuthData = loginResponse.OAuthData;
|
||||
|
||||
SessionData session = new SessionData();
|
||||
session.OAuthToken = oAuthData.OAuthToken;
|
||||
session.SteamID = oAuthData.SteamID;
|
||||
session.SteamLogin = session.SteamID + "%7C%7C" + oAuthData.SteamLogin;
|
||||
session.SteamLoginSecure = session.SteamID + "%7C%7C" + oAuthData.SteamLoginSecure;
|
||||
session.WebCookie = oAuthData.Webcookie;
|
||||
session.SessionID = readableCookies["sessionid"].Value;
|
||||
this.Session = session;
|
||||
this.LoggedIn = true;
|
||||
return LoginResult.LoginOkay;
|
||||
}
|
||||
|
||||
return LoginResult.GeneralFailure;
|
||||
}
|
||||
|
||||
private class LoginResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("login_complete")]
|
||||
public bool LoginComplete { get; set; }
|
||||
|
||||
[JsonProperty("oauth")]
|
||||
public string OAuthDataString { get; set; }
|
||||
|
||||
public OAuth OAuthData
|
||||
{
|
||||
get
|
||||
{
|
||||
return OAuthDataString != null ? JsonConvert.DeserializeObject<OAuth>(OAuthDataString) : null;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("captcha_needed")]
|
||||
public bool CaptchaNeeded { get; set; }
|
||||
|
||||
[JsonProperty("captcha_gid")]
|
||||
public string CaptchaGID { get; set; }
|
||||
|
||||
[JsonProperty("emailsteamid")]
|
||||
public ulong EmailSteamID { get; set; }
|
||||
|
||||
[JsonProperty("emailauth_needed")]
|
||||
public bool EmailAuthNeeded { get; set; }
|
||||
|
||||
[JsonProperty("requires_twofactor")]
|
||||
public bool TwoFactorNeeded { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
internal class OAuth
|
||||
{
|
||||
[JsonProperty("steamid")]
|
||||
public ulong SteamID { get; set; }
|
||||
|
||||
[JsonProperty("oauth_token")]
|
||||
public string OAuthToken { get; set; }
|
||||
|
||||
[JsonProperty("wgtoken")]
|
||||
public string SteamLogin { get; set; }
|
||||
|
||||
[JsonProperty("wgtoken_secure")]
|
||||
public string SteamLoginSecure { get; set; }
|
||||
|
||||
[JsonProperty("webcookie")]
|
||||
public string Webcookie { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class RSAResponse
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("publickey_exp")]
|
||||
public string Exponent { get; set; }
|
||||
|
||||
[JsonProperty("publickey_mod")]
|
||||
public string Modulus { get; set; }
|
||||
|
||||
[JsonProperty("timestamp")]
|
||||
public string Timestamp { get; set; }
|
||||
|
||||
[JsonProperty("steamid")]
|
||||
public ulong SteamID { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public enum LoginResult
|
||||
{
|
||||
LoginOkay,
|
||||
GeneralFailure,
|
||||
BadRSA,
|
||||
BadCredentials,
|
||||
NeedCaptcha,
|
||||
Need2FA,
|
||||
NeedEmail,
|
||||
TooManyFailedLogins,
|
||||
}
|
||||
}
|
||||
24
SteamAuth/Util.cs
Normal file
24
SteamAuth/Util.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace SteamAuth
|
||||
{
|
||||
public class Util
|
||||
{
|
||||
public static long GetSystemUnixTime()
|
||||
{
|
||||
return (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
|
||||
}
|
||||
|
||||
public static byte[] HexStringToByteArray(string hex)
|
||||
{
|
||||
int hexLen = hex.Length;
|
||||
byte[] ret = new byte[hexLen / 2];
|
||||
for (int i = 0; i < hexLen; i += 2)
|
||||
{
|
||||
ret[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
SteamAuth/packages.config
Normal file
4
SteamAuth/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="8.0.1-beta3" targetFramework="net452" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user