Compare commits

..

6 Commits

14 changed files with 133 additions and 85 deletions

View File

@ -3,6 +3,19 @@ History
------- -------
3.0.15 (2015-04-01)
+++++++++++++++++++
- obfuscate api key when logging to Sublime Text Console in debug mode
3.0.14 (2015-03-31)
+++++++++++++++++++
- always use external python binary because ST builtin python does not support checking SSL certs
- upgrade wakatime cli to v4.0.6
3.0.13 (2015-03-23) 3.0.13 (2015-03-23)
+++++++++++++++++++ +++++++++++++++++++

View File

@ -6,7 +6,9 @@ License: BSD, see LICENSE for more details.
Website: https://wakatime.com/ Website: https://wakatime.com/
===========================================================""" ==========================================================="""
__version__ = '3.0.13'
__version__ = '3.0.15'
import sublime import sublime
import sublime_plugin import sublime_plugin
@ -19,13 +21,14 @@ import time
import threading import threading
import webbrowser import webbrowser
from datetime import datetime from datetime import datetime
from os.path import expanduser, dirname, basename, realpath, join from subprocess import Popen
# globals # globals
ACTION_FREQUENCY = 2 ACTION_FREQUENCY = 2
ST_VERSION = int(sublime.version()) ST_VERSION = int(sublime.version())
PLUGIN_DIR = dirname(realpath(__file__)) PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
API_CLIENT = '%s/packages/wakatime/cli.py' % PLUGIN_DIR API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
SETTINGS_FILE = 'WakaTime.sublime-settings' SETTINGS_FILE = 'WakaTime.sublime-settings'
SETTINGS = {} SETTINGS = {}
LAST_ACTION = { LAST_ACTION = {
@ -33,27 +36,16 @@ LAST_ACTION = {
'file': None, 'file': None,
'is_write': False, 'is_write': False,
} }
HAS_SSL = False
LOCK = threading.RLock() LOCK = threading.RLock()
PYTHON_LOCATION = None PYTHON_LOCATION = None
# add wakatime package to path # add wakatime package to path
sys.path.insert(0, join(PLUGIN_DIR, 'packages')) sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
# check if we have SSL support
try: try:
import ssl from wakatime.base import parseConfigFile
import socket except ImportError:
assert ssl pass
assert socket.ssl
assert ssl.OPENSSL_VERSION
HAS_SSL = True
except (ImportError, AttributeError):
from subprocess import Popen
if HAS_SSL:
# import wakatime package so we can use built-in python
import wakatime
def createConfigFile(): def createConfigFile():
@ -81,7 +73,6 @@ def prompt_api_key():
default_key = '' default_key = ''
try: try:
from wakatime.base import parseConfigFile
configs = parseConfigFile() configs = parseConfigFile()
if configs is not None: if configs is not None:
if configs.has_option('settings', 'api_key'): if configs.has_option('settings', 'api_key'):
@ -123,7 +114,7 @@ def python_binary():
except: except:
pass pass
for path in glob.iglob('/python*'): for path in glob.iglob('/python*'):
path = realpath(join(path, 'pythonw')) path = os.path.realpath(os.path.join(path, 'pythonw'))
try: try:
Popen([path, '--version']) Popen([path, '--version'])
PYTHON_LOCATION = path PYTHON_LOCATION = path
@ -133,6 +124,17 @@ def python_binary():
return None return None
def obfuscate_apikey(cmd):
apikey_index = None
for num in range(len(cmd)):
if cmd[num] == '--key':
apikey_index = num + 1
break
if apikey_index is not None and apikey_index < len(cmd):
cmd[apikey_index] = '********-****-****-****-********' + cmd[apikey_index][-4:]
return cmd
def enough_time_passed(now, last_time): def enough_time_passed(now, last_time):
if now - last_time > ACTION_FREQUENCY * 60: if now - last_time > ACTION_FREQUENCY * 60:
return True return True
@ -151,10 +153,13 @@ def find_project_name_from_folders(folders):
def handle_action(view, is_write=False): def handle_action(view, is_write=False):
target_file = view.file_name() window = view.window()
project = view.window().project_file_name() if hasattr(view.window(), 'project_file_name') else None if window is not None:
thread = SendActionThread(target_file, view, is_write=is_write, project=project, folders=view.window().folders()) target_file = view.file_name()
thread.start() project = window.project_file_name() if hasattr(window, 'project_file_name') else None
folders = window.folders()
thread = SendActionThread(target_file, view, is_write=is_write, project=project, folders=folders)
thread.start()
class SendActionThread(threading.Thread): class SendActionThread(threading.Thread):
@ -195,7 +200,7 @@ class SendActionThread(threading.Thread):
if self.is_write: if self.is_write:
cmd.append('--write') cmd.append('--write')
if self.project: if self.project:
self.project = basename(self.project).replace('.sublime-project', '', 1) self.project = os.path.basename(self.project).replace('.sublime-project', '', 1)
if self.project: if self.project:
cmd.extend(['--project', self.project]) cmd.extend(['--project', self.project])
elif self.folders: elif self.folders:
@ -206,28 +211,18 @@ class SendActionThread(threading.Thread):
cmd.extend(['--ignore', pattern]) cmd.extend(['--ignore', pattern])
if self.debug: if self.debug:
cmd.append('--verbose') cmd.append('--verbose')
if HAS_SSL: if python_binary():
cmd.insert(0, python_binary())
if self.debug: if self.debug:
print('[WakaTime] %s' % ' '.join(cmd)) print('[WakaTime] %s' % ' '.join(obfuscate_apikey(cmd)))
code = wakatime.main(cmd) if platform.system() == 'Windows':
if code != 0: Popen(cmd, shell=False)
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
else: else:
self.sent() with open(os.path.join(os.path.expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
self.sent()
else: else:
python = python_binary() print('[WakaTime] Error: Unable to find python binary.')
if python:
cmd.insert(0, python)
if self.debug:
print('[WakaTime] %s %s' % (python, ' '.join(cmd)))
if platform.system() == 'Windows':
Popen(cmd, shell=False)
else:
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
Popen(cmd, stderr=stderr)
self.sent()
else:
print('[WakaTime] Error: Unable to find python binary.')
def sent(self): def sent(self):
sublime.set_timeout(self.set_status_bar, 0) sublime.set_timeout(self.set_status_bar, 0)
@ -250,11 +245,9 @@ def plugin_loaded():
global SETTINGS global SETTINGS
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__) print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
if not HAS_SSL: if not python_binary():
python = python_binary() sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
if not python: return
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
return
SETTINGS = sublime.load_settings(SETTINGS_FILE) SETTINGS = sublime.load_settings(SETTINGS_FILE)
after_loaded() after_loaded()

View File

@ -1,7 +1,7 @@
__title__ = 'wakatime' __title__ = 'wakatime'
__description__ = 'Common interface to the WakaTime api.' __description__ = 'Common interface to the WakaTime api.'
__url__ = 'https://github.com/wakatime/wakatime' __url__ = 'https://github.com/wakatime/wakatime'
__version_info__ = ('4', '0', '4') __version_info__ = ('4', '0', '6')
__version__ = '.'.join(__version_info__) __version__ = '.'.join(__version_info__)
__author__ = 'Alan Hamlett' __author__ = 'Alan Hamlett'
__author_email__ = 'alan@wakatime.com' __author_email__ = 'alan@wakatime.com'

View File

@ -24,8 +24,9 @@ try:
except ImportError: except ImportError:
import configparser import configparser
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages', 'requests', 'packages'))
from .__about__ import __version__ from .__about__ import __version__
from .compat import u, open, is_py3 from .compat import u, open, is_py3

View File

@ -24,6 +24,9 @@ except ImportError:
class CustomEncoder(json.JSONEncoder): class CustomEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, bytes):
obj = bytes.decode(obj)
return json.dumps(obj)
try: try:
encoded = super(CustomEncoder, self).default(obj) encoded = super(CustomEncoder, self).default(obj)
except UnicodeDecodeError: except UnicodeDecodeError:

View File

@ -42,7 +42,7 @@ is at <http://python-requests.org>.
""" """
__title__ = 'requests' __title__ = 'requests'
__version__ = '2.5.3' __version__ = '2.6.0'
__build__ = 0x020503 __build__ = 0x020503
__author__ = 'Kenneth Reitz' __author__ = 'Kenneth Reitz'
__license__ = 'Apache 2.0' __license__ = 'Apache 2.0'

View File

@ -11,10 +11,10 @@ and maintain connections.
import socket import socket
from .models import Response from .models import Response
from .packages.urllib3 import Retry
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
from .packages.urllib3.response import HTTPResponse from .packages.urllib3.response import HTTPResponse
from .packages.urllib3.util import Timeout as TimeoutSauce from .packages.urllib3.util import Timeout as TimeoutSauce
from .packages.urllib3.util.retry import Retry
from .compat import urlparse, basestring from .compat import urlparse, basestring
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers, from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
prepend_scheme_if_needed, get_auth_from_url, urldefragauth) prepend_scheme_if_needed, get_auth_from_url, urldefragauth)

View File

@ -16,7 +16,6 @@ from . import sessions
def request(method, url, **kwargs): def request(method, url, **kwargs):
"""Constructs and sends a :class:`Request <Request>`. """Constructs and sends a :class:`Request <Request>`.
Returns :class:`Response <Response>` object.
:param method: method for the new :class:`Request` object. :param method: method for the new :class:`Request` object.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
@ -37,6 +36,8 @@ def request(method, url, **kwargs):
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
:param stream: (optional) if ``False``, the response content will be immediately downloaded. :param stream: (optional) if ``False``, the response content will be immediately downloaded.
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
:return: :class:`Response <Response>` object
:rtype: requests.Response
Usage:: Usage::
@ -55,10 +56,12 @@ def request(method, url, **kwargs):
def get(url, **kwargs): def get(url, **kwargs):
"""Sends a GET request. Returns :class:`Response` object. """Sends a GET request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
kwargs.setdefault('allow_redirects', True) kwargs.setdefault('allow_redirects', True)
@ -66,10 +69,12 @@ def get(url, **kwargs):
def options(url, **kwargs): def options(url, **kwargs):
"""Sends a OPTIONS request. Returns :class:`Response` object. """Sends a OPTIONS request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
kwargs.setdefault('allow_redirects', True) kwargs.setdefault('allow_redirects', True)
@ -77,10 +82,12 @@ def options(url, **kwargs):
def head(url, **kwargs): def head(url, **kwargs):
"""Sends a HEAD request. Returns :class:`Response` object. """Sends a HEAD request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
kwargs.setdefault('allow_redirects', False) kwargs.setdefault('allow_redirects', False)
@ -88,44 +95,52 @@ def head(url, **kwargs):
def post(url, data=None, json=None, **kwargs): def post(url, data=None, json=None, **kwargs):
"""Sends a POST request. Returns :class:`Response` object. """Sends a POST request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`. :param json: (optional) json data to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
return request('post', url, data=data, json=json, **kwargs) return request('post', url, data=data, json=json, **kwargs)
def put(url, data=None, **kwargs): def put(url, data=None, **kwargs):
"""Sends a PUT request. Returns :class:`Response` object. """Sends a PUT request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
return request('put', url, data=data, **kwargs) return request('put', url, data=data, **kwargs)
def patch(url, data=None, **kwargs): def patch(url, data=None, **kwargs):
"""Sends a PATCH request. Returns :class:`Response` object. """Sends a PATCH request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
return request('patch', url, data=data, **kwargs) return request('patch', url, data=data, **kwargs)
def delete(url, **kwargs): def delete(url, **kwargs):
"""Sends a DELETE request. Returns :class:`Response` object. """Sends a DELETE request.
:param url: URL for the new :class:`Request` object. :param url: URL for the new :class:`Request` object.
:param \*\*kwargs: Optional arguments that ``request`` takes. :param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
""" """
return request('delete', url, **kwargs) return request('delete', url, **kwargs)

View File

@ -143,12 +143,13 @@ class RequestEncodingMixin(object):
else: else:
fn = guess_filename(v) or k fn = guess_filename(v) or k
fp = v fp = v
if isinstance(fp, str):
fp = StringIO(fp)
if isinstance(fp, (bytes, bytearray)):
fp = BytesIO(fp)
rf = RequestField(name=k, data=fp.read(), if isinstance(fp, (str, bytes, bytearray)):
fdata = fp
else:
fdata = fp.read()
rf = RequestField(name=k, data=fdata,
filename=fn, headers=fh) filename=fn, headers=fh)
rf.make_multipart(content_type=ft) rf.make_multipart(content_type=ft)
new_fields.append(rf) new_fields.append(rf)
@ -572,7 +573,11 @@ class Response(object):
self.cookies = cookiejar_from_dict({}) self.cookies = cookiejar_from_dict({})
#: The amount of time elapsed between sending the request #: The amount of time elapsed between sending the request
#: and the arrival of the response (as a timedelta) #: and the arrival of the response (as a timedelta).
#: This property specifically measures the time taken between sending
#: the first byte of the request and finishing parsing the headers. It
#: is therefore unaffected by consuming the response content or the
#: value of the ``stream`` keyword argument.
self.elapsed = datetime.timedelta(0) self.elapsed = datetime.timedelta(0)
#: The :class:`PreparedRequest <PreparedRequest>` object to which this #: The :class:`PreparedRequest <PreparedRequest>` object to which this

View File

@ -4,7 +4,7 @@ urllib3 - Thread-safe connection pooling and re-using.
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' __author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
__license__ = 'MIT' __license__ = 'MIT'
__version__ = 'dev' __version__ = '1.10.2'
from .connectionpool import ( from .connectionpool import (

View File

@ -20,8 +20,6 @@ from .packages.six import iterkeys, itervalues, PY3
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] __all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
MULTIPLE_HEADERS_ALLOWED = frozenset(['cookie', 'set-cookie', 'set-cookie2'])
_Null = object() _Null = object()
@ -143,7 +141,10 @@ class HTTPHeaderDict(dict):
def __init__(self, headers=None, **kwargs): def __init__(self, headers=None, **kwargs):
dict.__init__(self) dict.__init__(self)
if headers is not None: if headers is not None:
self.extend(headers) if isinstance(headers, HTTPHeaderDict):
self._copy_from(headers)
else:
self.extend(headers)
if kwargs: if kwargs:
self.extend(kwargs) self.extend(kwargs)
@ -223,11 +224,8 @@ class HTTPHeaderDict(dict):
vals.append(val) vals.append(val)
else: else:
# vals should be a tuple then, i.e. only one item so far # vals should be a tuple then, i.e. only one item so far
if key_lower in MULTIPLE_HEADERS_ALLOWED: # Need to convert the tuple to list for further extension
# Need to convert the tuple to list for further extension _dict_setitem(self, key_lower, [vals[0], vals[1], val])
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
else:
_dict_setitem(self, key_lower, new_vals)
def extend(*args, **kwargs): def extend(*args, **kwargs):
"""Generic import function for any type of header-like object. """Generic import function for any type of header-like object.
@ -276,14 +274,17 @@ class HTTPHeaderDict(dict):
def __repr__(self): def __repr__(self):
return "%s(%s)" % (type(self).__name__, dict(self.itermerged())) return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
def copy(self): def _copy_from(self, other):
clone = type(self)() for key in other:
for key in self: val = _dict_getitem(other, key)
val = _dict_getitem(self, key)
if isinstance(val, list): if isinstance(val, list):
# Don't need to convert tuples # Don't need to convert tuples
val = list(val) val = list(val)
_dict_setitem(clone, key, val) _dict_setitem(self, key, val)
def copy(self):
clone = type(self)()
clone._copy_from(self)
return clone return clone
def iteritems(self): def iteritems(self):

View File

@ -157,3 +157,8 @@ class InsecureRequestWarning(SecurityWarning):
class SystemTimeWarning(SecurityWarning): class SystemTimeWarning(SecurityWarning):
"Warned when system time is suspected to be wrong" "Warned when system time is suspected to be wrong"
pass pass
class InsecurePlatformWarning(SecurityWarning):
"Warned when certain SSL configuration is not available on a platform."
pass

View File

@ -1,7 +1,7 @@
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from hashlib import md5, sha1, sha256 from hashlib import md5, sha1, sha256
from ..exceptions import SSLError from ..exceptions import SSLError, InsecurePlatformWarning
SSLContext = None SSLContext = None
@ -10,6 +10,7 @@ create_default_context = None
import errno import errno
import ssl import ssl
import warnings
try: # Test for SSL features try: # Test for SSL features
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23 from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
@ -69,6 +70,14 @@ except ImportError:
self.ciphers = cipher_suite self.ciphers = cipher_suite
def wrap_socket(self, socket, server_hostname=None): def wrap_socket(self, socket, server_hostname=None):
warnings.warn(
'A true SSLContext object is not available. This prevents '
'urllib3 from configuring SSL appropriately and may cause '
'certain SSL connections to fail. For more information, see '
'https://urllib3.readthedocs.org/en/latest/security.html'
'#insecureplatformwarning.',
InsecurePlatformWarning
)
kwargs = { kwargs = {
'keyfile': self.keyfile, 'keyfile': self.keyfile,
'certfile': self.certfile, 'certfile': self.certfile,

View File

@ -171,7 +171,10 @@ class SessionRedirectMixin(object):
except KeyError: except KeyError:
pass pass
extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw) # Extract any cookies sent on the response to the cookiejar
# in the new request. Because we've mutated our copied prepared
# request, use the old one that we haven't yet touched.
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
prepared_request._cookies.update(self.cookies) prepared_request._cookies.update(self.cookies)
prepared_request.prepare_cookies(prepared_request._cookies) prepared_request.prepare_cookies(prepared_request._cookies)