diff --git a/pgoapi/auth.py b/pgoapi/auth.py index 38fe30e8..7b8a74c8 100755 --- a/pgoapi/auth.py +++ b/pgoapi/auth.py @@ -29,8 +29,8 @@ from pgoapi.utilities import get_time, get_format_time_diff -class Auth: +class Auth: def __init__(self): self.log = logging.getLogger(__name__) @@ -38,21 +38,19 @@ def __init__(self): self._login = False - """ - oauth2 uses refresh tokens (which basically never expires) + """ + oauth2 uses refresh tokens (which basically never expires) to get an access_token which is only valid for a certain time) """ self._refresh_token = None self._access_token = None self._access_token_expiry = 0 - # TODO: can be removed - self._auth_token = None - """ - Pokemon Go uses internal tickets, like an internal + """ + Pokemon Go uses internal tickets, like an internal session to keep a user logged in over a certain time (30 minutes) """ - self._ticket_expire = None + self._ticket_expire = 0 self._ticket_start = None self._ticket_end = None @@ -66,39 +64,34 @@ def get_token(self): return self._access_token def has_ticket(self): - if self._ticket_expire and self._ticket_start and self._ticket_end: - return True - else: - return False + return (self._ticket_expire and self._ticket_start and self._ticket_end) def set_ticket(self, params): self._ticket_expire, self._ticket_start, self._ticket_end = params def is_new_ticket(self, new_ticket_time_ms): - if self._ticket_expire is None or new_ticket_time_ms > self._ticket_expire: - return True - else: - return False + return (not self._ticket_expire or new_ticket_time_ms > self._ticket_expire) def check_ticket(self): - if self.has_ticket(): - now_ms = get_time(ms = True) - if now_ms < (self._ticket_expire - 10000): - h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) - self.log.debug('Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) - return True - else: - self.log.debug('Removed expired Session Ticket (%s < %s)', now_ms, self._ticket_expire) - self._ticket_expire, self._ticket_start, self._ticket_end = (None, None, None) - return False - else: + if not self.has_ticket(): return False + now_ms = get_time(ms=True) + if now_ms < (self._ticket_expire + 10000): + h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) + self.log.debug( + 'Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) + return True + + self.log.debug('Removed expired Session Ticket (%s < %s)', + now_ms, self._ticket_expire) + self._ticket_expire, self._ticket_start, self._ticket_end = ( + 0, None, None) + return False def get_ticket(self): if self.check_ticket(): return (self._ticket_expire, self._ticket_start, self._ticket_end) - else: - return False + return False def user_login(self, username, password): raise NotImplementedError() @@ -106,28 +99,26 @@ def user_login(self, username, password): def set_refresh_token(self, username, password): raise NotImplementedError() - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): raise NotImplementedError() - def check_access_token(self): - """ - Add few seconds to now so the token get refreshed - before it invalidates in the middle of the request - """ - now_s = get_time() + 120 - - if self._access_token is not None: - if self._access_token_expiry == 0: - self.log.debug('No Access Token Expiry found - assuming it is still valid!') - return True - elif self._access_token_expiry > now_s: - h, m, s = get_format_time_diff(now_s, self._access_token_expiry, False) - self.log.debug('Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_s, self._access_token_expiry) - return True - else: - self.log.info('Access Token expired!') - return False - else: + if self._access_token is None: self.log.debug('No Access Token available!') - return False \ No newline at end of file + return False + + now_s = get_time() + if self._access_token_expiry == 0: + self.log.debug( + 'No Access Token Expiry found - assuming it is still valid!') + return True + elif self._access_token_expiry > now_s: + h, m, s = get_format_time_diff( + now_s, self._access_token_expiry, False) + self.log.debug( + 'Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_s, self._access_token_expiry) + return True + + self.log.info('Access Token expired!') + return False diff --git a/pgoapi/auth_google.py b/pgoapi/auth_google.py index dab31f5d..45d4b512 100755 --- a/pgoapi/auth_google.py +++ b/pgoapi/auth_google.py @@ -32,10 +32,11 @@ from gpsoauth import perform_master_login, perform_oauth from six import string_types + class AuthGoogle(Auth): GOOGLE_LOGIN_ANDROID_ID = '9774d56d682e549c' - GOOGLE_LOGIN_SERVICE= 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' + GOOGLE_LOGIN_SERVICE = 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' GOOGLE_LOGIN_APP = 'com.nianticlabs.pokemongo' GOOGLE_LOGIN_CLIENT_SIG = '321187995bc7cdc2b5fc91b11a96e2baa8602c62' @@ -52,13 +53,20 @@ def set_proxy(self, proxy_config): def user_login(self, username, password): self.log.info('Google User Login for: {}'.format(username)) - if not isinstance(username, string_types) or not isinstance(password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(username, string_types) or not isinstance( + password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") - user_login = perform_master_login(username, password, self.GOOGLE_LOGIN_ANDROID_ID, proxy=self._proxy) + user_login = perform_master_login( + username, + password, + self.GOOGLE_LOGIN_ANDROID_ID, + proxy=self._proxy) if user_login and user_login.get('Error', None) == 'NeedsBrowser': - raise AuthGoogleTwoFactorRequiredException(user_login['Url'], user_login['ErrorDetail']) + raise AuthGoogleTwoFactorRequiredException( + user_login['Url'], user_login['ErrorDetail']) try: refresh_token = user_login.get('Token', None) @@ -79,7 +87,7 @@ def set_refresh_token(self, refresh_token): self.log.info('Google Refresh Token provided by user') self._refresh_token = refresh_token - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): token_validity = self.check_access_token() if token_validity is True and force_refresh is False: @@ -91,8 +99,14 @@ def get_access_token(self, force_refresh = False): else: self.log.info('Request Google Access Token...') - token_data = perform_oauth(None, self._refresh_token, self.GOOGLE_LOGIN_ANDROID_ID, self.GOOGLE_LOGIN_SERVICE, self.GOOGLE_LOGIN_APP, - self.GOOGLE_LOGIN_CLIENT_SIG, proxy=self._proxy) + token_data = perform_oauth( + None, + self._refresh_token, + self.GOOGLE_LOGIN_ANDROID_ID, + self.GOOGLE_LOGIN_SERVICE, + self.GOOGLE_LOGIN_APP, + self.GOOGLE_LOGIN_CLIENT_SIG, + proxy=self._proxy) access_token = token_data.get('Auth', None) if access_token is not None: @@ -101,7 +115,8 @@ def get_access_token(self, force_refresh = False): self._login = True self.log.info('Google Access Token successfully received.') - self.log.debug('Google Access Token: %s...', self._access_token[:25]) + self.log.debug('Google Access Token: %s...', + self._access_token[:25]) return self._access_token else: self._access_token = None diff --git a/pgoapi/auth_ptc.py b/pgoapi/auth_ptc.py index afae6c87..8c9240e4 100755 --- a/pgoapi/auth_ptc.py +++ b/pgoapi/auth_ptc.py @@ -36,137 +36,169 @@ from pgoapi.utilities import get_time from pgoapi.exceptions import AuthException, AuthTimeoutException, InvalidCredentialsException -from requests.exceptions import RequestException, Timeout +from requests.exceptions import RequestException, Timeout, ProxyError, SSLError, ConnectionError -class AuthPtc(Auth): - PTC_LOGIN_URL1 = 'https://sso.pokemon.com/sso/oauth2.0/authorize?client_id=mobile-app_pokemon-go&redirect_uri=https%3A%2F%2Fwww.nianticlabs.com%2Fpokemongo%2Ferror' - PTC_LOGIN_URL2 = 'https://sso.pokemon.com/sso/login?service=http%3A%2F%2Fsso.pokemon.com%2Fsso%2Foauth2.0%2FcallbackAuthorize' - PTC_LOGIN_OAUTH = 'https://sso.pokemon.com/sso/oauth2.0/accessToken' - PTC_LOGIN_CLIENT_SECRET = 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR' +class AuthPtc(Auth): - def __init__(self, username=None, password=None, user_agent=None, timeout=None): + def __init__(self, + username=None, + password=None, + user_agent=None, + timeout=None, + locale=None): Auth.__init__(self) self._auth_provider = 'ptc' - - self._session = requests.session() - self._session.headers = {'User-Agent': user_agent or 'pokemongo/1 CFNetwork/811.4.18 Darwin/16.5.0', 'Host': 'sso.pokemon.com', 'X-Unity-Version': '2017.1.2f1'} self._username = username self._password = password - self.timeout = timeout or 15 + self.timeout = timeout or 10 + self.locale = locale or 'en_US' + self.user_agent = user_agent or 'pokemongo/0 CFNetwork/893.14.2 Darwin/17.3.0' + + self._session = requests.session() + self._session.headers = { + 'Host': 'sso.pokemon.com', + 'Accept': '*/*', + 'Connection': 'keep-alive', + 'User-Agent': self.user_agent, + 'Accept-Language': self.locale.lower().replace('_', '-'), + 'Accept-Encoding': 'br, gzip, deflate', + 'X-Unity-Version': '2017.1.2f1' + } def set_proxy(self, proxy_config): self._session.proxies = proxy_config - def user_login(self, username=None, password=None, retry=True): + def user_login(self, username=None, password=None): self._username = username or self._username self._password = password or self._password - if not isinstance(self._username, string_types) or not isinstance(self._password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(self._username, string_types) or not isinstance( + self._password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") self.log.info('PTC User Login for: {}'.format(self._username)) self._session.cookies.clear() - now = get_time() try: - r = self._session.get(self.PTC_LOGIN_URL1, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth GET timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) + now = get_time() - try: - data = r.json() + logout_params = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize' + } + r = self._session.get( + 'https://sso.pokemon.com/sso/logout', + params=logout_params, + timeout=self.timeout, + allow_redirects=False) + r.close() + + login_params_get = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + r = self._session.get( + 'https://sso.pokemon.com/sso/login', + params=login_params_get, + timeout=self.timeout) + + data = r.json(encoding='utf-8') + + assert 'lt' in data data.update({ '_eventId': 'submit', 'username': self._username, - 'password': self._password, + 'password': self._password }) - except (ValueError, AttributeError) as e: - self.log.error('PTC User Login Error - invalid JSON response: {}'.format(e)) - raise AuthException('Invalid JSON response: {}'.format(e)) - try: - r = self._session.post(self.PTC_LOGIN_URL2, data=data, timeout=self.timeout, allow_redirects=False) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') + login_params_post = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + login_headers_post = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/login', + params=login_params_post, + headers=login_headers_post, + data=data, + timeout=self.timeout, + allow_redirects=False) + + try: + self._access_token = self._session.cookies['CASTGC'] + except (AttributeError, KeyError, TypeError): + try: + j = r.json(encoding='utf-8') + except ValueError as e: + raise AuthException('Unable to decode second response: {}'.format(e)) + try: + if j.get('error_code') == 'users.login.activation_required': + raise AuthException('Account email not verified.') + raise AuthException(j['errors'][0]) + except (AttributeError, IndexError, KeyError, TypeError) as e: + raise AuthException('Unable to login or get error information: {}'.format(e)) + + token_data = { + 'client_id': 'mobile-app_pokemon-go', + 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', + 'client_secret': 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR', + 'grant_type': 'refresh_token', + 'code': r.headers['Location'].split("ticket=")[1] + } + token_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/oauth2.0/accessToken', + headers=token_headers, + data=token_data, + timeout=self.timeout) + r.close() + + profile_data = { + 'access_token': self._access_token, + 'client_id': 'mobile-app_pokemon-go', + 'locale': self.locale + } + profile_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/oauth2.0/profile', + headers=profile_headers, + data=profile_data, + timeout=self.timeout) + r.close() + + except (ProxyError, SSLError, ConnectionError) as e: + raise AuthException('Proxy connection error during user_login: {}'.format(e)) + except Timeout as e: + raise AuthTimeoutException('user_login timeout') except RequestException as e: raise AuthException('Caught RequestException: {}'.format(e)) + except (AssertionError, TypeError, ValueError) as e: + raise AuthException('Invalid initial JSON response: {}'.format(e)) - try: - qs = parse_qs(urlsplit(r.headers['Location'])[3]) - self._refresh_token = qs.get('ticket')[0] - except Exception as e: - raise AuthException('Could not retrieve token! {}'.format(e)) - - self._access_token = self._session.cookies.get('CASTGC') if self._access_token: self._login = True - self._access_token_expiry = int(now) + 7200 + self._access_token_expiry = now + 7195.0 self.log.info('PTC User Login successful.') - elif self._refresh_token and retry: - self.get_access_token() - else: - self._login = False - raise AuthException("Could not retrieve a PTC Access Token") - return self._login + return self._login - def set_refresh_token(self, refresh_token): - self.log.info('PTC Refresh Token provided by user') - self._refresh_token = refresh_token + self._login = False + raise AuthException("Could not retrieve a PTC Access Token") - def get_access_token(self, force_refresh=False): - token_validity = self.check_access_token() - if token_validity is True and force_refresh is False: + def get_access_token(self, force_refresh=False): + if not force_refresh and self.check_access_token(): self.log.debug('Using cached PTC Access Token') return self._access_token - else: - if force_refresh: - self.log.info('Forced request of PTC Access Token!') - else: - self.log.info('Request PTC Access Token...') - data = { - 'client_id': 'mobile-app_pokemon-go', - 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', - 'client_secret': self.PTC_LOGIN_CLIENT_SECRET, - 'grant_type': 'refresh_token', - 'code': self._refresh_token, - } - - try: - r = self._session.post(self.PTC_LOGIN_OAUTH, data=data, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) - - token_data = parse_qs(r.text) - - access_token = token_data.get('access_token') - if access_token is not None: - self._access_token = access_token[0] - - # set expiration to an hour less than value received because Pokemon OAuth - # login servers return an access token with an explicit expiry time of - # three hours, however, the token stops being valid after two hours. - # See issue #86 - expires = int(token_data.get('expires', [0])[0]) - 3600 - if expires > 0: - self._access_token_expiry = expires + get_time() - else: - self._access_token_expiry = 0 - - self._login = True - - self.log.info('PTC Access Token successfully retrieved.') - self.log.debug('PTC Access Token: {}'.format(self._access_token)) - else: - self._access_token = None - self._login = False - if force_refresh: - self.log.info('Reauthenticating with refresh token failed, using credentials instead.') - return self.user_login(retry=False) - raise AuthException("Could not retrieve a PTC Access Token") + self._access_token = None + self._ticket_expire = 0 + self._login = False + self.user_login() + return self._access_token diff --git a/pgoapi/exceptions.py b/pgoapi/exceptions.py index af432a65..8b0edee2 100755 --- a/pgoapi/exceptions.py +++ b/pgoapi/exceptions.py @@ -27,9 +27,11 @@ class PgoapiError(Exception): """Any custom exception in this module""" + class HashServerException(PgoapiError): """Parent class of all hashing server errors""" + class TimeoutException(PgoapiError): """Raised when a request times out.""" @@ -37,9 +39,11 @@ class TimeoutException(PgoapiError): class AuthException(PgoapiError): """Raised when logging in fails""" + class AuthTimeoutException(AuthException, TimeoutException): """Raised when an auth request times out.""" + class InvalidCredentialsException(AuthException, ValueError): """Raised when the username, password, or provider are empty/invalid""" @@ -47,6 +51,7 @@ class InvalidCredentialsException(AuthException, ValueError): class AuthTokenExpiredException(PgoapiError): """Raised when your auth token has expired (code 102)""" + class AuthGoogleTwoFactorRequiredException(Exception): def __init__(self, redirectUrl, message): self.redirectUrl = redirectUrl @@ -59,6 +64,7 @@ def __str__(self): class BadRequestException(PgoapiError): """Raised when HTTP code 400 is returned""" + class BadHashRequestException(BadRequestException): """Raised when hashing server returns code 400""" @@ -70,10 +76,13 @@ class BannedAccountException(PgoapiError): class MalformedResponseException(PgoapiError): """Raised when the response is empty or not in an expected format""" + class MalformedNianticResponseException(PgoapiError): """Raised when a Niantic response is empty or not in an expected format""" -class MalformedHashResponseException(MalformedResponseException, HashServerException): + +class MalformedHashResponseException(MalformedResponseException, + HashServerException): """Raised when the response from the hash server cannot be parsed.""" @@ -88,15 +97,20 @@ class NotLoggedInException(PgoapiError): class ServerBusyOrOfflineException(PgoapiError): """Raised when unable to establish a connection with a server""" + class NianticOfflineException(ServerBusyOrOfflineException): """Raised when unable to establish a conection with Niantic""" + class NianticTimeoutException(NianticOfflineException, TimeoutException): """Raised when an RPC request times out.""" -class HashingOfflineException(ServerBusyOrOfflineException, HashServerException): + +class HashingOfflineException(ServerBusyOrOfflineException, + HashServerException): """Raised when unable to establish a conection with the hashing server""" + class HashingTimeoutException(HashingOfflineException, TimeoutException): """Raised when a request to the hashing server times out.""" @@ -112,12 +126,16 @@ class PleaseInstallProtobufVersion3(PgoapiError): class ServerSideAccessForbiddenException(PgoapiError): """Raised when access to a server is forbidden""" + class NianticIPBannedException(ServerSideAccessForbiddenException): """Raised when Niantic returns a 403, meaning your IP is probably banned""" -class HashingForbiddenException(ServerSideAccessForbiddenException, HashServerException): + +class HashingForbiddenException(ServerSideAccessForbiddenException, + HashServerException): """Raised when the hashing server returns 403""" + class TempHashingBanException(HashingForbiddenException): """Raised when your IP is temporarily banned for sending too many requests with invalid keys.""" @@ -125,22 +143,28 @@ class TempHashingBanException(HashingForbiddenException): class ServerSideRequestThrottlingException(PgoapiError): """Raised when too many requests were made in a short period""" + class NianticThrottlingException(ServerSideRequestThrottlingException): """Raised when too many requests to Niantic were made in a short period""" -class HashingQuotaExceededException(ServerSideRequestThrottlingException, HashServerException): + +class HashingQuotaExceededException(ServerSideRequestThrottlingException, + HashServerException): """Raised when you exceed your hashing server quota""" class UnexpectedResponseException(PgoapiError): """Raised when an unhandled HTTP status code is received""" -class UnexpectedHashResponseException(UnexpectedResponseException, HashServerException): + +class UnexpectedHashResponseException(UnexpectedResponseException, + HashServerException): """Raised when an unhandled HTTP code is received from the hash server""" class ServerApiEndpointRedirectException(PgoapiError): """Raised when the API redirects you to another endpoint""" + def __init__(self): self._api_endpoint = None diff --git a/pgoapi/hash_engine.py b/pgoapi/hash_engine.py index e68e8881..88994d0f 100644 --- a/pgoapi/hash_engine.py +++ b/pgoapi/hash_engine.py @@ -4,12 +4,15 @@ def __init__(self): self.location_auth_hash = None self.request_hashes = [] - def hash(self, timestamp, latitude, longitude, altitude, authticket, sessiondata, requests): + def hash(self, timestamp, latitude, longitude, altitude, authticket, + sessiondata, requests): raise NotImplementedError() def get_location_hash(self): return self.location_hash + def get_location_auth_hash(self): return self.location_auth_hash + def get_request_hashes(self): return self.request_hashes diff --git a/pgoapi/hash_server.py b/pgoapi/hash_server.py index 9bafca41..dd4c91c1 100644 --- a/pgoapi/hash_server.py +++ b/pgoapi/hash_server.py @@ -9,51 +9,72 @@ from pgoapi.hash_engine import HashEngine from pgoapi.exceptions import BadHashRequestException, HashingOfflineException, HashingQuotaExceededException, HashingTimeoutException, MalformedHashResponseException, NoHashKeyException, TempHashingBanException, UnexpectedHashResponseException + class HashServer(HashEngine): _session = requests.session() _adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) _session.mount('https://', _adapter) _session.verify = True _session.headers.update({'User-Agent': 'Python pgoapi @pogodev'}) - endpoint = "https://pokehash.buddyauth.com/api/v157_5/hash" + endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' status = {} def __init__(self, auth_token): if not auth_token: raise NoHashKeyException('Token not provided for hashing server.') - self.headers = {'content-type': 'application/json', 'Accept' : 'application/json', 'X-AuthToken' : auth_token} + self.headers = { + 'content-type': 'application/json', + 'Accept': 'application/json', + 'X-AuthToken': auth_token + } - def hash(self, timestamp, latitude, longitude, accuracy, authticket, sessiondata, requestslist): + def hash(self, timestamp, latitude, longitude, accuracy, authticket, + sessiondata, requestslist): self.location_hash = None self.location_auth_hash = None self.request_hashes = [] payload = { - 'Timestamp': timestamp, - 'Latitude64': unpack('>31))<<32)|cnt - self.log.debug("Incremented RPC Request ID: %s", reqid) - - return reqid - def decode_raw(self, raw): output = error = None try: - process = subprocess.Popen(['protoc', '--decode_raw'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + process = subprocess.Popen( + ['protoc', '--decode_raw'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) output, error = process.communicate(raw) except (subprocess.SubprocessError, OSError): output = "Couldn't find protoc in your environment OR other issue..." @@ -116,7 +100,8 @@ def _make_rpc(self, endpoint, request_proto_plain): request_proto_serialized = request_proto_plain.SerializeToString() try: - http_response = self._session.post(endpoint, data=request_proto_serialized, timeout=30) + http_response = self._session.post( + endpoint, data=request_proto_serialized, timeout=30) except requests.exceptions.Timeout: raise NianticTimeoutException('RPC request timed out.') except requests.exceptions.ConnectionError as e: @@ -124,33 +109,44 @@ def _make_rpc(self, endpoint, request_proto_plain): return http_response - def request(self, endpoint, subrequests, platforms, player_position, use_dict = True): + def request(self, + endpoint, + subrequests, + platforms, + player_position, + use_dict=True): if not self._auth_provider or self._auth_provider.is_login() is False: raise NotLoggedInException() - self.request_proto = self.request_proto or self._build_main_request(subrequests, platforms, player_position) + self.request_proto = self.request_proto or self._build_main_request( + subrequests, platforms, player_position) response = self._make_rpc(endpoint, self.request_proto) - response_dict = self._parse_main_response(response, subrequests, use_dict) + response_dict = self._parse_main_response(response, subrequests, + use_dict) # some response validations if isinstance(response_dict, dict): if use_dict: status_code = response_dict.get('status_code') - if ('auth_ticket' in response_dict) and ('expire_timestamp_ms' in response_dict['auth_ticket']): + if ('auth_ticket' in response_dict) and ( + 'expire_timestamp_ms' in response_dict['auth_ticket']): ticket = response_dict['auth_ticket'] - self.check_authentication(ticket['expire_timestamp_ms'], ticket['start'], ticket['end']) + self.check_authentication(ticket['expire_timestamp_ms'], + ticket['start'], ticket['end']) else: status_code = response_dict['envelope'].status_code ticket = response_dict['envelope'].auth_ticket if ticket: - self.check_authentication(ticket.expire_timestamp_ms, ticket.start, ticket.end) - + self.check_authentication(ticket.expire_timestamp_ms, + ticket.start, ticket.end) + if status_code == 102: raise AuthTokenExpiredException elif status_code == 52: - raise NianticThrottlingException("Request throttled by server... slow down man") + raise NianticThrottlingException( + "Request throttled by server... slow down man") elif status_code == 53: api_url = response_dict.get('api_url') if api_url: @@ -172,17 +168,31 @@ def check_authentication(self, expire_timestamp_ms, start, end): h, m, s = get_format_time_diff(now_ms, expire_timestamp_ms, True) if had_ticket: - self.log.debug('Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) + self.log.debug( + 'Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) else: - self.log.debug('Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) + self.log.debug( + 'Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) - def _build_main_request(self, subrequests, platforms, player_position=None): + def _build_main_request(self, subrequests, platforms, + player_position=None): self.log.debug('Generating main RPC request...') request = RequestEnvelope() request.status_code = 2 - request.request_id = self.get_rpc_id() - request.accuracy = random.choice((5, 5, 5, 5, 10, 10, 10, 30, 30, 50, 65, random.uniform(66,80))) + request.request_id = self.request_id + # 5: 43%, 10: 30%, 30: 5%, 50: 4%, 65: 10%, 200: 1%, float: 7% + request.accuracy = weighted_choice([ + (5, 43), + (10, 30), + (30, 5), + (50, 4), + (65, 10), + (200, 1), + (random.uniform(65, 200), 7) + ]) if player_position: request.latitude, request.longitude, altitude = player_position @@ -193,26 +203,31 @@ def _build_main_request(self, subrequests, platforms, player_position=None): ticket = self._auth_provider.get_ticket() if ticket: - self.log.debug('Found Session Ticket - using this instead of oauth token') + self.log.debug( + 'Found Session Ticket - using this instead of oauth token') request.auth_ticket.expire_timestamp_ms, request.auth_ticket.start, request.auth_ticket.end = ticket ticket_serialized = request.auth_ticket.SerializeToString() else: - self.log.debug('No Session Ticket found - using OAUTH Access Token') - request.auth_info.provider = self._auth_provider.get_name() - request.auth_info.token.contents = self._auth_provider.get_access_token() + self.log.debug( + 'No Session Ticket found - using OAUTH Access Token') + auth_provider = self._auth_provider + request.auth_info.provider = auth_provider.get_name() + request.auth_info.token.contents = auth_provider.get_access_token() request.auth_info.token.unknown2 = self.token2 - ticket_serialized = request.auth_info.SerializeToString() #Sig uses this when no auth_ticket available + # Sig uses this when no auth_ticket available. + ticket_serialized = request.auth_info.SerializeToString() sig = Signature() - sig.session_hash = self.session_hash + sig.session_hash = self.state.session_hash sig.timestamp = get_time(ms=True) - sig.timestamp_since_start = get_time(ms=True) - RpcApi.START_TIME - if sig.timestamp_since_start < 5000: - sig.timestamp_since_start = random.randint(5000, 8000) + sig.timestamp_since_start = get_time(ms=True) - self.start_time - self._hash_engine.hash(sig.timestamp, request.latitude, request.longitude, request.accuracy, ticket_serialized, sig.session_hash, request.requests) + self._hash_engine.hash(sig.timestamp, request.latitude, + request.longitude, request.accuracy, + ticket_serialized, sig.session_hash, + request.requests) sig.location_hash1 = self._hash_engine.get_location_auth_hash() sig.location_hash2 = self._hash_engine.get_location_hash() for req_hash in self._hash_engine.get_request_hashes(): @@ -221,52 +236,91 @@ def _build_main_request(self, subrequests, platforms, player_position=None): loc = sig.location_fix.add() sen = sig.sensor_info.add() - sen.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 100) - loc.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 1000) + sen.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(93, 4900, 3000)) + loc.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(320, 3000, 1000)) - loc.provider = random.choice(('network', 'network', 'network', 'network', 'fused')) + loc.provider = 'fused' loc.latitude = request.latitude loc.longitude = request.longitude - loc.altitude = altitude or random.triangular(300, 400, 350) + loc.altitude = altitude or random.uniform(150, 250) - if random.random() > .95: - # no reading for roughly 1 in 20 updates + if random.random() > .85: + # no reading for roughly 1 in 7 updates loc.course = -1 loc.speed = -1 else: - self.course = random.triangular(0, 360, self.course) - loc.course = self.course - loc.speed = random.triangular(0.2, 4.25, 1) + loc.course = self.state.course + loc.speed = random.triangular(0.25, 9.7, 8.2) loc.provider_status = 3 loc.location_type = 1 - if request.accuracy >= 65: - loc.vertical_accuracy = random.triangular(35, 100, 65) - loc.horizontal_accuracy = random.choice((request.accuracy, 65, 65, random.uniform(66,80), 200)) + if isinstance(request.accuracy, float): + loc.horizontal_accuracy = weighted_choice([ + (request.accuracy, 50), + (65, 40), + (200, 10) + ]) + loc.vertical_accuracy = weighted_choice([ + (random.uniform(10, 96), 50), + (10, 34), + (12, 5), + (16, 3), + (24, 4), + (32, 2), + (48, 1), + (96, 1) + ]) else: - if request.accuracy > 10: - loc.vertical_accuracy = random.choice((24, 32, 48, 48, 64, 64, 96, 128)) - else: - loc.vertical_accuracy = random.choice((3, 4, 6, 6, 8, 12, 24)) loc.horizontal_accuracy = request.accuracy - - sen.linear_acceleration_x = random.triangular(-3, 1, 0) - sen.linear_acceleration_y = random.triangular(-2, 3, 0) - sen.linear_acceleration_z = random.triangular(-4, 2, 0) - sen.magnetic_field_x = random.triangular(-50, 50, 0) - sen.magnetic_field_y = random.triangular(-60, 50, -5) - sen.magnetic_field_z = random.triangular(-60, 40, -30) - sen.magnetic_field_accuracy = random.choice((-1, 1, 1, 2, 2, 2, 2)) - sen.attitude_pitch = random.triangular(-1.5, 1.5, 0.2) - sen.attitude_yaw = random.uniform(-3, 3) - sen.attitude_roll = random.triangular(-2.8, 2.5, 0.25) - sen.rotation_rate_x = random.triangular(-6, 4, 0) - sen.rotation_rate_y = random.triangular(-5.5, 5, 0) - sen.rotation_rate_z = random.triangular(-5, 3, 0) - sen.gravity_x = random.triangular(-1, 1, 0.15) - sen.gravity_y = random.triangular(-1, 1, -.2) - sen.gravity_z = random.triangular(-1, .7, -0.8) + if request.accuracy >= 10: + loc.vertical_accuracy = weighted_choice([ + (6, 4), + (8, 34), + (10, 35), + (12, 11), + (16, 4), + (24, 8), + (32, 3), + (48, 1) + ]) + else: + loc.vertical_accuracy = weighted_choice([ + (3, 15), + (4, 39), + (6, 14), + (8, 13), + (10, 14), + (12, 5) + ]) + + sen.magnetic_field_accuracy = weighted_choice([ + (-1, 8), + (0, 2), + (1, 42), + (2, 48) + ]) + if sen.magnetic_field_accuracy == -1: + sen.magnetic_field_x = 0 + sen.magnetic_field_y = 0 + sen.magnetic_field_z = 0 + else: + sen.magnetic_field_x = self.state.magnetic_field_x + sen.magnetic_field_y = self.state.magnetic_field_y + sen.magnetic_field_z = self.state.magnetic_field_z + + sen.linear_acceleration_x = random.triangular(-1.5, 2.5, 0) + sen.linear_acceleration_y = random.triangular(-1.2, 1.4, 0) + sen.linear_acceleration_z = random.triangular(-1.4, .9, 0) + sen.attitude_pitch = random.triangular(-1.56, 1.57, 0.475) + sen.attitude_yaw = random.triangular(-1.56, 3.14, .1) + sen.attitude_roll = random.triangular(-3.14, 3.14, 0) + sen.rotation_rate_x = random.triangular(-3.2, 3.52, 0) + sen.rotation_rate_y = random.triangular(-3.1, 4.88, 0) + sen.rotation_rate_z = random.triangular(-6, 3.7, 0) + sen.gravity_x = random.triangular(-1, 1, 0.01) + sen.gravity_y = random.triangular(-1, 1, -.4) + sen.gravity_z = random.triangular(-1, 1, -.4) sen.status = 3 sig.unknown25 = 4500779412463383546 @@ -287,14 +341,15 @@ def _build_main_request(self, subrequests, platforms, player_position=None): plat8 = request.platform_requests.add() plat8.type = 8 plat8.request_message = plat_eight.SerializeToString() - + sig_request = SendEncryptedSignatureRequest() - sig_request.encrypted_signature = pycrypt(signature_proto, sig.timestamp_since_start) + sig_request.encrypted_signature = pycrypt(signature_proto, + sig.timestamp_since_start) plat = request.platform_requests.add() plat.type = 6 plat.request_message = sig_request.SerializeToString() - request.ms_since_last_locationfix = int(random.triangular(300, 30000, 10000)) + request.ms_since_last_locationfix = sig.timestamp_since_start - loc.timestamp_snapshot self.log.debug('Generated protobuf request: \n\r%s', request) @@ -307,13 +362,12 @@ def _needsPtr8(self, requests): rtype, _ = requests[0] # GetMapObjects or GetPlayer: 50% # Encounter: 10% - # Others: 3% + # Others: 3% if ((rtype in (2, 106) and randval > 0.5) - or (rtype == 102 and randval > 0.9) - or randval > 0.97): + or (rtype == 102 and randval > 0.9) or randval > 0.97): return True return False - + def _build_sub_requests(self, mainrequest, subrequest_list): self.log.debug('Generating sub RPC requests...') @@ -321,7 +375,9 @@ def _build_sub_requests(self, mainrequest, subrequest_list): if params: entry_name = RequestType.Name(entry_id) proto_name = entry_name.lower() + '_message' - bytes = self._get_proto_bytes('pogoprotos.networking.requests.messages.', proto_name, params) + bytes = self._get_proto_bytes( + 'pogoprotos.networking.requests.messages.', proto_name, + params) subrequest = mainrequest.requests.add() subrequest.request_type = entry_id @@ -342,7 +398,9 @@ def _build_platform_requests(self, mainrequest, platform_list): if entry_name == 'UNKNOWN_PTR_8': entry_name = 'UNKNOWN_PTR8' proto_name = entry_name.lower() + '_request' - bytes = self._get_proto_bytes('pogoprotos.networking.platform.requests.', proto_name, params) + bytes = self._get_proto_bytes( + 'pogoprotos.networking.platform.requests.', proto_name, + params) platform = mainrequest.platform_requests.add() platform.type = entry_id @@ -353,7 +411,6 @@ def _build_platform_requests(self, mainrequest, platform_list): platform.type = entry_id return mainrequest - def _get_proto_bytes(self, path, name, entry_content): proto_classname = path + name + '_pb2.' + name @@ -370,14 +427,18 @@ def _get_proto_bytes(self, path, name, entry_content): r = getattr(proto, key) r.append(i) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, i, proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, i, proto_classname, e) elif isinstance(value, dict): for k in value.keys(): try: r = getattr(proto, key) setattr(r, k, value[k]) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, str(value), proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, str(value), proto_classname, e) else: try: setattr(proto, key, value) @@ -387,23 +448,30 @@ def _get_proto_bytes(self, path, name, entry_content): r = getattr(proto, key) r.append(value) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, value, proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, value, proto_classname, e) return proto.SerializeToString() - def _parse_main_response(self, response_raw, subrequests, use_dict = True): + def _parse_main_response(self, response_raw, subrequests, use_dict=True): self.log.debug('Parsing main RPC response...') if response_raw.status_code == 400: raise BadRequestException("400: Bad Request") if response_raw.status_code == 403: - raise NianticIPBannedException("Seems your IP Address is banned or something else went badly wrong...") + raise NianticIPBannedException( + "Seems your IP Address is banned or something else went badly wrong..." + ) elif response_raw.status_code in (502, 503, 504): - raise NianticOfflineException('{} Server Error'.format(response_raw.status_code)) + raise NianticOfflineException( + '{} Server Error'.format(response_raw.status_code)) elif response_raw.status_code != 200: - error = 'Unexpected HTTP server response - needs 200 got {}'.format(response_raw.status_code) + error = 'Unexpected HTTP server response - needs 200 got {}'.format( + response_raw.status_code) self.log.warning(error) - self.log.debug('HTTP output: \n%s', response_raw.content.decode('utf-8')) + self.log.debug('HTTP output: \n%s', + response_raw.content.decode('utf-8')) raise UnexpectedResponseException(error) if not response_raw.content: @@ -415,11 +483,15 @@ def _parse_main_response(self, response_raw, subrequests, use_dict = True): response_proto.ParseFromString(response_raw.content) except message.DecodeError as e: self.log.error('Could not parse response: %s', e) - raise MalformedNianticResponseException('Could not decode response.') + raise MalformedNianticResponseException( + 'Could not decode response.') - self.log.debug('Protobuf structure of rpc response:\n\r%s', response_proto) + self.log.debug('Protobuf structure of rpc response:\n\r%s', + response_proto) try: - self.log.debug('Decode raw over protoc (protoc has to be in your PATH):\n\r%s', self.decode_raw(response_raw.content).decode('utf-8')) + self.log.debug( + 'Decode raw over protoc (protoc has to be in your PATH):\n\r%s', + self.decode_raw(response_raw.content).decode('utf-8')) except Exception: self.log.debug('Error during protoc parsing - ignored.') @@ -431,17 +503,23 @@ def _parse_main_response(self, response_raw, subrequests, use_dict = True): response_proto_dict = {'envelope': response_proto} if not response_proto_dict: - raise MalformedNianticResponseException('Could not convert protobuf to dict.') - - response_proto_dict = self._parse_sub_responses(response_proto, subrequests, response_proto_dict, use_dict) - - #It can't be done before + raise MalformedNianticResponseException( + 'Could not convert protobuf to dict.') + + response_proto_dict = self._parse_sub_responses( + response_proto, subrequests, response_proto_dict, use_dict) + + # It can't be done before. if not use_dict: del response_proto_dict['envelope'].returns[:] return response_proto_dict - def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_dict, use_dict = True): + def _parse_sub_responses(self, + response_proto, + subrequests_list, + response_proto_dict, + use_dict=True): self.log.debug('Parsing sub RPC responses...') response_proto_dict['responses'] = {} @@ -462,9 +540,10 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ subresponse_return = None try: subresponse_extension = self.get_class(proto_classname)() - except Exception as e: + except Exception: subresponse_extension = None - error = 'Protobuf definition for {} not found'.format(proto_classname) + error = 'Protobuf definition for {} not found'.format( + proto_classname) subresponse_return = error self.log.warning(error) @@ -473,11 +552,13 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ subresponse_extension.ParseFromString(subresponse) if use_dict: - subresponse_return = protobuf_to_dict(subresponse_extension) + subresponse_return = protobuf_to_dict( + subresponse_extension) else: subresponse_return = subresponse_extension except Exception: - error = "Protobuf definition for {} seems not to match".format(proto_classname) + error = "Protobuf definition for {} seems not to match".format( + proto_classname) subresponse_return = error self.log.warning(error) @@ -485,3 +566,33 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ i += 1 return response_proto_dict + + +# Original by Noctem. +class RpcState: + def __init__(self): + self.session_hash = os.urandom(16) + self.mag_x_min = random.uniform(-80, 60) + self.mag_x_max = self.mag_x_min + 20 + self.mag_y_min = random.uniform(-120, 90) + self.mag_y_max = self.mag_y_min + 30 + self.mag_z_min = random.uniform(-70, 40) + self.mag_z_max = self.mag_y_min + 15 + self._course = random.uniform(0, 359.99) + + @property + def magnetic_field_x(self): + return random.uniform(self.mag_x_min, self.mag_x_max) + + @property + def magnetic_field_y(self): + return random.uniform(self.mag_y_min, self.mag_y_max) + + @property + def magnetic_field_z(self): + return random.uniform(self.mag_z_min, self.mag_z_max) + + @property + def course(self): + self._course = random.triangular(0, 359.99, self._course) + return self._course diff --git a/pgoapi/utilities.py b/pgoapi/utilities.py index 709aecdc..1181f0cc 100755 --- a/pgoapi/utilities.py +++ b/pgoapi/utilities.py @@ -21,13 +21,13 @@ import time import struct +import random import logging from json import JSONEncoder from binascii import unhexlify # other stuff -from google.protobuf.internal import encoder from geopy.geocoders import GoogleV3 from s2sphere import LatLng, Angle, Cap, RegionCoverer, math @@ -37,25 +37,26 @@ def f2i(float): - return struct.unpack(' 1500: radius = 1500 # radius = 1500 is max allowed by the server - region = Cap.from_axis_angle(LatLng.from_degrees(lat, long).to_point(), Angle.from_degrees(360*radius/(2*math.pi*EARTH_RADIUS))) + region = Cap.from_axis_angle( + LatLng.from_degrees(lat, long).to_point(), + Angle.from_degrees(360 * radius / (2 * math.pi * EARTH_RADIUS))) coverer = RegionCoverer() coverer.min_level = 15 coverer.max_level = 15 @@ -90,14 +94,14 @@ def get_cell_ids(lat, long, radius=500): return sorted([x.id() for x in cells]) -def get_time(ms = False): +def get_time(ms=False): if ms: return int(time.time() * 1000) else: return int(time.time()) -def get_format_time_diff(low, high, ms = True): +def get_format_time_diff(low, high, ms=True): diff = (high - low) if ms: m, s = divmod(diff / 1000, 60) @@ -113,3 +117,14 @@ def parse_api_endpoint(api_url): api_url = 'https://{}/rpc'.format(api_url) return api_url + + +def weighted_choice(choices): + total = sum(w for c, w in choices) + r = random.uniform(0, total) + upto = 0 + for c, w in choices: + if upto + w >= r: + return c + upto += w + assert False, "Shouldn't get here"