Compare commits

...

21 Commits

Author SHA1 Message Date
JustArchi
9f4ed24704 And copy it... 2015-12-18 07:01:03 +01:00
JustArchi
015c6b7bdf Add minimal.xml to VS project 2015-12-18 06:59:47 +01:00
JustArchi
a17c1fc35a VS please 2015-12-18 06:56:33 +01:00
JustArchi
19e46ce78d Misc 2015-12-18 06:54:53 +01:00
JustArchi
cf6ee3b60d Switch to new async methods 2015-12-17 22:04:13 +01:00
JustArchi
488003993f Do not wait longer than 5 sec 2015-12-17 22:00:24 +01:00
JustArchi
c64a6fabbc SteamAuth sync 2015-12-17 21:59:32 +01:00
JustArchi
eb2751861d Don't forget to reset status back to false after the test, closes #34 2015-12-17 20:57:03 +01:00
JustArchi
48a1cf1189 Handle expired StartFarming too 2015-12-16 22:14:53 +01:00
JustArchi
98e1d51a48 Handle expired sessionIDs for main accounts 2015-12-16 22:05:42 +01:00
JustArchi
5e22c832a2 Do not reject friend invites, group invites, and trades, by default, closes #33 2015-12-16 20:09:31 +01:00
JustArchi
6523b30f10 Always use LoginID, closes #32 2015-12-16 20:01:40 +01:00
JustArchi
8fefa7c5af Use random UniqueID for LoginID 2015-12-16 10:21:13 +01:00
JustArchi
b6a5b8942c Actually do not wait 25 minutes after expired login keys 2015-12-16 09:39:03 +01:00
JustArchi
cd622989d6 SteamAuth sync 2015-12-16 01:30:02 +01:00
JustArchi
8251274bf8 Version bump 2015-12-16 01:23:56 +01:00
JustArchi
dd63c97ced Clean it up a little 2015-12-16 01:20:54 +01:00
JustArchi
b17960096e Internal improvements 2015-12-16 01:18:58 +01:00
JustArchi
83934af041 Bump to 0.9.1 2015-12-13 21:06:08 +01:00
JustArchi
0421da30cd Fix derp, closes #30 2015-12-13 21:05:01 +01:00
Łukasz Domeradzki
bf084a1ffb Update README.md 2015-12-13 15:56:29 +01:00
18 changed files with 348 additions and 62 deletions

3
.gitignore vendored
View File

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

View File

@@ -123,6 +123,9 @@
<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">
@@ -139,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>

View File

@@ -150,6 +150,31 @@ namespace ArchiSteamFarm {
}
}
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() {
if (ApiKey == null) {
return null;

View File

@@ -69,7 +69,7 @@ namespace ArchiSteamFarm {
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) {
@@ -160,15 +160,15 @@ namespace ArchiSteamFarm {
var fireAndForget = Task.Run(async () => await Start().ConfigureAwait(false));
}
internal void AcceptAllConfirmations() {
internal async Task AcceptAllConfirmations() {
if (SteamGuardAccount == null) {
return;
}
SteamGuardAccount.RefreshSession();
await SteamGuardAccount.RefreshSessionAsync().ConfigureAwait(false);
try {
foreach (Confirmation confirmation in SteamGuardAccount.FetchConfirmations()) {
foreach (Confirmation confirmation in await SteamGuardAccount.FetchConfirmationsAsync().ConfigureAwait(false)) {
if (SteamGuardAccount.AcceptConfirmation(confirmation)) {
Logging.LogGenericInfo(BotName, "Accepting confirmation: Success!");
} else {
@@ -369,7 +369,7 @@ namespace ArchiSteamFarm {
}
await bot.Stop().ConfigureAwait(false);
Bots.TryRemove(botName, out bot);
Bots.TryRemove(bot.BotName, out bot);
Program.OnBotShutdown();
return true;
@@ -542,7 +542,7 @@ namespace ArchiSteamFarm {
}
// TODO: We should use SteamUser.LogOn with proper LoginID once https://github.com/SteamRE/SteamKit/pull/217 gets merged
ArchiHandler.HackedLogOn(0xBAADF00D, new SteamUser.LogOnDetails {
ArchiHandler.HackedLogOn(Program.UniqueID, new SteamUser.LogOnDetails {
Username = SteamLogin,
Password = SteamPassword,
AuthCode = AuthCode,
@@ -597,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;
}
@@ -731,15 +729,18 @@ namespace ArchiSteamFarm {
}
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);
// InvalidPassword means that we must assume login key has expired
LoginKey = null;
File.Delete(LoginKeyFile);
// InvalidPassword also means that we might get captcha or other network-based throttling
await Utilities.SleepAsync(25 * 60 * 1000).ConfigureAwait(false); // Captcha disappears after around 20 minutes, so we make it 25
// 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);

View File

@@ -143,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
@@ -289,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);
}
@@ -299,7 +321,7 @@ namespace ArchiSteamFarm {
}
private async Task CheckGamesForFarming() {
if (NowFarming || CurrentGamesFarming.Count > 0 || GamesToFarm.Count > 0) {
if (NowFarming || GamesToFarm.Count > 0) {
return;
}
@@ -307,15 +329,22 @@ namespace ArchiSteamFarm {
}
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");
}
if (appID == 0) {
return false;
}
return result;
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) {

View File

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

View File

@@ -52,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);
@@ -104,6 +107,7 @@ namespace ArchiSteamFarm {
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: ");
@@ -134,7 +138,9 @@ namespace ArchiSteamFarm {
}
result = Console.ReadLine();
Console.Clear(); // For security purposes
ConsoleIsBusy = false;
}
return result.Trim(); // Get rid of all whitespace characters
}

View File

@@ -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.9.0.0")]
[assembly: AssemblyFileVersion("0.9.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -62,7 +62,7 @@ namespace ArchiSteamFarm {
}
await Task.WhenAll(tasks).ConfigureAwait(false);
Bot.AcceptAllConfirmations();
await Bot.AcceptAllConfirmations().ConfigureAwait(false);
}
private async Task ParseTrade(SteamTradeOffer tradeOffer) {
@@ -75,20 +75,11 @@ namespace ArchiSteamFarm {
return;
}
bool success, tradeAccepted;
if (tradeOffer.items_to_give.Count == 0 || tradeOffer.OtherSteamID64 == Bot.SteamMasterID) {
tradeAccepted = true;
Logging.LogGenericInfo(Bot.BotName, "Accepting trade: " + tradeID);
success = await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
await Bot.ArchiWebHandler.AcceptTradeOffer(tradeID).ConfigureAwait(false);
} else {
tradeAccepted = false;
Logging.LogGenericInfo(Bot.BotName, "Rejecting trade: " + tradeID);
success = Bot.ArchiWebHandler.DeclineTradeOffer(tradeID);
}
if (tradeAccepted && success) {
// Do whatever we want with success
Logging.LogGenericInfo(Bot.BotName, "Ignoring trade: " + tradeID);
}
}
}

View File

@@ -22,11 +22,14 @@
*/
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace ArchiSteamFarm {
internal static class Utilities {
internal static readonly Random Random = new Random();
internal static async Task SleepAsync(int miliseconds) {
await Task.Delay(miliseconds).ConfigureAwait(false);
}

View File

@@ -1,6 +1,7 @@
<?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: [& - &amp;] | [" - &quot;] | [' - &apos;] | [< - &lt;] | [> - &gt;] -->
@@ -71,7 +72,7 @@
<!-- 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 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 -->

View 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>

View File

@@ -3,6 +3,8 @@ 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
@@ -11,18 +13,17 @@ ASF is a C# application that allows you to farm steam cards using multiple steam
- 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
- 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. 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.
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.
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:**

View File

@@ -43,7 +43,7 @@ namespace SteamAuth
public AuthenticatorLinker(SessionData session)
{
this._session = session;
this.DeviceID = _generateDeviceID();
this.DeviceID = GenerateDeviceID();
this._cookies = new CookieContainer();
session.AddCookies(_cookies);
@@ -76,7 +76,17 @@ namespace SteamAuth
if (response == null) return LinkResult.GeneralFailure;
var addAuthenticatorResponse = JsonConvert.DeserializeObject<AddAuthenticatorResponse>(response);
if (addAuthenticatorResponse == null || addAuthenticatorResponse.Response == null || addAuthenticatorResponse.Response.Status != 1)
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;
}
@@ -181,7 +191,8 @@ namespace SteamAuth
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!)
GeneralFailure, //General failure (really now!)
AuthenticatorPresent
}
public enum FinalizeResult
@@ -231,7 +242,7 @@ namespace SteamAuth
public bool Success { get; set; }
}
private string _generateDeviceID()
public static string GenerateDeviceID()
{
using (var sha1 = new SHA1Managed())
{
@@ -240,8 +251,27 @@ namespace SteamAuth
secureRandom.GetBytes(randomBytes);
byte[] hashedBytes = sha1.ComputeHash(randomBytes);
return "android:" + BitConverter.ToString(hashedBytes).Replace("-", "");
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;
}
}
}

View File

@@ -6,6 +6,7 @@ using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SteamAuth
{
@@ -54,11 +55,11 @@ namespace SteamAuth
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()
public bool DeactivateAuthenticator(int scheme = 2)
{
var postData = new NameValueCollection();
postData.Add("steamid", this.Session.SteamID.ToString());
postData.Add("steamguard_scheme", "2");
postData.Add("steamguard_scheme", scheme.ToString());
postData.Add("revocation_code", this.RevocationCode);
postData.Add("access_token", this.Session.OAuthToken);
@@ -171,6 +172,56 @@ namespace SteamAuth
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");
@@ -213,6 +264,38 @@ namespace SteamAuth
}
}
/// <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";
@@ -241,6 +324,9 @@ namespace SteamAuth
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;
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace SteamAuth
{
@@ -67,12 +68,67 @@ namespace SteamAuth
using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
{
string responseData = responseStream.ReadToEnd();
return responseData;
}
}
}
catch (WebException ex)
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;
}

View File

@@ -1,4 +1,6 @@
using System.Net;
using System;
using System.Threading.Tasks;
using System.Net;
using Newtonsoft.Json;
namespace SteamAuth
@@ -21,6 +23,15 @@ namespace SteamAuth
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();
@@ -33,13 +44,30 @@ namespace SteamAuth
TimeAligner._timeDifference = (int)(query.Response.ServerTime - currentTime);
TimeAligner._aligned = true;
}
catch (WebException e)
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")]

View File

@@ -59,7 +59,7 @@ namespace SteamAuth
postData.Add("username", this.Username);
response = SteamWeb.MobileLoginRequest(APIEndpoints.COMMUNITY_BASE + "/login/getrsakey", "POST", postData, cookies);
if (response == null) return LoginResult.GeneralFailure;
if (response == null || response.Contains("<BODY>\nAn error occurred while processing your request.")) return LoginResult.GeneralFailure;
var rsaResponse = JsonConvert.DeserializeObject<RSAResponse>(response);
@@ -106,6 +106,11 @@ namespace SteamAuth
var loginResponse = JsonConvert.DeserializeObject<LoginResponse>(response);
if (loginResponse.Message != null && loginResponse.Message.Contains("Incorrect login"))
{
return LoginResult.BadCredentials;
}
if (loginResponse.CaptchaNeeded)
{
this.RequiresCaptcha = true;
@@ -126,6 +131,11 @@ namespace SteamAuth
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;
@@ -189,6 +199,9 @@ namespace SteamAuth
[JsonProperty("requires_twofactor")]
public bool TwoFactorNeeded { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
internal class OAuth
{
[JsonProperty("steamid")]
@@ -236,5 +249,6 @@ namespace SteamAuth
NeedCaptcha,
Need2FA,
NeedEmail,
TooManyFailedLogins,
}
}