Beispiel #1
0
    def upload(self, user):
        """
        Upload an activity once authenticated
        """
        assert isinstance(user, User)
        assert user.session is not None

        api = GarminAPI()
        try:
            self.id, uploaded = api.upload_activity(user.session, self)
        except GarminAPIException as e:
            logger.warning('Upload failure: {}'.format(e))
            return False

        if uploaded:
            logger.info('Uploaded activity {}'.format(self))

            # Set activity name if specified
            if self.name:
                try:
                    api.set_activity_name(user.session, self)
                except GarminAPIException as e:
                    logger.warning('Activity name update failed: {}'.format(e))

            # Set activity type if specified
            if self.type:
                try:
                    api.set_activity_type(user.session, self)
                except GarminAPIException as e:
                    logger.warning('Activity type update failed: {}'.format(e))

        else:
            logger.info('Activity already uploaded {}'.format(self))

        return True
Beispiel #2
0
    def rate_limit(self):
        min_period = 1
        if not self.last_request:
            self.last_request = 0.0

        wait_time = max(0, min_period - (time.time() - self.last_request))
        if wait_time <= 0:
            return
        time.sleep(wait_time)

        self.last_request = time.time()
        logger.info("Rate limited for %f" % wait_time)
Beispiel #3
0
    def run(self):
        """
        Authenticated part of the workflow
        Simply login & upload every activity
        """
        if not self.user.authenticate():
            raise Exception('Invalid credentials')

        for activity in self.activities:
            self.rate_limit()
            activity.upload(self.user)

        logger.info('All done.')
Beispiel #4
0
    def authenticate(self):
        """
        Authenticate on Garmin API
        """
        logger.info('Try to login on GarminConnect...')
        logger.debug('Username: {}'.format(self.username))
        logger.debug('Password: {}'.format('*'*len(self.password)))

        api = GarminAPI()
        try:
            self.session = api.authenticate(self.username, self.password)
            logger.debug('Login Successful.')
        except Exception as e:
            logger.critical('Login Failure: {}'.format(e))
            return False

        return True
Beispiel #5
0
    def load_activities(self, paths):
        """
        Load all activities files:
        Sort out file name args given on command line.  Figure out if they are
        fitness file names, directory names containing fitness files, or names
        of csv file lists.
        Also, expand file name wildcards, if necessary.  Check to see if files
        exist and if the file extension is valid.  Build lists of fitnes
        filenames, directories # which will be further searched for files, and
        list files.
        """
        def is_csv(filename):
            '''
            check to see if file exists and that the file
            extension is .csv
            '''
            extension = os.path.splitext(filename)[1].lower()
            return extension == '.csv' and os.path.isfile(filename)

        def is_activity(filename):
            '''
            check to see if file exists and that the extension is a
            valid activity file accepted by GC.
            '''
            if not os.path.isfile(filename):
                logger.warning("File '{}' does not exist. Skipping...".format(
                    filename))  # noqa
                return False

            # Get file extension from name
            extension = os.path.splitext(filename)[1].lower()
            logger.debug("File '{}' has extension '{}'".format(
                filename, extension))  # noqa

            # Valid file extensions are .tcx, .fit, and .gpx
            if extension in VALID_GARMIN_FILE_EXTENSIONS:
                logger.debug("File '{}' extension '{}' is valid.".format(
                    filename, extension))  # noqa
                return True
            else:
                logger.warning(
                    "File '{}' extension '{}' is not valid. Skipping file...".
                    format(filename, extension))  # noqa
                return False

        valid_paths, csv_files = [], []
        for path in paths:
            path = os.path.realpath(path)
            if is_activity(path):
                # Use file directly
                valid_paths.append(path)

            elif is_csv(path):
                # Use file directly
                logger.info("List file '{}' will be processed...".format(path))
                csv_files.append(path)

            elif os.path.isdir(path):
                # Use files in directory
                # - Does not recursively drill into directories.
                # - Does not search for csv files in directories.
                valid_paths += [
                    f for f in glob.glob(os.path.join(path, '*'))
                    if is_activity(f)
                ]

        # Activity name given on command line only applies if a single filename
        # is given.  Otherwise, ignore.
        if len(valid_paths) != 1 and self.activity_name:
            logger.warning(
                '-a option valid only when one fitness file given. Ignoring -a option.'
            )  # noqa
            self.activity_name = None

        # Build activities from valid paths
        activities = [
            Activity(p, self.activity_name, self.activity_type)
            for p in valid_paths
        ]

        # Pull in file info from csv files and apppend activities
        for csv_file in csv_files:
            with open(csv_file, 'r') as csvfile:
                reader = csv.DictReader(csvfile)
                activities += [
                    Activity(row['filename'], row['name'], row['type'])
                    for row in reader if is_activity(row['filename'])
                ]

        if len(activities) == 0:
            raise Exception('No valid files.')

        return activities
Beispiel #6
0
    def authenticate(self, username, password):
        """
        That's where the magic happens !
        Try to mimick a browser behavior trying to login
        on Garmin Connect as closely as possible
        Outputs a Requests session, loaded with precious cookies
        """
        # Use a valid Browser user agent
        # TODO: use several UA picked randomly
        session = requests.Session()
        session.headers.update({
            'User-Agent':
            'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/50.0',  # noqa
        })

        # Request sso hostname
        sso_hostname = None
        resp = session.get(URL_HOSTNAME)
        if not resp.ok:
            raise Exception('Invalid SSO first request status code {}'.format(
                resp.status_code))  # noqa
        sso_hostname = resp.json().get('host')

        # Load login page to get login ticket
        # Full parameters from Firebug, we have to maintain
        # F**k this shit.
        # Who needs mandatory urls in a request parameters !
        params = {
            'clientId': 'GarminConnect',
            'connectLegalTerms': 'true',
            'consumeServiceTicket': 'false',
            'createAccountShown': 'true',
            'cssUrl': 'https://connect.garmin.com/gauth-custom-v1.2-min.css',
            'displayNameShown': 'false',
            'embedWidget': 'false',
            'gauthHost': 'https://sso.garmin.com/sso',
            'generateExtraServiceTicket': 'true',
            'generateNoServiceTicket': 'false',
            'generateTwoExtraServiceTickets': 'true',
            'globalOptInChecked': 'false',
            'globalOptInShown': 'true',
            'id': 'gauth-widget',
            'initialFocus': 'true',
            'locale': 'fr_FR',
            'locationPromptShown': 'true',
            'mfaRequired': 'false',
            'mobile': 'false',
            'openCreateAccount': 'false',
            'privacyStatementUrl':
            'https://www.garmin.com/fr-FR/privacy/connect/',  # noqa
            'redirectAfterAccountCreationUrl':
            'https://connect.garmin.com/modern/',  # noqa
            'redirectAfterAccountLoginUrl':
            'https://connect.garmin.com/modern/',  # noqa
            'rememberMeChecked': 'false',
            'rememberMeShown': 'true',
            'rememberMyBrowserChecked': 'false',
            'rememberMyBrowserShown': 'false',
            'service': 'https://connect.garmin.com/modern/',
            'showConnectLegalAge': 'false',
            'showPassword': '******',
            'showPrivacyPolicy': 'false',
            'showTermsOfUse': 'false',
            'source': 'https://connect.garmin.com/signin/',
            'useCustomHeader': 'false',
            'webhost': sso_hostname,
        }
        res = session.get(URL_LOGIN, params=params)
        if res.status_code != 200:
            raise Exception('No login form')

        # Lookup for CSRF token
        csrf = re.search(r'<input type="hidden" name="_csrf" value="(\w+)" />',
                         res.content.decode('utf-8'))  # noqa
        if csrf is None:
            raise Exception('No CSRF token')
        csrf_token = csrf.group(1)
        logger.debug('Found CSRF token {}'.format(csrf_token))

        # Login/Password with login ticket
        data = {
            'embed': 'false',
            'username': username,
            'password': password,
            '_csrf': csrf_token,
        }
        headers = {
            'Host': URL_HOST_SSO,
            'Referer': URL_SSO_SIGNIN,
        }
        res = session.post(URL_LOGIN,
                           params=params,
                           data=data,
                           headers=headers)
        if not res.ok:
            raise Exception('Authentification failed.')

        # Check we have sso guid in cookies
        if 'GARMIN-SSO-GUID' not in session.cookies:
            raise Exception('Missing Garmin auth cookie')

        # Try to find the full post login url in response
        regex = 'var response_url(\s+)= (\"|\').*?ticket=(?P<ticket>[\w\-]+)(\"|\')'  # noqa
        params = {}
        matches = re.search(regex, res.text)
        if not matches:
            raise Exception('Missing service ticket')
        params['ticket'] = matches.group('ticket')
        logger.debug('Found service ticket {}'.format(params['ticket']))

        # Second auth step
        # Needs a service ticket from previous response
        headers = {
            'Host': URL_HOST_CONNECT,
        }
        res = session.get(URL_POST_LOGIN, params=params, headers=headers)
        if res.status_code != 200 and not res.history:
            raise Exception('Second auth step failed.')

        # Check login
        res = session.get(URL_PROFILE)
        if not res.ok:
            raise Exception("Login check failed.")
        garmin_user = res.json()
        logger.info('Logged in as {}'.format(garmin_user['username']))

        return session
Beispiel #7
0
    def authenticate(self, username, password):
        """
        That's where the magic happens !
        Try to mimick a browser behavior trying to login
        on Garmin Connect as closely as possible
        Outputs a Requests session, loaded with precious cookies
        """
        # Use a valid Browser user agent
        # TODO: use several UA picked randomly
        session = requests.Session()
        session.headers.update({
            'User-Agent':
            'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/50.0',  # noqa
        })

        # Request sso hostname
        sso_hostname = None
        resp = session.get(URL_HOSTNAME)
        if not resp.ok:
            raise Exception('Invalid SSO first request status code {}'.format(
                resp.status_code))  # noqa
        sso_hostname = resp.json().get('host')

        # Load login page to get login ticket
        # Full parameters from Firebug, we have to maintain
        # F**k this shit.
        # Who needs mandatory urls in a request parameters !
        params = {
            'clientId': 'GarminConnect',
            'connectLegalTerms': 'true',
            'consumeServiceTicket': 'false',
            'createAccountShown': 'true',
            'cssUrl':
            'https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css',  # noqa
            'displayNameShown': 'false',
            'embedWidget': 'false',
            'gauthHost': 'https://sso.garmin.com/sso',
            'generateExtraServiceTicket': 'false',
            'globalOptInChecked': 'false',
            'globalOptInShown': 'false',
            'id': 'gauth-widget',
            'initialFocus': 'true',
            'locale': 'fr',
            'mobile': 'false',
            'openCreateAccount': 'false',
            'privacyStatementUrl': '//connect.garmin.com/fr-FR/privacy/',
            'redirectAfterAccountCreationUrl':
            'https://connect.garmin.com/modern/',  # noqa
            'redirectAfterAccountLoginUrl':
            'https://connect.garmin.com/modern/',  # noqa
            'rememberMeChecked': 'false',
            'rememberMeShown': 'true',
            'service': 'https://connect.garmin.com/modern/',
            'source': 'https://connect.garmin.com/fr-FR/signin',
            'usernameShown': 'false',
            'webhost': sso_hostname
        }

        res = session.get(URL_LOGIN, params=params)
        if res.status_code != 200:
            raise Exception('No login form')

        # Login/Password with login ticket
        data = {
            'embed': 'false',
            'username': username,
            'password': password,
        }
        headers = {
            'Host': URL_HOST_SSO,
        }
        res = session.post(URL_LOGIN,
                           params=params,
                           data=data,
                           headers=headers)
        if res.status_code != 200:
            raise Exception('Authentification failed.')

        # Check we have sso guid in cookies
        if 'GARMIN-SSO-GUID' not in session.cookies:
            raise Exception('Missing Garmin auth cookie')

        # Try to find the full post login url in response
        regex = 'var response_url(\s+)= (\"|\').*?ticket=(?P<ticket>[\w\-]+)(\"|\')'  # noqa
        params = {}
        matches = re.search(regex, res.text)
        if not matches:
            raise Exception('Missing service ticket')
        params['ticket'] = matches.group('ticket')
        logger.debug('Found service ticket {}'.format(params['ticket']))

        # Second auth step
        # Needs a service ticket from previous response
        headers = {
            'Host': URL_HOST_CONNECT,
        }
        res = session.get(URL_POST_LOGIN, params=params, headers=headers)
        if res.status_code != 200 and not res.history:
            raise Exception('Second auth step failed.')

        # Get Jsessionid
        res = session.get(URL_SESSION)
        res.raise_for_status()
        if 'JSESSIONID' not in session.cookies:
            raise Exception('Missing jsession auth cookie')

        # Check login
        res = session.get(URL_CHECK_LOGIN)
        garmin_user = res.json()
        username = garmin_user.get('username')
        if not res.ok or not username:
            raise Exception("Login check failed.")
        logger.info('Logged in as {}'.format(username))

        return session