Skip to content

Commit

Permalink
Fix #18
Browse files Browse the repository at this point in the history
As described in play-store-api code [1] recenlty google introduced a Time
To Live to each auth token, which caused them to expire in a matter of
seconds. Moreover, in order to retrieve a long-ttl token, a second auth
request now is needed, sending the Master Token returned from the first
request. This 'second round' request will return the actual authSubToken.

Another addition is that the code now return response's text for some
generic errors, in order to help debugging those problems.
  • Loading branch information
Domenico Iezzi committed Nov 3, 2017
1 parent 25bfb4a commit df2fa82
Showing 1 changed file with 58 additions and 17 deletions.
75 changes: 58 additions & 17 deletions gpapi/googleplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(self, debug=False, device_codename='bacon'):
self.gsfId = None
self.debug = debug
self.deviceBuilder = config.DeviceBuilder(device_codename)
# save last response text for error logging
self.lastResponseText = None

def encrypt_password(self, login, passwd):
"""Encrypt the password using the google publickey, using
Expand Down Expand Up @@ -152,12 +154,14 @@ def login(self, email=None, password=None, gsfId=None, authSubToken=None):
# AC2DM token
params = self.deviceBuilder.getLoginParams(email, encryptedPass)
response = requests.post(self.AUTHURL, data=params, verify=ssl_verify)
if self.debug:
self.lastResponseText = response.text
data = response.text.split()
params = {}
for d in data:
if "=" not in d:
continue
k, v = d.split("=")[0:2]
k, v = d.split("=", 1)
params[k.strip().lower()] = v.strip()
if "auth" in params:
ac2dmToken = params["auth"]
Expand All @@ -168,6 +172,8 @@ def login(self, email=None, password=None, gsfId=None, authSubToken=None):
"to unlock, or setup an app-specific password")
raise LoginError("server says: " + params["error"])
else:
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError("Auth token not found.")

self.gsfId = self.checkin(email, ac2dmToken)
Expand All @@ -187,31 +193,58 @@ def login(self, email=None, password=None, gsfId=None, authSubToken=None):
raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed')

def getAuthSubToken(self, email, passwd):
params = self.deviceBuilder.getAuthParams(email, passwd)
response = requests.post(self.AUTHURL, data=params, verify=ssl_verify)
requestParams = self.deviceBuilder.getAuthParams(email, passwd)
response = requests.post(self.AUTHURL, data=requestParams, verify=ssl_verify)
data = response.text.split()
if self.debug:
self.lastResponseText = response.text
params = {}
for d in data:
if "=" not in d:
continue
k, v = d.split("=")[0:2]
k, v = d.split("=", 1)
params[k.strip().lower()] = v.strip()
if "auth" in params:
self.setAuthSubToken(params["auth"])
if "token" in params:
firstToken = params["token"]
if self.debug:
print('Master token: %s' % firstToken)
secondToken = self.getSecondRoundToken(requestParams, firstToken)
self.setAuthSubToken(secondToken)
elif "error" in params:
raise LoginError("server says: " + params["error"])
else:
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError("Auth token not found.")

def _check_response_integrity(self, apps):
"""Like described in issue #18, after some time it seems
that google invalidates the token. And the strange thing is that when
sending requests with an invalid token, it won't throw an error but
it returns empty responses. This is a function used to check if the
content returned is valid (usually a docId field is always present)"""
if any([a['docId'] == '' for a in apps]):
raise LoginError('Unexpected behaviour, probably expired '
'token')
def getSecondRoundToken(self, previousParams, firstToken):
previousParams['Token'] = firstToken
previousParams['service'] = 'androidmarket'
previousParams['check_email'] = '1'
previousParams['token_request_options'] = 'CAA4AQ=='
previousParams['system_partition'] = '1'
previousParams['_opt_is_called_from_account_manager'] = '1'
previousParams['google_play_services_version'] = '11518448'
previousParams.pop('Email')
previousParams.pop('EncryptedPasswd')
response = requests.post(self.AUTHURL, data=previousParams, verify=ssl_verify)
data = response.text.split()
if self.debug:
self.lastResponseText = response.text
params = {}
for d in data:
if "=" not in d:
continue
k, v = d.split("=", 1)
params[k.strip().lower()] = v.strip()
if "auth" in params:
return params["auth"]
elif "error" in params:
raise LoginError("server says: " + params["error"])
else:
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError("Auth token not found.")

def executeRequestApi2(self, path, datapost=None,
post_content_type="application/x-www-form-urlencoded; charset=UTF-8"):
Expand All @@ -232,8 +265,12 @@ def executeRequestApi2(self, path, datapost=None,
verify=ssl_verify,
timeout=60)

if self.debug:
self.lastResponseText = response.text
message = googleplay_pb2.ResponseWrapper.FromString(response.content)
if message.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(message.commands.displayErrorMessage)

return message
Expand Down Expand Up @@ -270,6 +307,8 @@ def search(self, query, nb_result, offset=None):
if len(response.payload.listResponse.cluster) == 0:
# strange behaviour, probably due to
# expired token
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError('Unexpected behaviour, probably expired '
'token')
cluster = response.payload.listResponse.cluster[0]
Expand All @@ -293,7 +332,6 @@ def details(self, packageName):
path = "details?doc=%s" % requests.utils.quote(packageName)
data = self.executeRequestApi2(path)
app = utils.fromDocToDictionary(data.payload.detailsResponse.docV2)
self._check_response_integrity([app])
return app

def bulkDetails(self, packageNames):
Expand Down Expand Up @@ -324,7 +362,6 @@ def bulkDetails(self, packageNames):
result.append(None)
else:
appDetails = utils.fromDocToDictionary(entry.doc)
self._check_response_integrity([appDetails])
result.append(appDetails)
return result

Expand Down Expand Up @@ -486,6 +523,8 @@ def delivery(self, packageName, versionCode, offerType=1,
timeout=60)
resObj = googleplay_pb2.ResponseWrapper.FromString(response.content)
if resObj.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(resObj.commands.displayErrorMessage)
elif resObj.payload.deliveryResponse.appDeliveryData.downloadUrl == "":
raise RequestError('App not purchased')
Expand Down Expand Up @@ -550,6 +589,8 @@ def download(self, packageName, versionCode, offerType=1,

resObj = googleplay_pb2.ResponseWrapper.FromString(response.content)
if resObj.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(resObj.commands.displayErrorMessage)
else:
dlToken = resObj.payload.buyResponse.downloadToken
Expand Down

0 comments on commit df2fa82

Please sign in to comment.