From 1702018257b6b1bc6ff503a62210e1835e45c14d Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 1 Jun 2016 12:43:11 +0200 Subject: [PATCH 01/16] Create mobileauth.py --- steam/mobileauth.py | 135 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 steam/mobileauth.py diff --git a/steam/mobileauth.py b/steam/mobileauth.py new file mode 100644 index 0000000..6ef7582 --- /dev/null +++ b/steam/mobileauth.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +from time import time +import sys +from base64 import b64encode +import requests + +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers +from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 +from steam.core.crypto import backend + +from steam.util.web import make_requests_session +from steam import SteamID + +if sys.version_info < (3,): + intBase = long +else: + intBase = int + + +class MobileAuth(object): + key = None + complete = False #: whether authentication has been completed successfully + session = None #: :class:`requests.Session` (with auth cookies after auth is complete) + captcha_gid = -1 + steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) + + def __init__(self, username, password): + self.__dict__.update(locals()) + self.session = make_requests_session() + + @property + def captcha_url(self): + if self.captcha_gid == -1: + return None + else: + return "https://store.steampowered.com/login/rendercaptcha/?gid=%s" % self.captcha_gid + + def get_rsa_key(self, username): + try: + resp = self.session.post('https://steamcommunity.com/mobilelogin/getrsakey/', + timeout=15, + data={ + 'username': username, + 'donotchache': int(time() * 1000), + }, + ).json() + except requests.exceptions.RequestException as e: + raise HTTPError(str(e)) + + return resp + + def _load_key(self): + if not self.key: + resp = self.get_rsa_key(self.username) + + nums = RSAPublicNumbers(intBase(resp['publickey_exp'], 16), + intBase(resp['publickey_mod'], 16), + ) + + self.key = backend.load_rsa_public_numbers(nums) + self.timestamp = resp['timestamp'] + + def login(self, captcha='', email_code='', twofactor_code='', language='english'): + if self.complete: + return self.session + + for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: + self.session.cookies.set('forceMobile', '1', domain=domain, secure=False) + self.session.cookies.set('mobileClientVersion', '0 (2.1.3)', domain=domain, secure=False) + self.session.cookies.set('mobileClient', 'android', domain=domain, secure=False) + self.session.cookies.set('Steam_Language', 'english', domain=domain, secure=False) + self.session.cookies.set('dob', '', domain=domain, secure=False) + + self._load_key() + + data = { + 'username' : self.username, + "password": b64encode(self.key.encrypt(self.password.encode('ascii'), PKCS1v15())), + "emailauth": email_code, + "emailsteamid": str(self.steamid) if email_code else '', + "twofactorcode": twofactor_code, + "captchagid": self.captcha_gid, + "captcha_text": captcha, + "loginfriendlyname": "python-steam webauth", + "rsatimestamp": self.timestamp, + "remember_login": 'true', + "donotcache": int(time() * 100000), + } + data['oauth_client_id'] = 'DE45CD61' + data['oauth_scope'] = 'read_profile write_profile read_client write_client' + data['loginfriendlyname'] = '#login_emailauth_friendlyname_mobile' + + try: + resp = self.session.post('https://steamcommunity.com/mobilelogin/dologin/', data=data, timeout=15).json() + except requests.exceptions.RequestException as e: + raise HTTPError(str(e)) + + self.captcha_gid = -1 + + if resp['success'] and resp['login_complete']: + self.complete = True + self.password = None + return resp + else: + if resp.get('captcha_needed', False): + self.captcha_gid = resp['captcha_gid'] + + raise CaptchaRequired(resp['message']) + elif resp.get('emailauth_needed', False): + self.steamid = SteamID(resp['emailsteamid']) + raise EmailCodeRequired(resp['message']) + elif resp.get('requires_twofactor', False): + raise TwoFactorCodeRequired(resp['message']) + else: + raise LoginIncorrect(resp['message']) + + return None + +class MobileWebAuthException(Exception): + pass + +class HTTPError(MobileWebAuthException): + pass + +class LoginIncorrect(MobileWebAuthException): + pass + +class CaptchaRequired(MobileWebAuthException): + pass + +class EmailCodeRequired(MobileWebAuthException): + pass + +class TwoFactorCodeRequired(MobileWebAuthException): + pass From 8db821491411e3415be0539d5810d901ce24afdf Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 1 Jun 2016 12:45:04 +0200 Subject: [PATCH 02/16] Create twofactorservice.py --- steam/twofactorservice.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 steam/twofactorservice.py diff --git a/steam/twofactorservice.py b/steam/twofactorservice.py new file mode 100644 index 0000000..4d42a89 --- /dev/null +++ b/steam/twofactorservice.py @@ -0,0 +1 @@ +from guard import * From b2f619f824888b90fae97531ec00f8f8d1e613e0 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 1 Jun 2016 13:03:29 +0200 Subject: [PATCH 03/16] Update mobileauth.py --- steam/mobileauth.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/steam/mobileauth.py b/steam/mobileauth.py index 6ef7582..e86693b 100644 --- a/steam/mobileauth.py +++ b/steam/mobileauth.py @@ -59,6 +59,22 @@ class MobileAuth(object): self.key = backend.load_rsa_public_numbers(nums) self.timestamp = resp['timestamp'] + + def request(self, uri, data): + if not self.complete: + return None + + headers = { + 'X-Requested-With': 'com.valvesoftware.android.steam.community', + 'User-agent': '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' + } + + try: + response = self.session.post(uri, data=data, headers=headers) + except requests.exceptions.RequestException as e: + raise HTTPError(str(e)) + else: + return response def login(self, captcha='', email_code='', twofactor_code='', language='english'): if self.complete: From bc4532fb3db39ab75a0f0266e7e0911bdc2c337e Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 1 Jun 2016 13:30:23 +0200 Subject: [PATCH 04/16] Update mobileauth.py --- steam/mobileauth.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/steam/mobileauth.py b/steam/mobileauth.py index e86693b..8ac2b0d 100644 --- a/steam/mobileauth.py +++ b/steam/mobileauth.py @@ -23,7 +23,7 @@ class MobileAuth(object): session = None #: :class:`requests.Session` (with auth cookies after auth is complete) captcha_gid = -1 steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) - + oauth = {} def __init__(self, username, password): self.__dict__.update(locals()) self.session = make_requests_session() @@ -76,6 +76,19 @@ class MobileAuth(object): else: return response + def refreshSession(self, oauth_token=None): + oauth_token = oauth_token or self.oauth['oauth_token'] + response = self.request('https://api.steampowered.com/IMobileAuthService/GetWGToken/v0001', {'access_token': oauth_token}) + try: + data = json.loads(response) + except Exception, e: + raise RefreshSessionFailed(str(e)) + else: + self.oauth['wgtoken'] = data['response']['token'] + self.oauth['wgtoken_secure'] = data['response']['token_secure'] + self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, sself.oauth['wgtoken']), domain=domain, secure=False) + self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, self.oauth['wgtoken_secure']), domain=domain, secure=True) + def login(self, captcha='', email_code='', twofactor_code='', language='english'): if self.complete: return self.session @@ -116,6 +129,12 @@ class MobileAuth(object): if resp['success'] and resp['login_complete']: self.complete = True self.password = None + self.steamid = SteamID(resp['oauth']['steamid']) + self.oauth = resp['oauth'] + for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: + self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, resp['oauth']['wgtoken']), domain=domain, secure=False) + self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, data['oauth']['wgtoken_secure']), domain=domain, secure=True) + return resp else: if resp.get('captcha_needed', False): @@ -149,3 +168,6 @@ class EmailCodeRequired(MobileWebAuthException): class TwoFactorCodeRequired(MobileWebAuthException): pass + +class RefreshSessionFailed(MobileWebAuthException): + pass From 3396d86aedf366c6472131beceb5363b06c055eb Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 1 Jun 2016 13:55:51 +0200 Subject: [PATCH 05/16] Update and rename twofactorservice.py to authenticator.py --- steam/authenticator.py | 86 +++++++++++++++++++++++++++++++++++++++ steam/twofactorservice.py | 1 - 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 steam/authenticator.py delete mode 100644 steam/twofactorservice.py diff --git a/steam/authenticator.py b/steam/authenticator.py new file mode 100644 index 0000000..13f2e86 --- /dev/null +++ b/steam/authenticator.py @@ -0,0 +1,86 @@ +import json + +from guard import * +from mobileauth import MobileAuth + +class MobileAuthenticator: + def __init__(self, username, password, authenticatorCredentials=False): + self.username = username + self.password = password + self.ready = None + self.mobile = MobileAuth(username, password) + self.credentials = authenticatorCredentials or { } + + def login(self): + if self.ready != None: + return False + + if 'secret' in self.credentials.keys(): + code = generate_twofactor_code(self.credentials.get('secret')) + self.mobile.login(twofactor_code=code) + return True + else: + try: + self.mobile.login() + except EmailCodeRequired: + raise AuthenticatorAlreadyActive('Two factor authentication already active') + except TwoFactorCodeRequired + raise AuthenticatorAlreadyActive('Two factor authentication already active') + else: + self.ready = False + return True + + def addAuthenticator(self): + if self.ready != False: + return None + + + data = { + 'steamid': self.mobile.steamid, + 'sms_phone_id': 1, + 'access_token': self.mobile.oauth['oauth_token'], + 'authenticator_time': get_time_offset(), + 'authenticator_type': 1, + 'device_identifier': generate_device_id(self.mobile.steamid) + } + + response = self.mobile.request('https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/', data) + if response.status_code == 200: + responseData = json.loads(response.text) + self.credentials = responseData['response'] + self.credentials['secret'] = self.credentials['uri'].split('?secret=')[1].split('&issuer')[0] + return True + else: + return [False, responseData] + + def finalizeAuthenticator(self, smsCode=None, tries=1): + if not smsCode or self.ready != False: + return None + + timestamp = get_time_offset() + + data = { + 'steamid': self.mobile.steamid, + 'access_token': self.mobile.oauth['oauth_token'], + 'authenticator_time': timestamp, + 'authenticator_code': generate_twofactor_code_for_time(self.credentials['secret'], timestamp), + 'activation_code': smsCode + } + response = self.mobile.request('https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/', data) + if response.status_code == 200: + responseData = json.loads(response.text) + if responseData['success']: + return True + else: + if responseData['want_more'] and tries < 30: + return self.finalizeAuthenticator(smsCode, tries) + else: + return False + else: + return False + +class MobileAuthenticatorException(Exception): + pass + +class AuthenticatorAlreadyActive(MobileAuthenticatorException) + pass diff --git a/steam/twofactorservice.py b/steam/twofactorservice.py deleted file mode 100644 index 4d42a89..0000000 --- a/steam/twofactorservice.py +++ /dev/null @@ -1 +0,0 @@ -from guard import * From 253dcdd4cbc11f0714b9c038a0db47debae62f34 Mon Sep 17 00:00:00 2001 From: Philipp Joos Date: Fri, 3 Jun 2016 14:01:09 +0200 Subject: [PATCH 06/16] Cleanup --- requirements.txt | 1 + steam/authenticator.py | 174 +++++++++++++++++--------------- steam/guard.py | 4 +- steam/mobile.py | 220 +++++++++++++++++++++++++++++++++++++++++ steam/mobileauth.py | 116 +++++++++++++--------- steam/webauth.py | 23 +++-- 6 files changed, 403 insertions(+), 135 deletions(-) create mode 100644 steam/mobile.py diff --git a/requirements.txt b/requirements.txt index 638817c..9c3bc1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ coverage==4.0.3 +setuptools>=11.3 gevent-eventemitter==1.4 enum34==1.1.2; python_version < '3.4' gevent==1.1.0 diff --git a/steam/authenticator.py b/steam/authenticator.py index 13f2e86..ee92b1e 100644 --- a/steam/authenticator.py +++ b/steam/authenticator.py @@ -1,86 +1,98 @@ +""" +This module provides methods for managing the mobile authenticator + +.. warning:: + Save your credentials! + +Example usage: + +.. code:: python + + import steam.authenticator + + ma = steam.authenticator.MobileAuthenticator('username', 'password') + credentials = ma.add_authenticator() + + sms_code = raw_input('SMS Code: ') + + ma.finalize_authenticator(sms_code) + + print credentials + +""" import json from guard import * -from mobileauth import MobileAuth - -class MobileAuthenticator: - def __init__(self, username, password, authenticatorCredentials=False): - self.username = username - self.password = password - self.ready = None - self.mobile = MobileAuth(username, password) - self.credentials = authenticatorCredentials or { } - - def login(self): - if self.ready != None: - return False - - if 'secret' in self.credentials.keys(): - code = generate_twofactor_code(self.credentials.get('secret')) - self.mobile.login(twofactor_code=code) - return True - else: - try: - self.mobile.login() - except EmailCodeRequired: - raise AuthenticatorAlreadyActive('Two factor authentication already active') - except TwoFactorCodeRequired - raise AuthenticatorAlreadyActive('Two factor authentication already active') - else: - self.ready = False - return True - - def addAuthenticator(self): - if self.ready != False: - return None - - - data = { - 'steamid': self.mobile.steamid, - 'sms_phone_id': 1, - 'access_token': self.mobile.oauth['oauth_token'], - 'authenticator_time': get_time_offset(), - 'authenticator_type': 1, - 'device_identifier': generate_device_id(self.mobile.steamid) - } - - response = self.mobile.request('https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/', data) - if response.status_code == 200: - responseData = json.loads(response.text) - self.credentials = responseData['response'] - self.credentials['secret'] = self.credentials['uri'].split('?secret=')[1].split('&issuer')[0] - return True - else: - return [False, responseData] - - def finalizeAuthenticator(self, smsCode=None, tries=1): - if not smsCode or self.ready != False: - return None - - timestamp = get_time_offset() - - data = { - 'steamid': self.mobile.steamid, - 'access_token': self.mobile.oauth['oauth_token'], - 'authenticator_time': timestamp, - 'authenticator_code': generate_twofactor_code_for_time(self.credentials['secret'], timestamp), - 'activation_code': smsCode - } - response = self.mobile.request('https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/', data) - if response.status_code == 200: - responseData = json.loads(response.text) - if responseData['success']: - return True - else: - if responseData['want_more'] and tries < 30: - return self.finalizeAuthenticator(smsCode, tries) +from mobile import SteamMobile + + +class MobileAuthenticator(object): + mobile = None + + def __init__(self, username, password, login_mode='normal', email_code='', twofactor_code=''): + self.mobile = SteamMobile(username, password, login_mode, email_code, twofactor_code) + + def add_authenticator(self): + """Start process of linking a mobile authenticator to the logged in steam account + + :return: account credentials or False + :rtype: :class:`tuple`, :class:`bool` + """ + + data = { + 'steamid': self.mobile.steamid, + 'sms_phone_id': 1, + 'access_token': self.mobile.oauth['oauth_token'], + 'authenticator_time': get_time_offset(), + 'authenticator_type': 1, + 'device_identifier': generate_device_id(self.mobile.steamid) + } + + [status, body] = self.mobile.api_request('ITwoFactorService', 'AddAuthenticator', data, + return_including_status_code=True) + if status == 200: + responseData = json.loads(body) + self.credentials = responseData['response'] + self.credentials['secret'] = self.credentials['uri'].split('?secret=')[1].split('&issuer')[0] + return responseData else: - return False - else: - return False - -class MobileAuthenticatorException(Exception): - pass + return False + + def finalize_authenticator(self, sms_code=None, tries=1): + """Start process of linking a mobile authenticator to the logged in steam account + + :param sms_code: text reponse recieved by sms + :type sms_code: :class:`str` + :return: :class:`None` it no sms code is supplied, `True` or `False` + :rtype: :class:`None`, :class:`bool` + """ -class AuthenticatorAlreadyActive(MobileAuthenticatorException) - pass + if not sms_code: + return None + + timestamp = get_time_offset() + + data = { + 'steamid': self.mobile.steamid, + 'access_token': self.mobile.oauth['oauth_token'], + 'authenticator_time': timestamp, + 'authenticator_code': generate_twofactor_code_for_time(self.credentials['shared_secret'], timestamp), + 'activation_code': sms_code + } + [status, body] = self.mobile.api_request('ITwoFactorService', 'FinalizeAddAuthenticator', data, + return_including_status_code=True) + if status == 200: + responseData = json.loads(body)['response'] + if responseData['success']: + return True + else: + if responseData['want_more'] and tries < 30: + return self.finalizeAuthenticator(sms_code, tries) + else: + return False + else: + return False + + +class MobileAuthenticatorException(Exception): + pass \ No newline at end of file diff --git a/steam/guard.py b/steam/guard.py index 7d184f2..df3c185 100644 --- a/steam/guard.py +++ b/steam/guard.py @@ -1,9 +1,11 @@ import struct +import hashlib from binascii import hexlify from time import time from steam import webapi from steam.core.crypto import hmac_sha1, sha1_hash + def generate_twofactor_code(shared_secret): """Generate Steam 2FA code for login with current time @@ -84,5 +86,5 @@ def generate_device_id(steamid): :return: android device id :rtype: str """ - h = hexlify(sha1(str(steamid).encode('ascii'))).decode('ascii') + h = hexlify(str(hashlib.sha1(str(steamid).encode('ascii')))).decode('ascii') return "android:%s-%s-%s-%s-%s" % (h[:8], h[8:12], h[12:16], h[16:20], h[20:32]) diff --git a/steam/mobile.py b/steam/mobile.py new file mode 100644 index 0000000..8a1b7af --- /dev/null +++ b/steam/mobile.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +""" +This module provides utility functions to access steam mobile pages +""" + +import json +import requests + +import mobileauth + +API_ENDPOINTS = { + "IMobileAuthService": { + "methods": { + "GetWGToken": { + "version": 1 + } + } + }, + "ISteamWebUserPresenceOAuth": { + "methods": { + "Logon": { + "version": 1 + }, + "Logoff": { + "version": 1 + }, + "Message": { + "version": 1 + }, + "DeviceInfo": { + "version": 1 + }, + "Poll": { + "version": 1 + } + } + }, + "ISteamUserOAuth": { + "methods": { + "GetUserSummaries": { + "version": 1 + }, + "GetGroupSummaries": { + "version": 1 + }, + "GetGroupList": { + "version": 1 + }, + "GetFriendList": { + "version": 1 + }, + "Search": { + "version": 1 + } + } + }, + + "ISteamGameOAuth": { + "methods": { + "GetAppInfo": { + "version": 1 + } + } + }, + "IMobileNotificationService": { + "methods": { + "SwitchSessionToPush": { + "version": 1 + } + } + }, + "IFriendMessagesService": { + "methods": { + "GetActiveMessageSessions": { + "version": 1 + }, + "GetRecentMessages": { + "version": 1 + }, + "MarkOfflineMessagesRead": { + "version": 1 + } + } + }, + "ITwoFactorService": { + "methods": { + "AddAuthenticator": { + "version": 1 + }, + "RecoverAuthenticatorCommit": { + "version": 1 + }, + "RecoverAuthenticatorContinue": { + "version": 1 + }, + "RemoveAuthenticator": { + "version": 1 + }, + "RemoveAuthenticatorViaChallengeStart": { + "version": 1 + }, + "RemoveAuthenticatorViaChallengeContinue": { + "version": 1 + }, + "FinalizeAddAuthenticator": { + "version": 1 + }, + "QueryStatus": { + "version": 1 + }, + "QueryTime": { + "version": 1 + }, + "QuerySecrets": { + "version": 1 + }, + "SendEmail": { + "version": 1 + }, + "ValidateToken": { + "version": 1 + }, + "CreateEmergencyCodes": { + "version": 1 + }, + "DestroyEmergencyCodes": { + "version": 1 + } + } + } +} + +API_BASE_URL = 'https://api.steampowered.com' + + +class SteamMobile(object): + session = None + oauth = None + steamid = None + + def __init__(self, username, password, login_mode='normal', email_code='', twofactor_code=''): + mobile_auth = mobileauth.MobileAuth(username, password) + + try: + if login_mode == 'normal': + mobile_auth.login() + elif login_mode == 'email': + mobile_auth.login(email_code=email_code) + elif login_mode == 'twofa': + mobile_auth.login(twofactor_code=twofactor_code) + + except mobileauth.CaptchaRequired: + raise CaptchaNotSupported("Captcha's are currently not supported. Please wait a few minutes before you try to login again.") + except mobileauth.EmailCodeRequired: + mobile_auth.login(email_code=email_code) + except mobileauth.TwoFactorCodeRequired: + mobile_auth.login(twofactor_code=twofactor_code) + + + self.session = mobile_auth.session + self.steamid = mobile_auth.steamid + self.oauth = mobile_auth.oauth + + def refresh_session(self, oauth_token=None): + oauth_token = oauth_token or self.oauth['oauth_token'] + response = self.api_request('IMobileAuthService', 'GetWGToken', {'access_token': oauth_token}) + try: + data = json.loads(response) + except Exception, e: + raise SessionRefreshFailed(str(e)) + else: + self.oauth['wgtoken'] = data['response']['token'] + self.oauth['wgtoken_secure'] = data['response']['token_secure'] + for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: + self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, self.oauth['wgtoken']), domain=domain, + secure=False) + self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, self.oauth['wgtoken_secure']), + domain=domain, secure=True) + + def _request(self, uri, data={}, return_including_status_code=False): + headers = { + 'X-Requested-With': 'com.valvesoftware.android.steam.community', + 'User-agent': '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' + } + + try: + response = self.session.post(uri, data=data, headers=headers) + except requests.exceptions.RequestException as e: + raise mobileauth.HTTPError(str(e)) + else: + if return_including_status_code: + return [response.status_code, response.text] + else: + return response.text + + def api_request(self, interface, method, data={}, return_including_status_code=False): + if interface in API_ENDPOINTS.keys() and method in API_ENDPOINTS.get(interface).get('methods').keys(): + uri = '%s/%s/%s/v%s/' % ( + API_BASE_URL, interface, method, API_ENDPOINTS.get(interface).get('methods').get(method).get('version')) + response = self._request(uri, data, return_including_status_code) + return response + else: + raise APIEndpointNotFound('Endpoint %s.%s not found' % (interface, method)) + + +class SteamMobileException(Exception): + pass + + +class CaptchaNotSupported(SteamMobileException): + pass + + +class SessionRefreshFailed(SteamMobileException): + pass + + +class APIEndpointNotFound(SteamMobileException): + pass \ No newline at end of file diff --git a/steam/mobileauth.py b/steam/mobileauth.py index 8ac2b0d..3004496 100644 --- a/steam/mobileauth.py +++ b/steam/mobileauth.py @@ -1,4 +1,54 @@ # -*- coding: utf-8 -*- +""" +This module simplifies the process of obtaining an authenticated session for mobile steam websites. +After authentication is complete, a :class:`requests.Session` is created containing the auth and mobile specific cookies. +The session can be used to access ``steamcommunity.com``, ``store.steampowered.com``, and ``help.steampowered.com``. +The main purpose of a mobile session, is to access the mobile trade confirmations page and to easily access the +ITwoFactorService api, but can also used to access all 'normal' steam pages. + + +.. warning:: + A web session may expire randomly, or when you login from different IP address. + Some pages will return status code `401` when that happens. + Keep in mind if you are trying to write robust code. + +Example usage: + +.. code:: python + + import steam.mobileauth as ma + + user = ma.WebAuth('username', 'password') + + try: + user.login() + except wa.CaptchaRequired: + print user.captcha_url + # ask a human to solve captcha + user.login(captcha='ABC123') + except wa.EmailCodeRequired: + user.login(email_code='ZXC123') + except wa.TwoFactorCodeRequired: + user.login(twofactor_code='ZXC123') + + user.session.get('https://store.steampowered.com/account/history/') + # OR + session = user.login() + session.get('https://store.steampowered.com/account/history') + +Alternatively, if Steam Guard is not enabled on the account: + +.. code:: python + + try: + session = wa.WebAuth('username', 'password').login() + except wa.HTTPError: + pass + +The :class:`MobileAuth` instance should be discarded once a session is obtained +as it is not reusable. +""" +import json from time import time import sys from base64 import b64encode @@ -20,10 +70,11 @@ else: class MobileAuth(object): key = None complete = False #: whether authentication has been completed successfully - session = None #: :class:`requests.Session` (with auth cookies after auth is complete) + session = None #: :class:`requests.Session` (with auth cookies after auth is complete) captcha_gid = -1 - steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) + steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) oauth = {} + def __init__(self, username, password): self.__dict__.update(locals()) self.session = make_requests_session() @@ -38,12 +89,12 @@ class MobileAuth(object): def get_rsa_key(self, username): try: resp = self.session.post('https://steamcommunity.com/mobilelogin/getrsakey/', - timeout=15, - data={ - 'username': username, - 'donotchache': int(time() * 1000), - }, - ).json() + timeout=15, + data={ + 'username': username, + 'donotchache': int(time() * 1000), + }, + ).json() except requests.exceptions.RequestException as e: raise HTTPError(str(e)) @@ -59,40 +110,11 @@ class MobileAuth(object): self.key = backend.load_rsa_public_numbers(nums) self.timestamp = resp['timestamp'] - - def request(self, uri, data): - if not self.complete: - return None - - headers = { - 'X-Requested-With': 'com.valvesoftware.android.steam.community', - 'User-agent': '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' - } - - try: - response = self.session.post(uri, data=data, headers=headers) - except requests.exceptions.RequestException as e: - raise HTTPError(str(e)) - else: - return response - - def refreshSession(self, oauth_token=None): - oauth_token = oauth_token or self.oauth['oauth_token'] - response = self.request('https://api.steampowered.com/IMobileAuthService/GetWGToken/v0001', {'access_token': oauth_token}) - try: - data = json.loads(response) - except Exception, e: - raise RefreshSessionFailed(str(e)) - else: - self.oauth['wgtoken'] = data['response']['token'] - self.oauth['wgtoken_secure'] = data['response']['token_secure'] - self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, sself.oauth['wgtoken']), domain=domain, secure=False) - self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, self.oauth['wgtoken_secure']), domain=domain, secure=True) def login(self, captcha='', email_code='', twofactor_code='', language='english'): if self.complete: return self.session - + for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: self.session.cookies.set('forceMobile', '1', domain=domain, secure=False) self.session.cookies.set('mobileClientVersion', '0 (2.1.3)', domain=domain, secure=False) @@ -103,7 +125,7 @@ class MobileAuth(object): self._load_key() data = { - 'username' : self.username, + 'username': self.username, "password": b64encode(self.key.encrypt(self.password.encode('ascii'), PKCS1v15())), "emailauth": email_code, "emailsteamid": str(self.steamid) if email_code else '', @@ -129,11 +151,14 @@ class MobileAuth(object): if resp['success'] and resp['login_complete']: self.complete = True self.password = None + resp['oauth'] = json.loads(resp['oauth']) self.steamid = SteamID(resp['oauth']['steamid']) self.oauth = resp['oauth'] for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: - self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, resp['oauth']['wgtoken']), domain=domain, secure=False) - self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, data['oauth']['wgtoken_secure']), domain=domain, secure=True) + self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, resp['oauth']['wgtoken']), + domain=domain, secure=False) + self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, resp['oauth']['wgtoken_secure']), + domain=domain, secure=True) return resp else: @@ -151,23 +176,26 @@ class MobileAuth(object): return None + class MobileWebAuthException(Exception): pass + class HTTPError(MobileWebAuthException): pass + class LoginIncorrect(MobileWebAuthException): pass + class CaptchaRequired(MobileWebAuthException): pass + class EmailCodeRequired(MobileWebAuthException): pass -class TwoFactorCodeRequired(MobileWebAuthException): - pass -class RefreshSessionFailed(MobileWebAuthException): +class TwoFactorCodeRequired(MobileWebAuthException): pass diff --git a/steam/webauth.py b/steam/webauth.py index 4e7d57b..3cc9513 100644 --- a/steam/webauth.py +++ b/steam/webauth.py @@ -70,9 +70,9 @@ else: class WebAuth(object): key = None complete = False #: whether authentication has been completed successfully - session = None #: :class:`requests.Session` (with auth cookies after auth is complete) + session = None #: :class:`requests.Session` (with auth cookies after auth is complete) captcha_gid = -1 - steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) + steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) def __init__(self, username, password): self.__dict__.update(locals()) @@ -97,12 +97,12 @@ class WebAuth(object): """ try: resp = self.session.post('https://store.steampowered.com/login/getrsakey/', - timeout=15, - data={ - 'username': username, - 'donotchache': int(time() * 1000), - }, - ).json() + timeout=15, + data={ + 'username': username, + 'donotchache': int(time() * 1000), + }, + ).json() except requests.exceptions.RequestException as e: raise HTTPError(str(e)) @@ -144,7 +144,7 @@ class WebAuth(object): self._load_key() data = { - 'username' : self.username, + 'username': self.username, "password": b64encode(self.key.encrypt(self.password.encode('ascii'), PKCS1v15())), "emailauth": email_code, "emailsteamid": str(self.steamid) if email_code else '', @@ -199,17 +199,22 @@ class WebAuth(object): class WebAuthException(Exception): pass + class HTTPError(WebAuthException): pass + class LoginIncorrect(WebAuthException): pass + class CaptchaRequired(WebAuthException): pass + class EmailCodeRequired(WebAuthException): pass + class TwoFactorCodeRequired(WebAuthException): pass From dcc7f0d65d6246523e451182db18a9b86181bdba Mon Sep 17 00:00:00 2001 From: Philipp Date: Sat, 4 Jun 2016 11:23:03 +0200 Subject: [PATCH 07/16] Update webauth.py fixed --- steam/webauth.py | 41 ++--------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/steam/webauth.py b/steam/webauth.py index 16b2063..1268457 100644 --- a/steam/webauth.py +++ b/steam/webauth.py @@ -76,9 +76,9 @@ else: class WebAuth(object): key = None complete = False #: whether authentication has been completed successfully - session = None #: :class:`requests.Session` (with auth cookies after auth is complete) + session = None #: :class:`requests.Session` (with auth cookies after auth is complete) captcha_gid = -1 - steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) + steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) def __init__(self, username, password): self.__dict__.update(locals()) @@ -106,15 +106,6 @@ class WebAuth(object): :raises HTTPError: any problem with http request, timeouts, 5xx, 4xx etc """ try: -<<<<<<< HEAD - resp = self.session.post('https://store.steampowered.com/login/getrsakey/', - timeout=15, - data={ - 'username': username, - 'donotchache': int(time() * 1000), - }, - ).json() -======= resp = self.session.post('https://steamcommunity.com/login/getrsakey/', timeout=15, data={ @@ -122,7 +113,6 @@ class WebAuth(object): 'donotchache': int(time() * 1000), }, ).json() ->>>>>>> refs/remotes/ValvePython/master except requests.exceptions.RequestException as e: raise HTTPError(str(e)) @@ -185,29 +175,7 @@ class WebAuth(object): return self.session self._load_key() -<<<<<<< HEAD - - data = { - 'username': self.username, - "password": b64encode(self.key.encrypt(self.password.encode('ascii'), PKCS1v15())), - "emailauth": email_code, - "emailsteamid": str(self.steamid) if email_code else '', - "twofactorcode": twofactor_code, - "captchagid": self.captcha_gid, - "captcha_text": captcha, - "loginfriendlyname": "python-steam webauth", - "rsatimestamp": self.timestamp, - "remember_login": 'true', - "donotcache": int(time() * 100000), - } - - try: - resp = self.session.post('https://store.steampowered.com/login/dologin/', data=data, timeout=15).json() - except requests.exceptions.RequestException as e: - raise HTTPError(str(e)) -======= resp = self._send_login(captcha=captcha, email_code=email_code, twofactor_code=twofactor_code) ->>>>>>> refs/remotes/ValvePython/master self.captcha_gid = -1 @@ -283,22 +251,17 @@ class MobileWebAuth(WebAuth): class WebAuthException(Exception): pass - class HTTPError(WebAuthException): pass - class LoginIncorrect(WebAuthException): pass - class CaptchaRequired(WebAuthException): pass - class EmailCodeRequired(WebAuthException): pass - class TwoFactorCodeRequired(WebAuthException): pass From dfeba66e8ced8ad4a550951b91135d622f564cc6 Mon Sep 17 00:00:00 2001 From: philippj Date: Sat, 4 Jun 2016 18:37:32 +0200 Subject: [PATCH 08/16] Added account.py --- steam/account.py | 139 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 steam/account.py diff --git a/steam/account.py b/steam/account.py new file mode 100644 index 0000000..b3dcc1c --- /dev/null +++ b/steam/account.py @@ -0,0 +1,139 @@ +""" +This module is used to safely store account credentials and provide mobile authenticator codes. + +Example usage: + +.. code:: python + + import steam.account + + account = steam.account.SteamAccount('username', 'password') + account.set_account_property('identity_secret', 'XYZ') + account.set_account_property('shared_secret', 'XYZ') + code = account.login_code + key = account.get_confirmation_key('conf') + + + +TODO: + - Where to save the credentials (Windows/Linux)? + - Implement mobile authenticator features? +""" +import os +import sys +import json +import base64 + +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend + +from steam.guard import * + +if sys.platform.startswith('win'): + BASE_LOCATION = '.' #Windows +else: + BASE_LOCATION = '.' + +ACCOUNT_ATTRIBUTES = ['username', 'password', 'steamid', 'shared_secret', 'identity_secret'] + +class SteamAccount(object): + username = None + password = None + _path = None + _file = None + _fernet_key = None + _fernet_suite = None + + def __init__(self, username, password): + self.username = username + self.password = password + self._setup() + + def __del__(self): + try: + self._update_credential_file() + except TypeError: + """ + Ignore TypeError exception when destructor gets called after the memory has been cleared + """ + pass + self._file.close() + + def set_account_property(self, property, value): + setattr(self, property, value) + self._update_credential_file() + + @property + def login_code(self): + try: + return generate_twofactor_code(self.shared_secret) + except AttributeError: + raise SharedSecretNotSet('Add shared_secret to this instance to generate login codes') + + def get_confirmation_key(self, tag, timestamp=None): + if not timestamp: + timestamp = get_time_offset() + try: + return generate_confirmation_key(self.identity_secret, timestamp, tag) + except AttributeError: + raise IdentitySecretNotSet('Add identity_secret to this instance to generate confirmation keys') + + def _setup(self): + self._generate_fernet_key() + self._spawn_fernet_suite() + self._path = '%s/%s' % (BASE_LOCATION, self.username) + self._file = open(self._path, 'r+') + if not os.path.isfile(self._path): + self._create_credential_file() + else: + credentials = self._parse_credential_file() + for key, value in credentials.iteritems(): + setattr(self, key, value) + + def _create_credential_file(self): + data = json.dumps({ + 'username': self.username, + 'password': self.password + }) + text = self._fernet_suite.encrypt(data) + self._file.write(text) + + def _parse_credential_file(self): + + text = self._file.read() + data = json.loads(self._fernet_suite.decrypt(text)) + return data + + def _update_credential_file(self): + credentials = self._gather_credentials() + data = json.dumps(credentials) + + text = self._fernet_suite.encrypt(data) + self._file.truncate() + self._file.write(text) + + def _gather_credentials(self): + data = { } + names = dir(self) + for name in names: + if name in ACCOUNT_ATTRIBUTES: + data.__setitem__(name, getattr(self, name)) + return data + + def _generate_fernet_key(self): + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(bytes(self.password)) + self._fernet_key = base64.urlsafe_b64encode(digest.finalize()) + + def _spawn_fernet_suite(self): + self._fernet_suite = Fernet(self._fernet_key) + +class SteamAccountException(Exception): + pass + +class SharedSecretNotSet(SteamAccountException): + pass + +class IdentitySecretNotSet(SteamAccountException): + pass \ No newline at end of file From e142411adef26575f79606ed5d9fe808d9b9c453 Mon Sep 17 00:00:00 2001 From: Philipp J Date: Sat, 4 Jun 2016 19:22:04 +0200 Subject: [PATCH 09/16] cleanup --- steam/authenticator.py | 98 ------------------ steam/mobile.py | 220 ----------------------------------------- steam/mobileauth.py | 201 ------------------------------------- 3 files changed, 519 deletions(-) delete mode 100644 steam/authenticator.py delete mode 100644 steam/mobile.py delete mode 100644 steam/mobileauth.py diff --git a/steam/authenticator.py b/steam/authenticator.py deleted file mode 100644 index ee92b1e..0000000 --- a/steam/authenticator.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This module provides methods for managing the mobile authenticator - -.. warning:: - Save your credentials! - -Example usage: - -.. code:: python - - import steam.authenticator - - ma = steam.authenticator.MobileAuthenticator('username', 'password') - credentials = ma.add_authenticator() - - sms_code = raw_input('SMS Code: ') - - ma.finalize_authenticator(sms_code) - - print credentials - -""" -import json - -from guard import * -from mobile import SteamMobile - - -class MobileAuthenticator(object): - mobile = None - - def __init__(self, username, password, login_mode='normal', email_code='', twofactor_code=''): - self.mobile = SteamMobile(username, password, login_mode, email_code, twofactor_code) - - def add_authenticator(self): - """Start process of linking a mobile authenticator to the logged in steam account - - :return: account credentials or False - :rtype: :class:`tuple`, :class:`bool` - """ - - data = { - 'steamid': self.mobile.steamid, - 'sms_phone_id': 1, - 'access_token': self.mobile.oauth['oauth_token'], - 'authenticator_time': get_time_offset(), - 'authenticator_type': 1, - 'device_identifier': generate_device_id(self.mobile.steamid) - } - - [status, body] = self.mobile.api_request('ITwoFactorService', 'AddAuthenticator', data, - return_including_status_code=True) - if status == 200: - responseData = json.loads(body) - self.credentials = responseData['response'] - self.credentials['secret'] = self.credentials['uri'].split('?secret=')[1].split('&issuer')[0] - return responseData - else: - return False - - def finalize_authenticator(self, sms_code=None, tries=1): - """Start process of linking a mobile authenticator to the logged in steam account - - :param sms_code: text reponse recieved by sms - :type sms_code: :class:`str` - :return: :class:`None` it no sms code is supplied, `True` or `False` - :rtype: :class:`None`, :class:`bool` - """ - - if not sms_code: - return None - - timestamp = get_time_offset() - - data = { - 'steamid': self.mobile.steamid, - 'access_token': self.mobile.oauth['oauth_token'], - 'authenticator_time': timestamp, - 'authenticator_code': generate_twofactor_code_for_time(self.credentials['shared_secret'], timestamp), - 'activation_code': sms_code - } - [status, body] = self.mobile.api_request('ITwoFactorService', 'FinalizeAddAuthenticator', data, - return_including_status_code=True) - if status == 200: - responseData = json.loads(body)['response'] - if responseData['success']: - return True - else: - if responseData['want_more'] and tries < 30: - return self.finalizeAuthenticator(sms_code, tries) - else: - return False - else: - return False - - -class MobileAuthenticatorException(Exception): - pass \ No newline at end of file diff --git a/steam/mobile.py b/steam/mobile.py deleted file mode 100644 index 8a1b7af..0000000 --- a/steam/mobile.py +++ /dev/null @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- -""" -This module provides utility functions to access steam mobile pages -""" - -import json -import requests - -import mobileauth - -API_ENDPOINTS = { - "IMobileAuthService": { - "methods": { - "GetWGToken": { - "version": 1 - } - } - }, - "ISteamWebUserPresenceOAuth": { - "methods": { - "Logon": { - "version": 1 - }, - "Logoff": { - "version": 1 - }, - "Message": { - "version": 1 - }, - "DeviceInfo": { - "version": 1 - }, - "Poll": { - "version": 1 - } - } - }, - "ISteamUserOAuth": { - "methods": { - "GetUserSummaries": { - "version": 1 - }, - "GetGroupSummaries": { - "version": 1 - }, - "GetGroupList": { - "version": 1 - }, - "GetFriendList": { - "version": 1 - }, - "Search": { - "version": 1 - } - } - }, - - "ISteamGameOAuth": { - "methods": { - "GetAppInfo": { - "version": 1 - } - } - }, - "IMobileNotificationService": { - "methods": { - "SwitchSessionToPush": { - "version": 1 - } - } - }, - "IFriendMessagesService": { - "methods": { - "GetActiveMessageSessions": { - "version": 1 - }, - "GetRecentMessages": { - "version": 1 - }, - "MarkOfflineMessagesRead": { - "version": 1 - } - } - }, - "ITwoFactorService": { - "methods": { - "AddAuthenticator": { - "version": 1 - }, - "RecoverAuthenticatorCommit": { - "version": 1 - }, - "RecoverAuthenticatorContinue": { - "version": 1 - }, - "RemoveAuthenticator": { - "version": 1 - }, - "RemoveAuthenticatorViaChallengeStart": { - "version": 1 - }, - "RemoveAuthenticatorViaChallengeContinue": { - "version": 1 - }, - "FinalizeAddAuthenticator": { - "version": 1 - }, - "QueryStatus": { - "version": 1 - }, - "QueryTime": { - "version": 1 - }, - "QuerySecrets": { - "version": 1 - }, - "SendEmail": { - "version": 1 - }, - "ValidateToken": { - "version": 1 - }, - "CreateEmergencyCodes": { - "version": 1 - }, - "DestroyEmergencyCodes": { - "version": 1 - } - } - } -} - -API_BASE_URL = 'https://api.steampowered.com' - - -class SteamMobile(object): - session = None - oauth = None - steamid = None - - def __init__(self, username, password, login_mode='normal', email_code='', twofactor_code=''): - mobile_auth = mobileauth.MobileAuth(username, password) - - try: - if login_mode == 'normal': - mobile_auth.login() - elif login_mode == 'email': - mobile_auth.login(email_code=email_code) - elif login_mode == 'twofa': - mobile_auth.login(twofactor_code=twofactor_code) - - except mobileauth.CaptchaRequired: - raise CaptchaNotSupported("Captcha's are currently not supported. Please wait a few minutes before you try to login again.") - except mobileauth.EmailCodeRequired: - mobile_auth.login(email_code=email_code) - except mobileauth.TwoFactorCodeRequired: - mobile_auth.login(twofactor_code=twofactor_code) - - - self.session = mobile_auth.session - self.steamid = mobile_auth.steamid - self.oauth = mobile_auth.oauth - - def refresh_session(self, oauth_token=None): - oauth_token = oauth_token or self.oauth['oauth_token'] - response = self.api_request('IMobileAuthService', 'GetWGToken', {'access_token': oauth_token}) - try: - data = json.loads(response) - except Exception, e: - raise SessionRefreshFailed(str(e)) - else: - self.oauth['wgtoken'] = data['response']['token'] - self.oauth['wgtoken_secure'] = data['response']['token_secure'] - for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: - self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, self.oauth['wgtoken']), domain=domain, - secure=False) - self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, self.oauth['wgtoken_secure']), - domain=domain, secure=True) - - def _request(self, uri, data={}, return_including_status_code=False): - headers = { - 'X-Requested-With': 'com.valvesoftware.android.steam.community', - 'User-agent': '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' - } - - try: - response = self.session.post(uri, data=data, headers=headers) - except requests.exceptions.RequestException as e: - raise mobileauth.HTTPError(str(e)) - else: - if return_including_status_code: - return [response.status_code, response.text] - else: - return response.text - - def api_request(self, interface, method, data={}, return_including_status_code=False): - if interface in API_ENDPOINTS.keys() and method in API_ENDPOINTS.get(interface).get('methods').keys(): - uri = '%s/%s/%s/v%s/' % ( - API_BASE_URL, interface, method, API_ENDPOINTS.get(interface).get('methods').get(method).get('version')) - response = self._request(uri, data, return_including_status_code) - return response - else: - raise APIEndpointNotFound('Endpoint %s.%s not found' % (interface, method)) - - -class SteamMobileException(Exception): - pass - - -class CaptchaNotSupported(SteamMobileException): - pass - - -class SessionRefreshFailed(SteamMobileException): - pass - - -class APIEndpointNotFound(SteamMobileException): - pass \ No newline at end of file diff --git a/steam/mobileauth.py b/steam/mobileauth.py deleted file mode 100644 index 3004496..0000000 --- a/steam/mobileauth.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -""" -This module simplifies the process of obtaining an authenticated session for mobile steam websites. -After authentication is complete, a :class:`requests.Session` is created containing the auth and mobile specific cookies. -The session can be used to access ``steamcommunity.com``, ``store.steampowered.com``, and ``help.steampowered.com``. -The main purpose of a mobile session, is to access the mobile trade confirmations page and to easily access the -ITwoFactorService api, but can also used to access all 'normal' steam pages. - - -.. warning:: - A web session may expire randomly, or when you login from different IP address. - Some pages will return status code `401` when that happens. - Keep in mind if you are trying to write robust code. - -Example usage: - -.. code:: python - - import steam.mobileauth as ma - - user = ma.WebAuth('username', 'password') - - try: - user.login() - except wa.CaptchaRequired: - print user.captcha_url - # ask a human to solve captcha - user.login(captcha='ABC123') - except wa.EmailCodeRequired: - user.login(email_code='ZXC123') - except wa.TwoFactorCodeRequired: - user.login(twofactor_code='ZXC123') - - user.session.get('https://store.steampowered.com/account/history/') - # OR - session = user.login() - session.get('https://store.steampowered.com/account/history') - -Alternatively, if Steam Guard is not enabled on the account: - -.. code:: python - - try: - session = wa.WebAuth('username', 'password').login() - except wa.HTTPError: - pass - -The :class:`MobileAuth` instance should be discarded once a session is obtained -as it is not reusable. -""" -import json -from time import time -import sys -from base64 import b64encode -import requests - -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers -from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from steam.core.crypto import backend - -from steam.util.web import make_requests_session -from steam import SteamID - -if sys.version_info < (3,): - intBase = long -else: - intBase = int - - -class MobileAuth(object): - key = None - complete = False #: whether authentication has been completed successfully - session = None #: :class:`requests.Session` (with auth cookies after auth is complete) - captcha_gid = -1 - steamid = None #: :class:`steam.steamid.SteamID` (after auth is complete) - oauth = {} - - def __init__(self, username, password): - self.__dict__.update(locals()) - self.session = make_requests_session() - - @property - def captcha_url(self): - if self.captcha_gid == -1: - return None - else: - return "https://store.steampowered.com/login/rendercaptcha/?gid=%s" % self.captcha_gid - - def get_rsa_key(self, username): - try: - resp = self.session.post('https://steamcommunity.com/mobilelogin/getrsakey/', - timeout=15, - data={ - 'username': username, - 'donotchache': int(time() * 1000), - }, - ).json() - except requests.exceptions.RequestException as e: - raise HTTPError(str(e)) - - return resp - - def _load_key(self): - if not self.key: - resp = self.get_rsa_key(self.username) - - nums = RSAPublicNumbers(intBase(resp['publickey_exp'], 16), - intBase(resp['publickey_mod'], 16), - ) - - self.key = backend.load_rsa_public_numbers(nums) - self.timestamp = resp['timestamp'] - - def login(self, captcha='', email_code='', twofactor_code='', language='english'): - if self.complete: - return self.session - - for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: - self.session.cookies.set('forceMobile', '1', domain=domain, secure=False) - self.session.cookies.set('mobileClientVersion', '0 (2.1.3)', domain=domain, secure=False) - self.session.cookies.set('mobileClient', 'android', domain=domain, secure=False) - self.session.cookies.set('Steam_Language', 'english', domain=domain, secure=False) - self.session.cookies.set('dob', '', domain=domain, secure=False) - - self._load_key() - - data = { - 'username': self.username, - "password": b64encode(self.key.encrypt(self.password.encode('ascii'), PKCS1v15())), - "emailauth": email_code, - "emailsteamid": str(self.steamid) if email_code else '', - "twofactorcode": twofactor_code, - "captchagid": self.captcha_gid, - "captcha_text": captcha, - "loginfriendlyname": "python-steam webauth", - "rsatimestamp": self.timestamp, - "remember_login": 'true', - "donotcache": int(time() * 100000), - } - data['oauth_client_id'] = 'DE45CD61' - data['oauth_scope'] = 'read_profile write_profile read_client write_client' - data['loginfriendlyname'] = '#login_emailauth_friendlyname_mobile' - - try: - resp = self.session.post('https://steamcommunity.com/mobilelogin/dologin/', data=data, timeout=15).json() - except requests.exceptions.RequestException as e: - raise HTTPError(str(e)) - - self.captcha_gid = -1 - - if resp['success'] and resp['login_complete']: - self.complete = True - self.password = None - resp['oauth'] = json.loads(resp['oauth']) - self.steamid = SteamID(resp['oauth']['steamid']) - self.oauth = resp['oauth'] - for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: - self.session.cookies.set('steamLogin', '%s||%s' % (self.steamid, resp['oauth']['wgtoken']), - domain=domain, secure=False) - self.session.cookies.set('steamLoginSecure', '%s||%s' % (self.steamid, resp['oauth']['wgtoken_secure']), - domain=domain, secure=True) - - return resp - else: - if resp.get('captcha_needed', False): - self.captcha_gid = resp['captcha_gid'] - - raise CaptchaRequired(resp['message']) - elif resp.get('emailauth_needed', False): - self.steamid = SteamID(resp['emailsteamid']) - raise EmailCodeRequired(resp['message']) - elif resp.get('requires_twofactor', False): - raise TwoFactorCodeRequired(resp['message']) - else: - raise LoginIncorrect(resp['message']) - - return None - - -class MobileWebAuthException(Exception): - pass - - -class HTTPError(MobileWebAuthException): - pass - - -class LoginIncorrect(MobileWebAuthException): - pass - - -class CaptchaRequired(MobileWebAuthException): - pass - - -class EmailCodeRequired(MobileWebAuthException): - pass - - -class TwoFactorCodeRequired(MobileWebAuthException): - pass From 250912777e5ed895d2fbed49ee1d06ec3832f3e8 Mon Sep 17 00:00:00 2001 From: philippj Date: Sat, 4 Jun 2016 19:23:31 +0200 Subject: [PATCH 10/16] Added account.py --- steam/guard.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/steam/guard.py b/steam/guard.py index df3c185..7d184f2 100644 --- a/steam/guard.py +++ b/steam/guard.py @@ -1,11 +1,9 @@ import struct -import hashlib from binascii import hexlify from time import time from steam import webapi from steam.core.crypto import hmac_sha1, sha1_hash - def generate_twofactor_code(shared_secret): """Generate Steam 2FA code for login with current time @@ -86,5 +84,5 @@ def generate_device_id(steamid): :return: android device id :rtype: str """ - h = hexlify(str(hashlib.sha1(str(steamid).encode('ascii')))).decode('ascii') + h = hexlify(sha1(str(steamid).encode('ascii'))).decode('ascii') return "android:%s-%s-%s-%s-%s" % (h[:8], h[8:12], h[12:16], h[16:20], h[20:32]) From c6be934eec901dbbc877962e8c5ec80291bfb795 Mon Sep 17 00:00:00 2001 From: philippj Date: Sat, 11 Jun 2016 16:28:52 +0200 Subject: [PATCH 11/16] Added webpresence.py --- steam/webpresence.py | 363 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 steam/webpresence.py diff --git a/steam/webpresence.py b/steam/webpresence.py new file mode 100644 index 0000000..782f5c1 --- /dev/null +++ b/steam/webpresence.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- +""" +Wrapping methods for `ISteamWebUserPresenceOAuth` and `Polling` functionality + +Example usage: + +.. code:: python + import steam.webauth + import steam.webpresence + webAuth = steam.webauth.MobileWebAuth('username', 'password') + webAuth.login() + + def my_callback(messages): + for message in messages: + print '[%s] %s from %s' % (message.timestamp, message.type, message.steamid_from.as_64) + + webPresence = steam.webpresence.WebUserPresence(webAuth) + webPresence.logon() + webPresence.start_polling(my_callback) + print 'Started polling' +""" +import threading +import requests + +from steam.webauth import MobileWebAuth +from steam import SteamID + +DEFAULT_MOBILE_HEADERS = { + 'X-Requested-With': 'com.valvesoftware.android.steam.community', + 'User-agent': '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' +} + +API_BASE = 'https://api.steampowered.com' + +DEFAULT_TIMEOUT = 30 + +PERSONA_STATES = ['Offline', 'Online', 'Busy', 'Away', 'Snooze', 'Looking to trade', 'Looking to play'] + +class WebUserPresence: + """ + Wrapping methods for `ISteamWebUserPresenceOAuth` and `Polling` functionality + """ + _loggedon = False + _timeout = None + + _oauth_token = None + _session = None + + _steamid = None + _umqid = None + _message_base = None + + def __init__(self, mobile_web_auth, timeout=None): + if not isinstance(mobile_web_auth, MobileWebAuth): + raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `MobileWebAuth`.') + + if not mobile_web_auth.complete: + raise InstanceNotReady('Please make sure your `MobileWebAuth` instance is logged in.') + + self._timeout = timeout + self._oauth_token = mobile_web_auth.oauth_token + self._session = mobile_web_auth.session + + def _request(self, uri, data, timeout=None): + """ + HTTP Request + :param uri: URI to be requested + :param data: Data to be delivered + :param timeout: HTTP timeout + :return: `requests.Response` + """ + timeout = timeout or self._timeout or DEFAULT_TIMEOUT + return self._session.post(uri, data=data, headers=DEFAULT_MOBILE_HEADERS, timeout=timeout) + + def _call(self, method, data, timeout=None): + """ + Calling an `ISteamWebUserPresenceOAuth` api method + :param method: Method to be called + :param data: Data to be delivered + :param timeout: HTTP timeout + :return: If successful, response as tuple, otherwise exceptions are thrown + """ + uri = '%s/ISteamWebUserPresenceOAuth/%s/v1/' % (API_BASE, method) + + try: + response = self._request(uri, data, timeout=timeout) + except requests.exceptions.ReadTimeout: + raise HTTPError('Timeout') + + if response.status_code == 401: + raise NotAuthorized('Not authorized. Please check your OAuth token and verify your `MobileWebAuth` login.') + elif response.status_code != 200: + raise HTTPError('HTTP request failed. Status code: %s' % response.status_code) + + if self._loggedon: + self._message_base += 1 + + try: + json_response = response.json() + except: + raise ValueError('Could not build json_response') + else: + return json_response + + def logon(self): + """ + Sends logon to the api + :return: True if successful. Raises an `LogonFailed` exception otherwise + """ + login_data = { + 'access_token': self._oauth_token + } + response = self._call('Logon', login_data) + if response.get('error') == 'OK': + self._steamid = SteamID(response.get('steamid')) + self._umqid = response.get('umqid') + self._message_base = response.get('message') + + self._loggedon = True + return True + else: + raise LogonFailed('Logon failed. Please check your OAuth token and verify your `MobileWebAuth` login.') + + def logoff(self): + """ + Sends logoff to the api + :return: True if logoff was successful, False otherwise. + """ + response = self._call('Logoff', self._build_call_data({}), True) + self._loggedon = False + return response.get('error') == 'OK' or False + + def poll(self): + """ + Starts an poll request + :return: Full response as Tuple + """ + poll_data = { + 'pollid': 0, + 'sectimeout': 5, + 'secidletime': 0, + 'use_accountids': 1 + } + response = self._call('Poll', self._build_call_data(poll_data, True), timeout=60) + if response.get('error') == 'OK': + return response + else: + raise PollCreationFailed(response.get('error')) + + def start_polling(self, callback): + """ + Creates an instance of `WebPolling` and starts the thread + :param callback: Function reference for handling `WebPollMessages` + :return: True + """ + web_polling = self._spawn_web_polling(callback) + web_polling.start() + return True + + def stop_polling(self): + """ + Stops the active `WebPolling` + :return: True, False. + """ + return self._kill_web_polling() + + def message(self, steamid, text): + """ + Delivers a steam message + :param steamid: SteamID64 of the target user + :param text: Message to deliver + :return: True if deliver was successful, False otherwise + """ + message_data = self._build_call_data({ + 'text': text, + 'type': 'saytext', + 'steamid_dst': steamid + }) + response = self._call('Message', message_data) + return response.get('error') == 'OK' or False + + def _spawn_web_polling(self, callback): + """ + Spawns a `WebPolling` instance + :param callback: A method reference for handling incoming `WebPollMessages` + :return: Instance reference + """ + self._web_polling = WebPolling(self, callback) + return self._web_polling + + def _kill_web_polling(self): + """ + Stops the run() method of WebPolling after the current poll request + :return: True if `_web_polling` is spawned and could be stopped, False otherwise + """ + if getattr(self, '_web_polling'): + self._web_polling._active = False + return True + return False + + def _prep_messages(self, messages): + """ + Prepares incoming raw `WebPolling` messages by creating `WebPollMessage` instances + :param messages: raw `WebPolling` messages + :return: List of `WebPollMessages` instances + """ + prepped_messages = [ ] + for message in messages: + prepped_messages.append(WebPollMessage(message, self)) + return prepped_messages + + def _build_call_data(self, data, set_message_base=False): + """ + Builds the data for `_call`'s + :param data: Data to deliver as Tuple + :param set_message_base: True, False. Only required for calls where `message` has to be set + :return: Prepared call data + """ + base_data = { + 'umqid': self._umqid, + 'access_token': self._oauth_token + } + if set_message_base: + base_data.__setitem__('message', self._message_base) + + for key in data: + base_data.__setitem__(key, data.get(key)) + + return base_data + +class WebPolling(threading.Thread): + """ + Threaded class for `Polling` + """ + _active = True + _web_user_presence = None + _callback = None + + def __init__(self, web_user_presence, callback): + threading.Thread.__init__(self) + + if not isinstance(web_user_presence, WebUserPresence): + raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `WebUserPresence`.') + + if not web_user_presence._loggedon: + raise InstanceNotReady('The `WebUserPresence` has to be logged on.') + + self._web_user_presence = web_user_presence + self._callback = callback + + self.setDaemon(False) + + def run(self): + """ + Sends HTTP requests while class is `_active` and calls a specified callback + :return: + """ + while self._active: + try: + response = self._web_user_presence.poll() + except PollCreationFailed: + """ + Ignore timeout exception + """ + pass + except HTTPError: + """ + Ignore http exceptions + """ + pass + else: + prepped_messages = self._web_user_presence._prep_messages(response.get('messages')) + if self._callback: + self._callback(prepped_messages) + +class WebPollMessage(object): + """ + Class for proper handling of polling messages + """ + complete = False + + _web_user_presence = None + + timestamp = 0 + type = None + steamid_from = SteamID() + + text = None + + persona_state = 0 + persona_name = None + status_flags = None + + _answerable = False + _full_data = None + + def __init__(self, message, web_user_presence): + if not isinstance(web_user_presence, WebUserPresence): + raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `WebUserPresence`.') + + if not web_user_presence._loggedon: + raise InstanceNotReady('The `WebUserPresence` has to be logged on.') + + self._full_data = message + + self._web_user_presence = web_user_presence + + self.timestamp = message.get('utc_timestamp') + self.type = message.get('type') + self.steamid_from = SteamID(message.get('accountid_from')) + + if self.type == 'typing': + self._answerable = True + elif self.type == 'saytext': + self._answerable = True + self.text = message.get('text') + elif self.type == 'personastate': + self.persona_name = message.get('persona_name') + self.persona_state = message.get('persona_state') + self.status_flags = message.get('status_flags') + + def answer(self, message): + """ + If the current message is `_answerable` ( current message has something to do with the steam chat ) an + answer can be sent + :param message: Message to be delivered + :return: True if message could be delivered, False otherwise + """ + if not self._answerable: + return False + self._web_user_presence.message(self.steamid_from.as_64, message) + return True + + def persona_state_to_str(self): + """ + Returns the `persona_state` as string + :return: `persona_state` as String or False if `persona_state` is not available + """ + if self.persona_state: + return PERSONA_STATES.__getitem__(self.persona_state) + return False + +class WebUserPresenceException(Exception): + pass + +class InvalidInstanceSupplied(WebUserPresenceException): + pass + +class InstanceNotReady(WebUserPresenceException): + pass + +class NotAuthorized(WebUserPresenceException): + pass + +class LogonFailed(WebUserPresenceException): + pass + +class PollCreationFailed(WebUserPresenceException): + pass + +class HTTPError(WebUserPresenceException): + pass \ No newline at end of file From e4b7e263b198e2303b6b843f3891d5e1b5bdc2f4 Mon Sep 17 00:00:00 2001 From: philippj Date: Sun, 12 Jun 2016 19:30:11 +0200 Subject: [PATCH 12/16] Added mobile authenticator features & Mobile/WebAuth to account.py --- steam/account.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/steam/account.py b/steam/account.py index b3dcc1c..a999c2d 100644 --- a/steam/account.py +++ b/steam/account.py @@ -23,19 +23,28 @@ import os import sys import json import base64 +import re from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from steam.guard import * +import steam.webauth if sys.platform.startswith('win'): BASE_LOCATION = '.' #Windows else: BASE_LOCATION = '.' -ACCOUNT_ATTRIBUTES = ['username', 'password', 'steamid', 'shared_secret', 'identity_secret'] +DEFAULT_MOBILE_HEADERS = { + 'X-Requested-With': 'com.valvesoftware.android.steam.community', + 'User-agent': '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' +} + +ACCOUNT_ATTRIBUTES = ['username', 'password', 'steamid', 'shared_secret', 'identity_secret', 'revocation_code',\ + 'secret_1', 'serial_number', 'deviceid', 'oauth_token'] class SteamAccount(object): username = None @@ -44,6 +53,8 @@ class SteamAccount(object): _file = None _fernet_key = None _fernet_suite = None + _web_auth = None + _session = None def __init__(self, username, password): self.username = username @@ -79,6 +90,141 @@ class SteamAccount(object): except AttributeError: raise IdentitySecretNotSet('Add identity_secret to this instance to generate confirmation keys') + def fetch_mobile_confirmations(self, retries=1): + self._verify_mobile_session() + + if not self._verify_mobile_authenticator(): + raise MobileAuthenticatorException('The steam mobile authenticator is required to access the mobile confirmations.') + + timestamp = get_time_offset() + confirmation_key = self.get_confirmation_key('conf', timestamp) + + confirmation_uri = 'https://steamcommunity.com/mobileconf/conf?p=%s&a=%s&k=%s&t=%s&m=android&tag=conf' %\ + ( self.deviceid, self.steamid, confirmation_key, timestamp) + + response = self.session.get(confirmation_uri, headers=DEFAULT_MOBILE_HEADERS) + + raw_confirmations = [ ] + + if response.status_code == 200: + if 'Invalid authenticator' in response.text: + retries += 1 + return self.fetch_mobile_confirmations(retries) + + confirmation_ids = re.findall(r'data-confid="(\d+)"', response.text) + confirmation_keys = re.findall(r'data-key="(\d+)"', response.text) + confirmation_descriptions = re.findall(r'
((Confirm|Trade with|Sell -) .+)<\/div>', response.text) + + if confirmation_ids and confirmation_keys: + for index, confirmation_id in enumerate(confirmation_ids): + raw_confirmations.append({ + 'id': confirmation_id, + 'key': confirmation_keys[index], + 'description': confirmation_descriptions[index] + }) + return raw_confirmations + return [ ] + + def add_mobile_authenticator(self): + if self._verify_mobile_authenticator(): + raise MobileAuthenticatorException('The steam mobile authenticator is already enabled.') + + self._verify_mobile_session() + + deviceid = getattr(self, 'deviceid') or generate_device_id(self.steamid) + + data = { + 'steamid': self.steamid, + 'sms_phone_id': 1, + 'access_token': self.oauth_token, + 'authenticator_time': get_time_offset(), + 'authenticator_type': 1, + 'device_identifier': deviceid + } + + response = self.session.post('https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/', + data, headers=DEFAULT_MOBILE_HEADERS) + if response.status_code == 200: + response_json = json.loads(response.text) + if response_json.get('response').get('status') == 1: + self.set_account_property('shared_secret', response_json.get('response').get('shared_secret')) + self.set_account_property('identity_secret', response_json.get('response').get('identity_secret')) + self.set_account_property('revocation_code', response_json.get('response').get('revocation_code')) + self.set_account_property('secret_1', response_json.get('response').get('secret_1')) + self.set_account_property('serial_number', response_json.get('response').get('serial_number')) + self.set_account_property('deviceid', deviceid) + return True + return False + + def finalize_mobile_authenticator(self, sms_code, retries=1): + if self._verify_mobile_authenticator(): + raise MobileAuthenticatorException('The steam mobile authenticator is already enabled.') + + self._verify_mobile_session() + + if not sms_code: + raise SMSCodeNotProvided('The sms code is required for finalizing the process of adding the mobile\ + authenticator') + + timestamp = get_time_offset() + + data = { + 'steamid': self.steamid, + 'access_token': self.oauth_token, + 'authenticator_time': timestamp, + 'authenticator_code': generate_twofactor_code_for_time(self.shared_secret, timestamp), + 'activation_code': sms_code + } + + response = self.session.post('https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/', + data, headers=DEFAULT_MOBILE_HEADERS) + + if response.status_code == 200: + response_json = json.loads(response.text) + if response_json.get('response').get('success'): + self.set_account_property('has_mobile_authenticator', True) + return True + else: + if response_json.get('response').get('success') and retries < 30: + retries += 1 + return self._finalize_mobile_authenticator(sms_code, retries) + return False + + def remove_mobile_authenticator(self): + if not self._verify_mobile_authenticator(): + raise MobileAuthenticatorException('The steam mobile authenticator is not enabled.') + + self._verify_mobile_session() + + data = { + 'steamid': self.steamid, + 'steamguard_scheme': 2, + 'revocation_code': self.revocation_code, + 'access_token': self.oauth_token + } + + response = self.session.post('https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/', + data, headers=DEFAULT_MOBILE_HEADERS) + + if response.status_code == 200: + response_json = json.loads(response.text) + if response_json.get('response').get('success'): + self.set_account_property('has_mobile_authenticator', False) + return True + return False + + def _verify_mobile_session(self): + if not isinstance(self._web_auth, steam.webauth.MobileWebAuth): + raise MobileAuthenticatorException('A mobile session is required.') + + if self._web_auth.complete: + raise MobileAuthenticatorException('The mobile session has to be logged in to steam.') + + def _verify_mobile_authenticator(self): + if getattr(self, 'has_mobile_authenticator') and self.has_mobile_authenticator: + return True + return False + def _setup(self): self._generate_fernet_key() self._spawn_fernet_suite() @@ -129,6 +275,45 @@ class SteamAccount(object): def _spawn_fernet_suite(self): self._fernet_suite = Fernet(self._fernet_key) + def _spawn_web_session(self): + self._web_auth = steam.webauth.WebAuth(self.username, self.password) + + def _spawn_mobile_session(self): + self._web_auth = steam.webauth.MobileWebAuth(self.username, self.password) + + def _login_web_session(self, captcha='', email_code='', twofactor_code=''): + try: + self._web_auth.login() + + except steam.webauth.CaptchaRequired: + if not captcha: + raise CaptchaNotProvided('The steam login captcha is required for logging in, but was not provided.') + self._web_auth.login(captcha=captcha) + + except steam.webauth.EmailCodeRequired: + if not email_code: + raise EMailCodeNotProvided('The email code is required for logging in, but was not provided.') + self._web_auth.login(email_code=email_code) + + except steam.webauth.TwoFactorCodeRequired: + if not twofactor_code: + try: + twofactor_code = self.login_code + except SharedSecretNotSet: + raise TwoFACodeNotProvided('The twofactor code is required for logging in, but was not provided.') + self._web_auth.login(twofactor_code=twofactor_code) + + if self._web_auth.complete: + if not getattr(self, 'steamid'): + self.set_account_property('steamid', self._web_auth.steamid) + + if isinstance(self._web_auth, steam.webauth.MobileWebAuth) and not getattr(self, 'oauth_token'): + self.set_account_property('oauth_token', self._web_auth.oauth_token) + + self._session = self._web_auth.session + else: + raise WebAuthNotComplete('The web authentication could not be completed.') + class SteamAccountException(Exception): pass @@ -136,4 +321,25 @@ class SharedSecretNotSet(SteamAccountException): pass class IdentitySecretNotSet(SteamAccountException): + pass + +class WebAuthNotComplete(SteamAccountException): + pass + +class MobileAuthenticatorException(SteamAccountException): + pass + +class ParameterNotProvidedException(SteamAccountException): + pass + +class CaptchaNotProvided(ParameterNotProvidedException): + pass + +class EMailCodeNotProvided(ParameterNotProvidedException): + pass + +class TwoFACodeNotProvided(ParameterNotProvidedException): + pass + +class SMSCodeNotProvided(ParameterNotProvidedException): pass \ No newline at end of file From aa3678b8b662289b45de47c400ca921aa38a1c52 Mon Sep 17 00:00:00 2001 From: philippj Date: Fri, 17 Jun 2016 14:58:03 +0200 Subject: [PATCH 13/16] Removed webpresence & cleanup --- requirements.txt | 1 - steam/account.py | 314 ++++++++++++++++--------------------- steam/webpresence.py | 363 ------------------------------------------- 3 files changed, 137 insertions(+), 541 deletions(-) delete mode 100644 steam/webpresence.py diff --git a/requirements.txt b/requirements.txt index cb09240..d974099 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ coverage==4.0.3 -setuptools>=11.3 gevent-eventemitter==1.4 enum34==1.1.2; python_version < '3.4' gevent==1.1.0 diff --git a/steam/account.py b/steam/account.py index a999c2d..9e97306 100644 --- a/steam/account.py +++ b/steam/account.py @@ -10,14 +10,7 @@ Example usage: account = steam.account.SteamAccount('username', 'password') account.set_account_property('identity_secret', 'XYZ') account.set_account_property('shared_secret', 'XYZ') - code = account.login_code - key = account.get_confirmation_key('conf') - - - -TODO: - - Where to save the credentials (Windows/Linux)? - - Implement mobile authenticator features? + api_key = account.get_api_key() """ import os import sys @@ -29,37 +22,40 @@ from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend -from steam.guard import * +import steam.guard import steam.webauth +import steam.webapi if sys.platform.startswith('win'): BASE_LOCATION = '.' #Windows else: BASE_LOCATION = '.' -DEFAULT_MOBILE_HEADERS = { - 'X-Requested-With': 'com.valvesoftware.android.steam.community', - 'User-agent': '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' -} - ACCOUNT_ATTRIBUTES = ['username', 'password', 'steamid', 'shared_secret', 'identity_secret', 'revocation_code',\ - 'secret_1', 'serial_number', 'deviceid', 'oauth_token'] + 'secret_1', 'serial_number', 'deviceid', 'oauth_token', 'apikey'] class SteamAccount(object): username = None password = None + + mobile_session = None + web_session = None + web_api = None + authenticator = None + _path = None _file = None _fernet_key = None _fernet_suite = None _web_auth = None - _session = None + _mobile_auth = None + def __init__(self, username, password): self.username = username self.password = password - self._setup() + if not self._setup(): + raise SteamAccountException('Could not access account.') def __del__(self): try: @@ -75,189 +71,113 @@ class SteamAccount(object): setattr(self, property, value) self._update_credential_file() + def del_account_property(self, property): + delattr(self, property) + self._update_credential_file() + + def check_account_property(self, property): + return hasattr(self, property) + @property def login_code(self): - try: - return generate_twofactor_code(self.shared_secret) - except AttributeError: + if self.authenticator: + return self.authenticator.get_code() + elif hasattr(self, 'shared_secret'): + return steam.guard.generate_twofactor_code(self.shared_secret) + else: raise SharedSecretNotSet('Add shared_secret to this instance to generate login codes') - def get_confirmation_key(self, tag, timestamp=None): - if not timestamp: - timestamp = get_time_offset() - try: - return generate_confirmation_key(self.identity_secret, timestamp, tag) - except AttributeError: - raise IdentitySecretNotSet('Add identity_secret to this instance to generate confirmation keys') - - def fetch_mobile_confirmations(self, retries=1): - self._verify_mobile_session() - - if not self._verify_mobile_authenticator(): - raise MobileAuthenticatorException('The steam mobile authenticator is required to access the mobile confirmations.') - - timestamp = get_time_offset() - confirmation_key = self.get_confirmation_key('conf', timestamp) - - confirmation_uri = 'https://steamcommunity.com/mobileconf/conf?p=%s&a=%s&k=%s&t=%s&m=android&tag=conf' %\ - ( self.deviceid, self.steamid, confirmation_key, timestamp) - - response = self.session.get(confirmation_uri, headers=DEFAULT_MOBILE_HEADERS) - - raw_confirmations = [ ] - - if response.status_code == 200: - if 'Invalid authenticator' in response.text: - retries += 1 - return self.fetch_mobile_confirmations(retries) - - confirmation_ids = re.findall(r'data-confid="(\d+)"', response.text) - confirmation_keys = re.findall(r'data-key="(\d+)"', response.text) - confirmation_descriptions = re.findall(r'
((Confirm|Trade with|Sell -) .+)<\/div>', response.text) - - if confirmation_ids and confirmation_keys: - for index, confirmation_id in enumerate(confirmation_ids): - raw_confirmations.append({ - 'id': confirmation_id, - 'key': confirmation_keys[index], - 'description': confirmation_descriptions[index] - }) - return raw_confirmations - return [ ] - - def add_mobile_authenticator(self): - if self._verify_mobile_authenticator(): - raise MobileAuthenticatorException('The steam mobile authenticator is already enabled.') - - self._verify_mobile_session() - - deviceid = getattr(self, 'deviceid') or generate_device_id(self.steamid) - - data = { - 'steamid': self.steamid, - 'sms_phone_id': 1, - 'access_token': self.oauth_token, - 'authenticator_time': get_time_offset(), - 'authenticator_type': 1, - 'device_identifier': deviceid - } - - response = self.session.post('https://api.steampowered.com/ITwoFactorService/AddAuthenticator/v1/', - data, headers=DEFAULT_MOBILE_HEADERS) - if response.status_code == 200: - response_json = json.loads(response.text) - if response_json.get('response').get('status') == 1: - self.set_account_property('shared_secret', response_json.get('response').get('shared_secret')) - self.set_account_property('identity_secret', response_json.get('response').get('identity_secret')) - self.set_account_property('revocation_code', response_json.get('response').get('revocation_code')) - self.set_account_property('secret_1', response_json.get('response').get('secret_1')) - self.set_account_property('serial_number', response_json.get('response').get('serial_number')) - self.set_account_property('deviceid', deviceid) - return True - return False - - def finalize_mobile_authenticator(self, sms_code, retries=1): - if self._verify_mobile_authenticator(): - raise MobileAuthenticatorException('The steam mobile authenticator is already enabled.') - - self._verify_mobile_session() - - if not sms_code: - raise SMSCodeNotProvided('The sms code is required for finalizing the process of adding the mobile\ - authenticator') - - timestamp = get_time_offset() + def get_api_key(self, retrieve_if_missing=True): + if self.check_account_property('apikey'): + return self.apikey - data = { - 'steamid': self.steamid, - 'access_token': self.oauth_token, - 'authenticator_time': timestamp, - 'authenticator_code': generate_twofactor_code_for_time(self.shared_secret, timestamp), - 'activation_code': sms_code - } + elif retrieve_if_missing: + if not self._has_web_session(): + self._spawn_web_session() - response = self.session.post('https://api.steampowered.com/ITwoFactorService/FinalizeAddAuthenticator/v1/', - data, headers=DEFAULT_MOBILE_HEADERS) - - if response.status_code == 200: - response_json = json.loads(response.text) - if response_json.get('response').get('success'): - self.set_account_property('has_mobile_authenticator', True) - return True - else: - if response_json.get('response').get('success') and retries < 30: - retries += 1 - return self._finalize_mobile_authenticator(sms_code, retries) - return False + api_key = self.retrieve_api_key() + self.set_account_property('apikey', api_key) + self._spawn_web_api() + return api_key + else: + raise APIKeyException('Could not return the apikey. The apikey is not set as account property and retrieve_if_missing is not allowed.') - def remove_mobile_authenticator(self): - if not self._verify_mobile_authenticator(): - raise MobileAuthenticatorException('The steam mobile authenticator is not enabled.') + def retrieve_api_key(self): + if not self._has_web_session(): + raise APIKeyException('A web session is required to retrieve the api key.') - self._verify_mobile_session() + response = self.web_session.get('https://steamcommunity.com/dev/apikey') - data = { - 'steamid': self.steamid, - 'steamguard_scheme': 2, - 'revocation_code': self.revocation_code, - 'access_token': self.oauth_token - } + if 'Access Denied' in response.text: + raise APIKeyException('You need at least 1 game on this account to access the steam api key page.') - response = self.session.post('https://api.steampowered.com/ITwoFactorService/RemoveAuthenticator/v1/', - data, headers=DEFAULT_MOBILE_HEADERS) + else: + if 'Register for a new Steam Web API Key' in response.text: + regex_result = re.search(r'', response.text) + session_id = regex_result.group(1) - if response.status_code == 200: - response_json = json.loads(response.text) - if response_json.get('response').get('success'): - self.set_account_property('has_mobile_authenticator', False) - return True - return False + data = { + 'domain': 'localhost.com', + 'agreeToTerms': 'agreed', + 'submit': 'Register', + 'sessionid': session_id + } - def _verify_mobile_session(self): - if not isinstance(self._web_auth, steam.webauth.MobileWebAuth): - raise MobileAuthenticatorException('A mobile session is required.') + self.web_session.post('https://steamcommunity.com/dev/registerkey', data=data) + return self.retrieve_api_key() - if self._web_auth.complete: - raise MobileAuthenticatorException('The mobile session has to be logged in to steam.') + elif 'Your Steam Web API Key' in response.text: + regex_result = re.search(r'

Key: (.*)

', response.text) + api_key = regex_result.group(1) + return api_key - def _verify_mobile_authenticator(self): - if getattr(self, 'has_mobile_authenticator') and self.has_mobile_authenticator: - return True - return False + else: + raise APIKeyException('An unhandled api key page appeared, please try again.') def _setup(self): self._generate_fernet_key() self._spawn_fernet_suite() self._path = '%s/%s' % (BASE_LOCATION, self.username) - self._file = open(self._path, 'r+') if not os.path.isfile(self._path): self._create_credential_file() else: + self._file = open(self._path, 'r+', 0) credentials = self._parse_credential_file() for key, value in credentials.iteritems(): setattr(self, key, value) + if self.check_account_property('shared_secret'): + self._spawn_authenticator() + else: + self.authenticator = steam.guard.SteamAuthenticator() + + if self.check_account_property('apikey'): + self._spawn_web_api() + return True + def _create_credential_file(self): + open(self._path, 'a').close() + self._file = open(self._path, 'r+', 0) data = json.dumps({ 'username': self.username, 'password': self.password }) - text = self._fernet_suite.encrypt(data) - self._file.write(text) + token = self._fernet_suite.encrypt(data) + self._file.write(token) def _parse_credential_file(self): - - text = self._file.read() - data = json.loads(self._fernet_suite.decrypt(text)) + self._file.seek(0) + token = self._file.read() + self._file.seek(0) + data = json.loads(self._fernet_suite.decrypt(token)) return data def _update_credential_file(self): credentials = self._gather_credentials() data = json.dumps(credentials) - - text = self._fernet_suite.encrypt(data) + token = self._fernet_suite.encrypt(data) self._file.truncate() - self._file.write(text) + self._file.write(token) def _gather_credentials(self): data = { } @@ -275,25 +195,61 @@ class SteamAccount(object): def _spawn_fernet_suite(self): self._fernet_suite = Fernet(self._fernet_key) - def _spawn_web_session(self): + def _spawn_web_api(self): + self.web_api = steam.webapi.WebAPI(self.apikey) + + def _spawn_authenticator(self): + secrets = { + 'identity_secret': getattr(self, 'identity_secret'), + 'shared_secret': getattr(self, 'shared_secret'), + 'secret_1': getattr(self, 'secret_1'), + 'revocation_code': getattr(self, 'revocation_code'), + } + self.authenticator = steam.guard.SteamAuthenticator(secrets) + self._spawn_mobile_session() + self.authenticator.medium = self._mobile_auth + + def _has_session(self): + if self._has_web_session() or self._has_mobile_session(): + return True + return False + + def _has_web_session(self): + if isinstance(self._web_auth, steam.webauth.WebAuth): + return True + return False + + def _has_mobile_session(self): + if isinstance(self._mobile_auth, steam.webauth.MobileWebAuth): + return True + return False + + def _spawn_web_session(self, captcha='', email_code='', twofactor_code=''): self._web_auth = steam.webauth.WebAuth(self.username, self.password) + self._login_web_session(self._web_auth, captcha, email_code, twofactor_code) + self.web_session = self._web_auth.session + + def _spawn_mobile_session(self, captcha='', email_code='', twofactor_code=''): + self._mobile_auth = steam.webauth.MobileWebAuth(self.username, self.password) + self._login_web_session(self._mobile_auth, captcha, email_code, twofactor_code) + self.mobile_session = self._mobile_auth.session - def _spawn_mobile_session(self): - self._web_auth = steam.webauth.MobileWebAuth(self.username, self.password) + def _login_web_session(self, web_auth, captcha='', email_code='', twofactor_code=''): + if not isinstance(web_auth, steam.webauth.WebAuth) and not isinstance(web_auth, steam.webauth.MobileWebAuth): + raise WebAuthNotComplete('Please supply a valid WebAuth or MobileWebAuth session') - def _login_web_session(self, captcha='', email_code='', twofactor_code=''): try: - self._web_auth.login() + web_auth.login() except steam.webauth.CaptchaRequired: if not captcha: raise CaptchaNotProvided('The steam login captcha is required for logging in, but was not provided.') - self._web_auth.login(captcha=captcha) + web_auth.login(captcha=captcha) except steam.webauth.EmailCodeRequired: if not email_code: raise EMailCodeNotProvided('The email code is required for logging in, but was not provided.') - self._web_auth.login(email_code=email_code) + web_auth.login(email_code=email_code) except steam.webauth.TwoFactorCodeRequired: if not twofactor_code: @@ -301,16 +257,14 @@ class SteamAccount(object): twofactor_code = self.login_code except SharedSecretNotSet: raise TwoFACodeNotProvided('The twofactor code is required for logging in, but was not provided.') - self._web_auth.login(twofactor_code=twofactor_code) + web_auth.login(twofactor_code=twofactor_code) - if self._web_auth.complete: - if not getattr(self, 'steamid'): - self.set_account_property('steamid', self._web_auth.steamid) + if web_auth.complete: + if not hasattr(self, 'steamid'): + self.set_account_property('steamid', web_auth.steam_id) - if isinstance(self._web_auth, steam.webauth.MobileWebAuth) and not getattr(self, 'oauth_token'): - self.set_account_property('oauth_token', self._web_auth.oauth_token) - - self._session = self._web_auth.session + if isinstance(web_auth, steam.webauth.MobileWebAuth) and not hasattr(self, 'oauth_token'): + self.set_account_property('oauth_token', web_auth.oauth_token) else: raise WebAuthNotComplete('The web authentication could not be completed.') @@ -326,6 +280,12 @@ class IdentitySecretNotSet(SteamAccountException): class WebAuthNotComplete(SteamAccountException): pass +class WebException(SteamAccountException): + pass + +class APIKeyException(SteamAccountException): + pass + class MobileAuthenticatorException(SteamAccountException): pass diff --git a/steam/webpresence.py b/steam/webpresence.py deleted file mode 100644 index 782f5c1..0000000 --- a/steam/webpresence.py +++ /dev/null @@ -1,363 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Wrapping methods for `ISteamWebUserPresenceOAuth` and `Polling` functionality - -Example usage: - -.. code:: python - import steam.webauth - import steam.webpresence - webAuth = steam.webauth.MobileWebAuth('username', 'password') - webAuth.login() - - def my_callback(messages): - for message in messages: - print '[%s] %s from %s' % (message.timestamp, message.type, message.steamid_from.as_64) - - webPresence = steam.webpresence.WebUserPresence(webAuth) - webPresence.logon() - webPresence.start_polling(my_callback) - print 'Started polling' -""" -import threading -import requests - -from steam.webauth import MobileWebAuth -from steam import SteamID - -DEFAULT_MOBILE_HEADERS = { - 'X-Requested-With': 'com.valvesoftware.android.steam.community', - 'User-agent': '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' -} - -API_BASE = 'https://api.steampowered.com' - -DEFAULT_TIMEOUT = 30 - -PERSONA_STATES = ['Offline', 'Online', 'Busy', 'Away', 'Snooze', 'Looking to trade', 'Looking to play'] - -class WebUserPresence: - """ - Wrapping methods for `ISteamWebUserPresenceOAuth` and `Polling` functionality - """ - _loggedon = False - _timeout = None - - _oauth_token = None - _session = None - - _steamid = None - _umqid = None - _message_base = None - - def __init__(self, mobile_web_auth, timeout=None): - if not isinstance(mobile_web_auth, MobileWebAuth): - raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `MobileWebAuth`.') - - if not mobile_web_auth.complete: - raise InstanceNotReady('Please make sure your `MobileWebAuth` instance is logged in.') - - self._timeout = timeout - self._oauth_token = mobile_web_auth.oauth_token - self._session = mobile_web_auth.session - - def _request(self, uri, data, timeout=None): - """ - HTTP Request - :param uri: URI to be requested - :param data: Data to be delivered - :param timeout: HTTP timeout - :return: `requests.Response` - """ - timeout = timeout or self._timeout or DEFAULT_TIMEOUT - return self._session.post(uri, data=data, headers=DEFAULT_MOBILE_HEADERS, timeout=timeout) - - def _call(self, method, data, timeout=None): - """ - Calling an `ISteamWebUserPresenceOAuth` api method - :param method: Method to be called - :param data: Data to be delivered - :param timeout: HTTP timeout - :return: If successful, response as tuple, otherwise exceptions are thrown - """ - uri = '%s/ISteamWebUserPresenceOAuth/%s/v1/' % (API_BASE, method) - - try: - response = self._request(uri, data, timeout=timeout) - except requests.exceptions.ReadTimeout: - raise HTTPError('Timeout') - - if response.status_code == 401: - raise NotAuthorized('Not authorized. Please check your OAuth token and verify your `MobileWebAuth` login.') - elif response.status_code != 200: - raise HTTPError('HTTP request failed. Status code: %s' % response.status_code) - - if self._loggedon: - self._message_base += 1 - - try: - json_response = response.json() - except: - raise ValueError('Could not build json_response') - else: - return json_response - - def logon(self): - """ - Sends logon to the api - :return: True if successful. Raises an `LogonFailed` exception otherwise - """ - login_data = { - 'access_token': self._oauth_token - } - response = self._call('Logon', login_data) - if response.get('error') == 'OK': - self._steamid = SteamID(response.get('steamid')) - self._umqid = response.get('umqid') - self._message_base = response.get('message') - - self._loggedon = True - return True - else: - raise LogonFailed('Logon failed. Please check your OAuth token and verify your `MobileWebAuth` login.') - - def logoff(self): - """ - Sends logoff to the api - :return: True if logoff was successful, False otherwise. - """ - response = self._call('Logoff', self._build_call_data({}), True) - self._loggedon = False - return response.get('error') == 'OK' or False - - def poll(self): - """ - Starts an poll request - :return: Full response as Tuple - """ - poll_data = { - 'pollid': 0, - 'sectimeout': 5, - 'secidletime': 0, - 'use_accountids': 1 - } - response = self._call('Poll', self._build_call_data(poll_data, True), timeout=60) - if response.get('error') == 'OK': - return response - else: - raise PollCreationFailed(response.get('error')) - - def start_polling(self, callback): - """ - Creates an instance of `WebPolling` and starts the thread - :param callback: Function reference for handling `WebPollMessages` - :return: True - """ - web_polling = self._spawn_web_polling(callback) - web_polling.start() - return True - - def stop_polling(self): - """ - Stops the active `WebPolling` - :return: True, False. - """ - return self._kill_web_polling() - - def message(self, steamid, text): - """ - Delivers a steam message - :param steamid: SteamID64 of the target user - :param text: Message to deliver - :return: True if deliver was successful, False otherwise - """ - message_data = self._build_call_data({ - 'text': text, - 'type': 'saytext', - 'steamid_dst': steamid - }) - response = self._call('Message', message_data) - return response.get('error') == 'OK' or False - - def _spawn_web_polling(self, callback): - """ - Spawns a `WebPolling` instance - :param callback: A method reference for handling incoming `WebPollMessages` - :return: Instance reference - """ - self._web_polling = WebPolling(self, callback) - return self._web_polling - - def _kill_web_polling(self): - """ - Stops the run() method of WebPolling after the current poll request - :return: True if `_web_polling` is spawned and could be stopped, False otherwise - """ - if getattr(self, '_web_polling'): - self._web_polling._active = False - return True - return False - - def _prep_messages(self, messages): - """ - Prepares incoming raw `WebPolling` messages by creating `WebPollMessage` instances - :param messages: raw `WebPolling` messages - :return: List of `WebPollMessages` instances - """ - prepped_messages = [ ] - for message in messages: - prepped_messages.append(WebPollMessage(message, self)) - return prepped_messages - - def _build_call_data(self, data, set_message_base=False): - """ - Builds the data for `_call`'s - :param data: Data to deliver as Tuple - :param set_message_base: True, False. Only required for calls where `message` has to be set - :return: Prepared call data - """ - base_data = { - 'umqid': self._umqid, - 'access_token': self._oauth_token - } - if set_message_base: - base_data.__setitem__('message', self._message_base) - - for key in data: - base_data.__setitem__(key, data.get(key)) - - return base_data - -class WebPolling(threading.Thread): - """ - Threaded class for `Polling` - """ - _active = True - _web_user_presence = None - _callback = None - - def __init__(self, web_user_presence, callback): - threading.Thread.__init__(self) - - if not isinstance(web_user_presence, WebUserPresence): - raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `WebUserPresence`.') - - if not web_user_presence._loggedon: - raise InstanceNotReady('The `WebUserPresence` has to be logged on.') - - self._web_user_presence = web_user_presence - self._callback = callback - - self.setDaemon(False) - - def run(self): - """ - Sends HTTP requests while class is `_active` and calls a specified callback - :return: - """ - while self._active: - try: - response = self._web_user_presence.poll() - except PollCreationFailed: - """ - Ignore timeout exception - """ - pass - except HTTPError: - """ - Ignore http exceptions - """ - pass - else: - prepped_messages = self._web_user_presence._prep_messages(response.get('messages')) - if self._callback: - self._callback(prepped_messages) - -class WebPollMessage(object): - """ - Class for proper handling of polling messages - """ - complete = False - - _web_user_presence = None - - timestamp = 0 - type = None - steamid_from = SteamID() - - text = None - - persona_state = 0 - persona_name = None - status_flags = None - - _answerable = False - _full_data = None - - def __init__(self, message, web_user_presence): - if not isinstance(web_user_presence, WebUserPresence): - raise InvalidInstanceSupplied('The instance supplied as parameter is no valid instance of `WebUserPresence`.') - - if not web_user_presence._loggedon: - raise InstanceNotReady('The `WebUserPresence` has to be logged on.') - - self._full_data = message - - self._web_user_presence = web_user_presence - - self.timestamp = message.get('utc_timestamp') - self.type = message.get('type') - self.steamid_from = SteamID(message.get('accountid_from')) - - if self.type == 'typing': - self._answerable = True - elif self.type == 'saytext': - self._answerable = True - self.text = message.get('text') - elif self.type == 'personastate': - self.persona_name = message.get('persona_name') - self.persona_state = message.get('persona_state') - self.status_flags = message.get('status_flags') - - def answer(self, message): - """ - If the current message is `_answerable` ( current message has something to do with the steam chat ) an - answer can be sent - :param message: Message to be delivered - :return: True if message could be delivered, False otherwise - """ - if not self._answerable: - return False - self._web_user_presence.message(self.steamid_from.as_64, message) - return True - - def persona_state_to_str(self): - """ - Returns the `persona_state` as string - :return: `persona_state` as String or False if `persona_state` is not available - """ - if self.persona_state: - return PERSONA_STATES.__getitem__(self.persona_state) - return False - -class WebUserPresenceException(Exception): - pass - -class InvalidInstanceSupplied(WebUserPresenceException): - pass - -class InstanceNotReady(WebUserPresenceException): - pass - -class NotAuthorized(WebUserPresenceException): - pass - -class LogonFailed(WebUserPresenceException): - pass - -class PollCreationFailed(WebUserPresenceException): - pass - -class HTTPError(WebUserPresenceException): - pass \ No newline at end of file From fa0e88c99e8c049556afe3b51f3faa543f79ffea Mon Sep 17 00:00:00 2001 From: philippj Date: Sat, 18 Jun 2016 10:40:36 +0200 Subject: [PATCH 14/16] SteamAuthenticator.get_confirmation_key paramerter order fix #38 --- steam/guard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/steam/guard.py b/steam/guard.py index 7668719..59a6bde 100644 --- a/steam/guard.py +++ b/steam/guard.py @@ -101,8 +101,8 @@ class SteamAuthenticator(object): :return: trade confirmation key :rtype: str """ - return generate_confirmation_key(b64decode(self.identity_secret), tag, - self.get_time() if timestamp is None else timestamp) + return generate_confirmation_key(b64decode(self.identity_secret), + self.get_time() if timestamp is None else timestamp, tag) def _send_request(self, action, params): action_map = { From 05172f461bf013b16b244b0e6f434bca64843869 Mon Sep 17 00:00:00 2001 From: philippj Date: Sat, 18 Jun 2016 11:56:57 +0200 Subject: [PATCH 15/16] Revert change --- steam/guard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/steam/guard.py b/steam/guard.py index 0b8b89e..6a78c6f 100644 --- a/steam/guard.py +++ b/steam/guard.py @@ -101,8 +101,8 @@ class SteamAuthenticator(object): :return: trade confirmation key :rtype: str """ - return generate_confirmation_key(b64decode(self.identity_secret), - self.get_time() if timestamp is None else timestamp, tag) + return generate_confirmation_key(b64decode(self.identity_secret), tag, + self.get_time() if timestamp is None else timestamp) def _send_request(self, action, params): medium = self.medium From 708be84d6fe354971389f0fc460f9491729e3f1f Mon Sep 17 00:00:00 2001 From: philippj Date: Mon, 20 Jun 2016 21:49:42 +0200 Subject: [PATCH 16/16] Fixed: encryption issues, file pointer inconsistency; Added a new parameter for get_api_key, changed attributes from instance attributes to variables accessed via __getattr__, proper atexit handling --- steam/account.py | 154 +++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/steam/account.py b/steam/account.py index 9e97306..541ff6f 100644 --- a/steam/account.py +++ b/steam/account.py @@ -17,6 +17,8 @@ import sys import json import base64 import re +import atexit +from base64 import b64decode from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes @@ -43,6 +45,7 @@ class SteamAccount(object): web_api = None authenticator = None + _credentials = { } _path = None _file = None _fernet_key = None @@ -52,12 +55,29 @@ class SteamAccount(object): def __init__(self, username, password): + atexit.register(self.__cleanup__) self.username = username self.password = password if not self._setup(): raise SteamAccountException('Could not access account.') + def __getattr__(self, key): + if key not in self._credentials.keys(): + raise AttributeError("No %s attribute" % repr(key)) + return self._credentials.get(key) + def __del__(self): + self.__save_account_credentials__() + + def __cleanup__(self): + self.__save_account_credentials__() + + def __save_account_credentials__(self): + """ + Make sure it doesnt write the file if the memory has been cleared + """ + if self._count_account_credentials() <= 2: + return try: self._update_credential_file() except TypeError: @@ -65,29 +85,33 @@ class SteamAccount(object): Ignore TypeError exception when destructor gets called after the memory has been cleared """ pass - self._file.close() + except ValueError: + """ + Ignore ValueError exception when the file could not be written + """ + pass def set_account_property(self, property, value): - setattr(self, property, value) + self._credentials[property] = value self._update_credential_file() def del_account_property(self, property): - delattr(self, property) + del self._credentials[property] self._update_credential_file() def check_account_property(self, property): - return hasattr(self, property) + return property in self._credentials @property def login_code(self): - if self.authenticator: + if self.authenticator and hasattr(self.authenticator, 'shared_secret'): return self.authenticator.get_code() elif hasattr(self, 'shared_secret'): - return steam.guard.generate_twofactor_code(self.shared_secret) + return steam.guard.generate_twofactor_code(b64decode(self.shared_secret)) else: raise SharedSecretNotSet('Add shared_secret to this instance to generate login codes') - def get_api_key(self, retrieve_if_missing=True): + def get_api_key(self, retrieve_if_missing=False, hostname_for_retrieving='localhost.com'): if self.check_account_property('apikey'): return self.apikey @@ -95,14 +119,14 @@ class SteamAccount(object): if not self._has_web_session(): self._spawn_web_session() - api_key = self.retrieve_api_key() + api_key = self.retrieve_api_key(hostname_for_retrieving) self.set_account_property('apikey', api_key) self._spawn_web_api() return api_key else: raise APIKeyException('Could not return the apikey. The apikey is not set as account property and retrieve_if_missing is not allowed.') - def retrieve_api_key(self): + def retrieve_api_key(self, hostname_for_retrieving='localhost.com'): if not self._has_web_session(): raise APIKeyException('A web session is required to retrieve the api key.') @@ -117,7 +141,7 @@ class SteamAccount(object): session_id = regex_result.group(1) data = { - 'domain': 'localhost.com', + 'domain': hostname_for_retrieving, 'agreeToTerms': 'agreed', 'submit': 'Register', 'sessionid': session_id @@ -138,16 +162,16 @@ class SteamAccount(object): self._generate_fernet_key() self._spawn_fernet_suite() self._path = '%s/%s' % (BASE_LOCATION, self.username) - if not os.path.isfile(self._path): + if not os.path.exists(self._path) or not os.path.isfile(self._path): self._create_credential_file() else: - self._file = open(self._path, 'r+', 0) credentials = self._parse_credential_file() for key, value in credentials.iteritems(): - setattr(self, key, value) + self._credentials[key] = value if self.check_account_property('shared_secret'): self._spawn_authenticator() + else: self.authenticator = steam.guard.SteamAuthenticator() @@ -156,37 +180,52 @@ class SteamAccount(object): return True def _create_credential_file(self): - open(self._path, 'a').close() - self._file = open(self._path, 'r+', 0) + open(self._path, 'w+').close() + self._spawn_file_pointer() + data = json.dumps({ 'username': self.username, 'password': self.password }) + token = self._fernet_suite.encrypt(data) self._file.write(token) + self._file.close() def _parse_credential_file(self): - self._file.seek(0) + self._spawn_file_pointer() + token = self._file.read() - self._file.seek(0) data = json.loads(self._fernet_suite.decrypt(token)) + self._file.close() return data def _update_credential_file(self): + self._spawn_file_pointer() + credentials = self._gather_credentials() data = json.dumps(credentials) token = self._fernet_suite.encrypt(data) + self._file.truncate() self._file.write(token) + self._file.close() def _gather_credentials(self): data = { } - names = dir(self) + names = self._credentials for name in names: if name in ACCOUNT_ATTRIBUTES: - data.__setitem__(name, getattr(self, name)) + data.__setitem__(name, self._credentials[name]) return data + def _count_account_credentials(self): + count = 0 + for attr in ACCOUNT_ATTRIBUTES: + if self.check_account_property(attr): + count += 1 + return count + def _generate_fernet_key(self): digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) digest.update(bytes(self.password)) @@ -195,75 +234,59 @@ class SteamAccount(object): def _spawn_fernet_suite(self): self._fernet_suite = Fernet(self._fernet_key) + def _spawn_file_pointer(self): + self._file = open(self._path, 'r+', 0) + def _spawn_web_api(self): self.web_api = steam.webapi.WebAPI(self.apikey) def _spawn_authenticator(self): secrets = { - 'identity_secret': getattr(self, 'identity_secret'), - 'shared_secret': getattr(self, 'shared_secret'), - 'secret_1': getattr(self, 'secret_1'), - 'revocation_code': getattr(self, 'revocation_code'), - } + 'identity_secret': self._credentials.get('identity_secret'), + 'shared_secret': self._credentials.get('shared_secret'), + 'secret_1': self._credentials.get('secret_1'), + 'revocation_code': self._credentials.get('revocation_code'), + 'deviceid': self._credentials.get('deviceid') + } self.authenticator = steam.guard.SteamAuthenticator(secrets) self._spawn_mobile_session() self.authenticator.medium = self._mobile_auth def _has_session(self): - if self._has_web_session() or self._has_mobile_session(): - return True - return False + return True if self._has_web_session() or self._has_mobile_session() else False def _has_web_session(self): - if isinstance(self._web_auth, steam.webauth.WebAuth): - return True - return False + return isinstance(self._web_auth, steam.webauth.WebAuth) def _has_mobile_session(self): - if isinstance(self._mobile_auth, steam.webauth.MobileWebAuth): - return True - return False + return isinstance(self._mobile_auth, steam.webauth.MobileWebAuth) - def _spawn_web_session(self, captcha='', email_code='', twofactor_code=''): + def _spawn_web_session(self): self._web_auth = steam.webauth.WebAuth(self.username, self.password) - self._login_web_session(self._web_auth, captcha, email_code, twofactor_code) + self._login_web_session(self._web_auth) self.web_session = self._web_auth.session - def _spawn_mobile_session(self, captcha='', email_code='', twofactor_code=''): + def _spawn_mobile_session(self): self._mobile_auth = steam.webauth.MobileWebAuth(self.username, self.password) - self._login_web_session(self._mobile_auth, captcha, email_code, twofactor_code) + self._login_web_session(self._mobile_auth) self.mobile_session = self._mobile_auth.session - def _login_web_session(self, web_auth, captcha='', email_code='', twofactor_code=''): + def _login_web_session(self, web_auth): if not isinstance(web_auth, steam.webauth.WebAuth) and not isinstance(web_auth, steam.webauth.MobileWebAuth): raise WebAuthNotComplete('Please supply a valid WebAuth or MobileWebAuth session') try: - web_auth.login() - - except steam.webauth.CaptchaRequired: - if not captcha: - raise CaptchaNotProvided('The steam login captcha is required for logging in, but was not provided.') - web_auth.login(captcha=captcha) - - except steam.webauth.EmailCodeRequired: - if not email_code: - raise EMailCodeNotProvided('The email code is required for logging in, but was not provided.') - web_auth.login(email_code=email_code) - - except steam.webauth.TwoFactorCodeRequired: - if not twofactor_code: - try: - twofactor_code = self.login_code - except SharedSecretNotSet: - raise TwoFACodeNotProvided('The twofactor code is required for logging in, but was not provided.') - web_auth.login(twofactor_code=twofactor_code) + twofactor_code = self.login_code + except SharedSecretNotSet: + twofactor_code = '' + + web_auth.login(twofactor_code=twofactor_code) if web_auth.complete: if not hasattr(self, 'steamid'): self.set_account_property('steamid', web_auth.steam_id) - if isinstance(web_auth, steam.webauth.MobileWebAuth) and not hasattr(self, 'oauth_token'): + if isinstance(web_auth, steam.webauth.MobileWebAuth): self.set_account_property('oauth_token', web_auth.oauth_token) else: raise WebAuthNotComplete('The web authentication could not be completed.') @@ -286,20 +309,5 @@ class WebException(SteamAccountException): class APIKeyException(SteamAccountException): pass -class MobileAuthenticatorException(SteamAccountException): - pass - class ParameterNotProvidedException(SteamAccountException): - pass - -class CaptchaNotProvided(ParameterNotProvidedException): - pass - -class EMailCodeNotProvided(ParameterNotProvidedException): - pass - -class TwoFACodeNotProvided(ParameterNotProvidedException): - pass - -class SMSCodeNotProvided(ParameterNotProvidedException): pass \ No newline at end of file