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