login procedure more true to original and fix for broken .fit downloads
This commit is contained in:
parent
d6cad6736f
commit
44d58f0e93
@ -36,6 +36,9 @@ log = logging.getLogger(__name__)
|
|||||||
# reduce logging noise from requests library
|
# reduce logging noise from requests library
|
||||||
logging.getLogger("requests").setLevel(logging.ERROR)
|
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
SSO_LOGIN_URL = "https://sso.garmin.com/sso/login"
|
||||||
|
"""The Garmin Connect Single-Sign On login URL."""
|
||||||
|
|
||||||
|
|
||||||
def require_session(client_function):
|
def require_session(client_function):
|
||||||
"""Decorator that is used to annotate :class:`GarminClient`
|
"""Decorator that is used to annotate :class:`GarminClient`
|
||||||
@ -109,18 +112,31 @@ class GarminClient(object):
|
|||||||
log.debug("auth ticket validation url: {}".format(validation_url))
|
log.debug("auth ticket validation url: {}".format(validation_url))
|
||||||
self._validate_auth_ticket(validation_url)
|
self._validate_auth_ticket(validation_url)
|
||||||
|
|
||||||
# referer seems to be a header that is required by the REST API
|
# Referer seems to be a header that is required by the REST API
|
||||||
self.session.headers.update({'Referer': "https://some.random.site"})
|
self.session.headers.update({'Referer': "https://some.random.site"})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_flow_execution_key(self, request_params):
|
def _get_flow_execution_key(self, request_params):
|
||||||
|
# The flowExecutionKey is embedded in the
|
||||||
|
# https://sso.garmin.com/sso/login response page. For example:
|
||||||
|
# <!-- flowExecutionKey: [e3s1] -->
|
||||||
log.debug("get flow execution key ...")
|
log.debug("get flow execution key ...")
|
||||||
response = self.session.get(
|
response = self.session.get(SSO_LOGIN_URL, params=request_params)
|
||||||
"https://sso.garmin.com/sso/login", params=request_params)
|
if response.status_code != 200:
|
||||||
# parse out flowExecutionKey
|
raise RuntimeError(
|
||||||
flow_execution_key = re.search(
|
"auth failure: %s: code %d: %s" %
|
||||||
r'name="lt"\s+value="([^"]+)"', response.text).groups(1)[0]
|
(SSO_LOGIN_URL, response.status_code, response.text))
|
||||||
|
# extract flowExecutionKey
|
||||||
|
match = re.search(r'name="lt"\s+value="([^"]+)"', response.text)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError(
|
||||||
|
"auth failure: unable to extract flowExecutionKey: %s:\n%s" %
|
||||||
|
(SSO_LOGIN_URL, response.text))
|
||||||
|
flow_execution_key = match.groups(1)[0]
|
||||||
return flow_execution_key
|
return flow_execution_key
|
||||||
|
|
||||||
|
|
||||||
def _get_auth_ticket(self, flow_execution_key, request_params):
|
def _get_auth_ticket(self, flow_execution_key, request_params):
|
||||||
data = {
|
data = {
|
||||||
"username": self.username, "password": self.password,
|
"username": self.username, "password": self.password,
|
||||||
@ -128,12 +144,12 @@ class GarminClient(object):
|
|||||||
}
|
}
|
||||||
log.debug("single sign-on ...")
|
log.debug("single sign-on ...")
|
||||||
sso_response = self.session.post(
|
sso_response = self.session.post(
|
||||||
"https://sso.garmin.com/sso/login",
|
SSO_LOGIN_URL, params=request_params,
|
||||||
params=request_params, data=data, allow_redirects=False)
|
data=data, allow_redirects=False)
|
||||||
# response must contain an SSO ticket
|
# response must contain an SSO ticket
|
||||||
ticket_match = re.search("ticket=([^']+)'", sso_response.text)
|
ticket_match = re.search("ticket=([^']+)'", sso_response.text)
|
||||||
if not ticket_match:
|
if not ticket_match:
|
||||||
raise ValueError("failed to get authentication ticket: "
|
raise ValueError("auth failure: unable to get auth ticket: "
|
||||||
"did you enter valid credentials?")
|
"did you enter valid credentials?")
|
||||||
ticket = ticket_match.group(1)
|
ticket = ticket_match.group(1)
|
||||||
log.debug("SSO ticket: {}".format(ticket))
|
log.debug("SSO ticket: {}".format(ticket))
|
||||||
@ -143,16 +159,31 @@ class GarminClient(object):
|
|||||||
validation_url = validation_url.group(1)
|
validation_url = validation_url.group(1)
|
||||||
return validation_url
|
return validation_url
|
||||||
|
|
||||||
|
|
||||||
def _validate_auth_ticket(self, validation_url):
|
def _validate_auth_ticket(self, validation_url):
|
||||||
log.debug("validating authentication ticket ...")
|
log.debug("validating auth ticket at %s ...", validation_url)
|
||||||
response = self.session.get(validation_url, allow_redirects=True)
|
response = self.session.get(validation_url, allow_redirects=False)
|
||||||
if response.status_code == 200 or response.status_code == 404:
|
|
||||||
# for some reason a 404 response code can also denote a
|
# It appears as if from this point on, the User-Agent header needs to
|
||||||
# successful auth ticket validation
|
# be set to something similar to the value below for authentication
|
||||||
|
# to succeed and for downloads of .fit files to work properly.
|
||||||
|
self.session.headers.update({
|
||||||
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36'
|
||||||
|
})
|
||||||
|
|
||||||
|
# we will be redirected several times. just follow through ..
|
||||||
|
while response.status_code == 302:
|
||||||
|
redirect_url = response.headers['Location']
|
||||||
|
log.debug("redirected to: '%s'", redirect_url)
|
||||||
|
response = self.session.get(redirect_url, allow_redirects=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
# auth ticket successfully validated.
|
||||||
|
# our client should now have all necessary cookies set.
|
||||||
return
|
return
|
||||||
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
u"failed to validate authentication ticket: {}:\n{}".format(
|
u"auth failure: unable to validate auth ticket: {}:\n{}".format(
|
||||||
response.status_code, response.text))
|
response.status_code, response.text))
|
||||||
|
|
||||||
|
|
||||||
@ -223,7 +254,7 @@ class GarminClient(object):
|
|||||||
:returns: The activity summary as a JSON dict.
|
:returns: The activity summary as a JSON dict.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
response = self.session.get("https://connect.garmin.com/proxy/activity-service-1.3/json/activity/{}".format(activity_id))
|
response = self.session.get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activity/{}".format(activity_id))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception(u"failed to fetch activity {}: {}\n{}".format(
|
raise Exception(u"failed to fetch activity {}: {}\n{}".format(
|
||||||
activity_id, response.status_code, response.text))
|
activity_id, response.status_code, response.text))
|
||||||
@ -241,7 +272,7 @@ class GarminClient(object):
|
|||||||
:rtype: dict
|
:rtype: dict
|
||||||
"""
|
"""
|
||||||
# mounted at xml or json depending on result encoding
|
# mounted at xml or json depending on result encoding
|
||||||
response = self.session.get("https://connect.garmin.com/proxy/activity-service-1.3/json/activityDetails/{}".format(activity_id))
|
response = self.session.get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/{}".format(activity_id))
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
raise Exception(u"failed to fetch activity details for {}: {}\n{}".format(
|
raise Exception(u"failed to fetch activity details for {}: {}\n{}".format(
|
||||||
activity_id, response.status_code, response.text))
|
activity_id, response.status_code, response.text))
|
||||||
@ -260,7 +291,7 @@ class GarminClient(object):
|
|||||||
or ``None`` if the activity couldn't be exported to GPX.
|
or ``None`` if the activity couldn't be exported to GPX.
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
response = self.session.get("https://connect.garmin.com/proxy/activity-service-1.3/gpx/course/{}".format(activity_id))
|
response = self.session.get("https://connect.garmin.com/modern/proxy/activity-service-1.3/gpx/course/{}".format(activity_id))
|
||||||
# An alternate URL that seems to produce the same results
|
# An alternate URL that seems to produce the same results
|
||||||
# and is the one used when exporting through the Garmin
|
# and is the one used when exporting through the Garmin
|
||||||
# Connect web page.
|
# Connect web page.
|
||||||
@ -288,7 +319,7 @@ class GarminClient(object):
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.session.get("https://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/{}?full=true".format(activity_id))
|
response = self.session.get("https://connect.garmin.com/modern/proxy/activity-service-1.1/tcx/activity/{}?full=true".format(activity_id))
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
return None
|
return None
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@ -309,7 +340,7 @@ class GarminClient(object):
|
|||||||
its contents, or :obj:`(None,None)` if no file is found.
|
its contents, or :obj:`(None,None)` if no file is found.
|
||||||
:rtype: (str, str)
|
:rtype: (str, str)
|
||||||
"""
|
"""
|
||||||
response = self.session.get("https://connect.garmin.com/proxy/download-service/files/activity/{}".format(activity_id))
|
response = self.session.get("https://connect.garmin.com/modern/proxy/download-service/files/activity/{}".format(activity_id))
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
# Manually entered activity, no file source available
|
# Manually entered activity, no file source available
|
||||||
return (None,None)
|
return (None,None)
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
requests==2.4.3
|
requests==2.9.1
|
||||||
python-dateutil==2.2
|
python-dateutil==2.4.1
|
||||||
|
Loading…
Reference in New Issue
Block a user