diff --git a/plugin/packages/wakatime/__about__.py b/plugin/packages/wakatime/__about__.py index 3f6cd7e..2b2b185 100644 --- a/plugin/packages/wakatime/__about__.py +++ b/plugin/packages/wakatime/__about__.py @@ -1,7 +1,7 @@ __title__ = 'wakatime' __description__ = 'Common interface to the WakaTime api.' __url__ = 'https://github.com/wakatime/wakatime' -__version_info__ = ('8', '0', '0') +__version_info__ = ('8', '0', '2') __version__ = '.'.join(__version_info__) __author__ = 'Alan Hamlett' __author_email__ = 'alan@wakatime.com' diff --git a/plugin/packages/wakatime/arguments.py b/plugin/packages/wakatime/arguments.py index 0fbe7f0..94920f3 100644 --- a/plugin/packages/wakatime/arguments.py +++ b/plugin/packages/wakatime/arguments.py @@ -76,6 +76,10 @@ def parseArguments(): 'https://user:pass@host:port or '+ 'socks5://user:pass@host:port or ' + 'domain\\user:pass') + parser.add_argument('--no-ssl-verify', dest='nosslverify', + action='store_true', + help='disables SSL certificate verification for HTTPS '+ + 'requests. By default, SSL certificates are verified.') parser.add_argument('--project', dest='project', help='optional project name') parser.add_argument('--alternate-project', dest='alternate_project', @@ -214,6 +218,8 @@ def parseArguments(): 'https://user:pass@host:port or ' + 'socks5://user:pass@host:port or ' + 'domain\\user:pass.') + if configs.has_option('settings', 'no_ssl_verify'): + args.nosslverify = configs.getboolean('settings', 'no_ssl_verify') if not args.verbose and configs.has_option('settings', 'verbose'): args.verbose = configs.getboolean('settings', 'verbose') if not args.verbose and configs.has_option('settings', 'debug'): diff --git a/plugin/packages/wakatime/configs.py b/plugin/packages/wakatime/configs.py index 9ff9ec0..a4b2d18 100644 --- a/plugin/packages/wakatime/configs.py +++ b/plugin/packages/wakatime/configs.py @@ -25,6 +25,22 @@ except ImportError: from .packages import configparser +def getConfigFile(): + """Returns the config file location. + + If $WAKATIME_HOME env varialbe is defined, returns + $WAKATIME_HOME/.wakatime.cfg, otherwise ~/.wakatime.cfg. + """ + + fileName = '.wakatime.cfg' + + home = os.environ.get('WAKATIME_HOME') + if home: + return os.path.join(os.path.expanduser(home), fileName) + + return os.path.join(os.path.expanduser('~'), fileName) + + def parseConfigFile(configFile=None): """Returns a configparser.SafeConfigParser instance with configs read from the config file. Default location of the config file is @@ -32,13 +48,8 @@ def parseConfigFile(configFile=None): """ # get config file location from ENV - home = os.environ.get('WAKATIME_HOME') - if not configFile and home: - configFile = os.path.join(os.path.expanduser(home), '.wakatime.cfg') - - # use default config file location if not configFile: - configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg') + configFile = getConfigFile() configs = configparser.ConfigParser(delimiters=('='), strict=False) try: diff --git a/plugin/packages/wakatime/constants.py b/plugin/packages/wakatime/constants.py index fece2b9..3e1905f 100644 --- a/plugin/packages/wakatime/constants.py +++ b/plugin/packages/wakatime/constants.py @@ -38,3 +38,15 @@ UNKNOWN_ERROR = 105 Exit code used when the JSON input from `--extra-heartbeats` is malformed. """ MALFORMED_HEARTBEAT_ERROR = 106 + +""" Connection Error +Exit code used when there was proxy or other problem connecting to the WakaTime +API servers. +""" +CONNECTION_ERROR = 107 + +""" Max file size supporting line number count stats. +Files larger than this in bytes will not have a line count stat for performance. +Default is 2MB. +""" +MAX_FILE_SIZE_SUPPORTED = 2000000 diff --git a/plugin/packages/wakatime/main.py b/plugin/packages/wakatime/main.py index 136ff6e..109bf7f 100644 --- a/plugin/packages/wakatime/main.py +++ b/plugin/packages/wakatime/main.py @@ -34,8 +34,19 @@ from .constants import ( MALFORMED_HEARTBEAT_ERROR, ) from .logger import setup_logging + +log = logging.getLogger('WakaTime') + +try: + from .packages import requests +except ImportError: + log.traceback(logging.ERROR) + print(traceback.format_exc()) + log.error('Please upgrade Python to the latest version.') + print('Please upgrade Python to the latest version.') + sys.exit(UNKNOWN_ERROR) + from .offlinequeue import Queue -from .packages import requests from .packages.requests.exceptions import RequestException from .project import get_project_info from .session_cache import SessionCache @@ -48,13 +59,11 @@ except (ImportError, SyntaxError): # pragma: nocover from .packages import tzlocal -log = logging.getLogger('WakaTime') - - def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, entity=None, timestamp=None, is_write=None, plugin=None, offline=None, entity_type='file', hidefilenames=None, - proxy=None, api_url=None, timeout=None, **kwargs): + proxy=None, nosslverify=None, api_url=None, timeout=None, + use_ntlm_proxy=False, **kwargs): """Sends heartbeat as POST request to WakaTime api server. Returns `SUCCESS` when heartbeat was sent, otherwise returns an @@ -126,9 +135,10 @@ def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, session_cache = SessionCache() session = session_cache.get() + should_try_ntlm = False proxies = {} if proxy: - if '\\' in proxy: + if use_ntlm_proxy: from .packages.requests_ntlm import HttpNtlmAuth username = proxy.rsplit(':', 1) password = '' @@ -137,37 +147,80 @@ def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, username = username[0] session.auth = HttpNtlmAuth(username, password, session) else: + should_try_ntlm = '\\' in proxy proxies['https'] = proxy - # log time to api + # send request to api response = None try: response = session.post(api_url, data=request_body, headers=headers, - proxies=proxies, timeout=timeout) + proxies=proxies, timeout=timeout, + verify=not nosslverify) except RequestException: - exception_data = { - sys.exc_info()[0].__name__: u(sys.exc_info()[1]), - } - if log.isEnabledFor(logging.DEBUG): - exception_data['traceback'] = traceback.format_exc() - if offline: - queue = Queue() - queue.push(data, json.dumps(stats), plugin) - if log.isEnabledFor(logging.DEBUG): - log.warn(exception_data) + if should_try_ntlm: + return send_heartbeat( + project=project, + entity=entity, + timestamp=timestamp, + branch=branch, + hostname=hostname, + stats=stats, + key=key, + is_write=is_write, + plugin=plugin, + offline=offline, + hidefilenames=hidefilenames, + entity_type=entity_type, + proxy=proxy, + api_url=api_url, + timeout=timeout, + use_ntlm_proxy=True, + ) else: - log.error(exception_data) + exception_data = { + sys.exc_info()[0].__name__: u(sys.exc_info()[1]), + } + if log.isEnabledFor(logging.DEBUG): + exception_data['traceback'] = traceback.format_exc() + if offline: + queue = Queue() + queue.push(data, json.dumps(stats), plugin) + if log.isEnabledFor(logging.DEBUG): + log.warn(exception_data) + else: + log.error(exception_data) except: # delete cached session when requests raises unknown exception - exception_data = { - sys.exc_info()[0].__name__: u(sys.exc_info()[1]), - 'traceback': traceback.format_exc(), - } - if offline: - queue = Queue() - queue.push(data, json.dumps(stats), plugin) - log.warn(exception_data) - session_cache.delete() + if should_try_ntlm: + return send_heartbeat( + project=project, + entity=entity, + timestamp=timestamp, + branch=branch, + hostname=hostname, + stats=stats, + key=key, + is_write=is_write, + plugin=plugin, + offline=offline, + hidefilenames=hidefilenames, + entity_type=entity_type, + proxy=proxy, + api_url=api_url, + timeout=timeout, + use_ntlm_proxy=True, + ) + else: + exception_data = { + sys.exc_info()[0].__name__: u(sys.exc_info()[1]), + 'traceback': traceback.format_exc(), + } + if offline: + queue = Queue() + queue.push(data, json.dumps(stats), plugin) + log.warn(exception_data) + session_cache.delete() + return API_ERROR else: code = response.status_code if response is not None else None @@ -178,32 +231,52 @@ def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None, }) session_cache.save(session) return SUCCESS - if offline: - if code != 400: - queue = Queue() - queue.push(data, json.dumps(stats), plugin) - if code == 401: + if should_try_ntlm: + return send_heartbeat( + project=project, + entity=entity, + timestamp=timestamp, + branch=branch, + hostname=hostname, + stats=stats, + key=key, + is_write=is_write, + plugin=plugin, + offline=offline, + hidefilenames=hidefilenames, + entity_type=entity_type, + proxy=proxy, + api_url=api_url, + timeout=timeout, + use_ntlm_proxy=True, + ) + else: + if offline: + if code != 400: + queue = Queue() + queue.push(data, json.dumps(stats), plugin) + if code == 401: + log.error({ + 'response_code': code, + 'response_content': content, + }) + session_cache.delete() + return AUTH_ERROR + elif log.isEnabledFor(logging.DEBUG): + log.warn({ + 'response_code': code, + 'response_content': content, + }) + else: log.error({ 'response_code': code, 'response_content': content, }) - session_cache.delete() - return AUTH_ERROR - elif log.isEnabledFor(logging.DEBUG): - log.warn({ - 'response_code': code, - 'response_content': content, - }) else: log.error({ 'response_code': code, 'response_content': content, }) - else: - log.error({ - 'response_code': code, - 'response_content': content, - }) session_cache.delete() return API_ERROR @@ -278,6 +351,7 @@ def process_heartbeat(args, configs, hostname, heartbeat): heartbeat['offline'] = args.offline heartbeat['hidefilenames'] = args.hidefilenames heartbeat['proxy'] = args.proxy + heartbeat['nosslverify'] = args.nosslverify heartbeat['api_url'] = args.api_url return send_heartbeat(**heartbeat) diff --git a/plugin/packages/wakatime/stats.py b/plugin/packages/wakatime/stats.py index f91ce01..e6fc3ff 100644 --- a/plugin/packages/wakatime/stats.py +++ b/plugin/packages/wakatime/stats.py @@ -15,6 +15,7 @@ import re import sys from .compat import u, open +from .constants import MAX_FILE_SIZE_SUPPORTED from .dependencies import DependencyParser from .language_priorities import LANGUAGES @@ -184,6 +185,11 @@ def get_language_from_extension(file_name): def number_lines_in_file(file_name): + try: + if os.path.getsize(file_name) > MAX_FILE_SIZE_SUPPORTED: + return None + except os.error: + pass lines = 0 try: with open(file_name, 'r', encoding='utf-8') as fh: