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.
This commit is contained in:
Domenico Iezzi 2017-11-03 11:35:50 +01:00
parent 25bfb4aaec
commit df2fa823fd
No known key found for this signature in database
GPG Key ID: 7AC94D5DDA2FB7EE

View File

@ -50,6 +50,8 @@ class GooglePlayAPI(object):
self.gsfId = None self.gsfId = None
self.debug = debug self.debug = debug
self.deviceBuilder = config.DeviceBuilder(device_codename) self.deviceBuilder = config.DeviceBuilder(device_codename)
# save last response text for error logging
self.lastResponseText = None
def encrypt_password(self, login, passwd): def encrypt_password(self, login, passwd):
"""Encrypt the password using the google publickey, using """Encrypt the password using the google publickey, using
@ -152,12 +154,14 @@ class GooglePlayAPI(object):
# AC2DM token # AC2DM token
params = self.deviceBuilder.getLoginParams(email, encryptedPass) params = self.deviceBuilder.getLoginParams(email, encryptedPass)
response = requests.post(self.AUTHURL, data=params, verify=ssl_verify) response = requests.post(self.AUTHURL, data=params, verify=ssl_verify)
if self.debug:
self.lastResponseText = response.text
data = response.text.split() data = response.text.split()
params = {} params = {}
for d in data: for d in data:
if "=" not in d: if "=" not in d:
continue continue
k, v = d.split("=")[0:2] k, v = d.split("=", 1)
params[k.strip().lower()] = v.strip() params[k.strip().lower()] = v.strip()
if "auth" in params: if "auth" in params:
ac2dmToken = params["auth"] ac2dmToken = params["auth"]
@ -168,6 +172,8 @@ class GooglePlayAPI(object):
"to unlock, or setup an app-specific password") "to unlock, or setup an app-specific password")
raise LoginError("server says: " + params["error"]) raise LoginError("server says: " + params["error"])
else: else:
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError("Auth token not found.") raise LoginError("Auth token not found.")
self.gsfId = self.checkin(email, ac2dmToken) self.gsfId = self.checkin(email, ac2dmToken)
@ -187,31 +193,58 @@ class GooglePlayAPI(object):
raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed') raise LoginError('Either (email,pass) or (gsfId, authSubToken) is needed')
def getAuthSubToken(self, email, passwd): def getAuthSubToken(self, email, passwd):
params = self.deviceBuilder.getAuthParams(email, passwd) requestParams = self.deviceBuilder.getAuthParams(email, passwd)
response = requests.post(self.AUTHURL, data=params, verify=ssl_verify) response = requests.post(self.AUTHURL, data=requestParams, verify=ssl_verify)
data = response.text.split() data = response.text.split()
if self.debug:
self.lastResponseText = response.text
params = {} params = {}
for d in data: for d in data:
if "=" not in d: if "=" not in d:
continue continue
k, v = d.split("=")[0:2] k, v = d.split("=", 1)
params[k.strip().lower()] = v.strip() params[k.strip().lower()] = v.strip()
if "auth" in params: if "token" in params:
self.setAuthSubToken(params["auth"]) firstToken = params["token"]
if self.debug:
print('Master token: %s' % firstToken)
secondToken = self.getSecondRoundToken(requestParams, firstToken)
self.setAuthSubToken(secondToken)
elif "error" in params: elif "error" in params:
raise LoginError("server says: " + params["error"]) raise LoginError("server says: " + params["error"])
else: else:
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError("Auth token not found.") raise LoginError("Auth token not found.")
def _check_response_integrity(self, apps): def getSecondRoundToken(self, previousParams, firstToken):
"""Like described in issue #18, after some time it seems previousParams['Token'] = firstToken
that google invalidates the token. And the strange thing is that when previousParams['service'] = 'androidmarket'
sending requests with an invalid token, it won't throw an error but previousParams['check_email'] = '1'
it returns empty responses. This is a function used to check if the previousParams['token_request_options'] = 'CAA4AQ=='
content returned is valid (usually a docId field is always present)""" previousParams['system_partition'] = '1'
if any([a['docId'] == '' for a in apps]): previousParams['_opt_is_called_from_account_manager'] = '1'
raise LoginError('Unexpected behaviour, probably expired ' previousParams['google_play_services_version'] = '11518448'
'token') 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, def executeRequestApi2(self, path, datapost=None,
post_content_type="application/x-www-form-urlencoded; charset=UTF-8"): post_content_type="application/x-www-form-urlencoded; charset=UTF-8"):
@ -232,8 +265,12 @@ class GooglePlayAPI(object):
verify=ssl_verify, verify=ssl_verify,
timeout=60) timeout=60)
if self.debug:
self.lastResponseText = response.text
message = googleplay_pb2.ResponseWrapper.FromString(response.content) message = googleplay_pb2.ResponseWrapper.FromString(response.content)
if message.commands.displayErrorMessage != "": if message.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(message.commands.displayErrorMessage) raise RequestError(message.commands.displayErrorMessage)
return message return message
@ -270,6 +307,8 @@ class GooglePlayAPI(object):
if len(response.payload.listResponse.cluster) == 0: if len(response.payload.listResponse.cluster) == 0:
# strange behaviour, probably due to # strange behaviour, probably due to
# expired token # expired token
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise LoginError('Unexpected behaviour, probably expired ' raise LoginError('Unexpected behaviour, probably expired '
'token') 'token')
cluster = response.payload.listResponse.cluster[0] cluster = response.payload.listResponse.cluster[0]
@ -293,7 +332,6 @@ class GooglePlayAPI(object):
path = "details?doc=%s" % requests.utils.quote(packageName) path = "details?doc=%s" % requests.utils.quote(packageName)
data = self.executeRequestApi2(path) data = self.executeRequestApi2(path)
app = utils.fromDocToDictionary(data.payload.detailsResponse.docV2) app = utils.fromDocToDictionary(data.payload.detailsResponse.docV2)
self._check_response_integrity([app])
return app return app
def bulkDetails(self, packageNames): def bulkDetails(self, packageNames):
@ -324,7 +362,6 @@ class GooglePlayAPI(object):
result.append(None) result.append(None)
else: else:
appDetails = utils.fromDocToDictionary(entry.doc) appDetails = utils.fromDocToDictionary(entry.doc)
self._check_response_integrity([appDetails])
result.append(appDetails) result.append(appDetails)
return result return result
@ -486,6 +523,8 @@ class GooglePlayAPI(object):
timeout=60) timeout=60)
resObj = googleplay_pb2.ResponseWrapper.FromString(response.content) resObj = googleplay_pb2.ResponseWrapper.FromString(response.content)
if resObj.commands.displayErrorMessage != "": if resObj.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(resObj.commands.displayErrorMessage) raise RequestError(resObj.commands.displayErrorMessage)
elif resObj.payload.deliveryResponse.appDeliveryData.downloadUrl == "": elif resObj.payload.deliveryResponse.appDeliveryData.downloadUrl == "":
raise RequestError('App not purchased') raise RequestError('App not purchased')
@ -550,6 +589,8 @@ class GooglePlayAPI(object):
resObj = googleplay_pb2.ResponseWrapper.FromString(response.content) resObj = googleplay_pb2.ResponseWrapper.FromString(response.content)
if resObj.commands.displayErrorMessage != "": if resObj.commands.displayErrorMessage != "":
if self.debug:
print('Last response text: %s' % self.lastResponseText)
raise RequestError(resObj.commands.displayErrorMessage) raise RequestError(resObj.commands.displayErrorMessage)
else: else:
dlToken = resObj.payload.buyResponse.downloadToken dlToken = resObj.payload.buyResponse.downloadToken