From 337d32124274b3f5e68251e58d7b6526139510df Mon Sep 17 00:00:00 2001 From: cread Date: Mon, 31 Aug 2015 15:16:30 -0500 Subject: [PATCH 1/2] Replace urllib2 with requests, add an option to disable SSL cert verification --- chef/api.py | 36 ++++++++++++------------------------ setup.py | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/chef/api.py b/chef/api.py index b79ffd8..a82bd03 100644 --- a/chef/api.py +++ b/chef/api.py @@ -1,4 +1,3 @@ -import six import datetime import logging import os @@ -6,13 +5,13 @@ import socket import subprocess import threading -import six.moves.urllib.request -import six.moves.urllib.error -import six.moves.urllib.parse import weakref +import six import pkg_resources +import requests + from chef.auth import sign_request from chef.exceptions import ChefServerError from chef.rsa import Key @@ -38,19 +37,6 @@ class UnknownRubyExpression(Exception): """Token exception for unprocessed Ruby expressions.""" -class ChefRequest(six.moves.urllib.request.Request): - """Workaround for using PUT/DELETE with urllib2.""" - def __init__(self, *args, **kwargs): - self._method = kwargs.pop('method', None) - # Request is an old-style class, no super() allowed. - six.moves.urllib.request.Request.__init__(self, *args, **kwargs) - - def get_method(self): - if self._method: - return self._method - return six.moves.urllib.request.Request.get_method(self) - - class ChefAPI(object): """The ChefAPI object is a wrapper for a single Chef server. @@ -70,6 +56,8 @@ class ChefAPI(object): env_value_re = re.compile(r'ENV\[(.+)\]') ruby_string_re = re.compile(r'^\s*(["\'])(.*?)\1\s*$') + verify_ssl = True + def __init__(self, url, key, client, version='0.10.8', headers={}): self.url = url.rstrip('/') self.parsed_url = six.moves.urllib.parse.urlparse(self.url) @@ -192,11 +180,8 @@ def __exit__(self, type, value, traceback): del api_stack_value()[-1] def _request(self, method, url, data, headers): - # Testing hook, subclass and override for WSGI intercept - if six.PY3 and data: - data = data.encode() - request = ChefRequest(url, data, headers, method=method) - return six.moves.urllib.request.urlopen(request).read() + request = requests.api.request(method, url, headers=headers, data=data, verify=self.verify_ssl) + return request def request(self, method, path, headers={}, data=None): auth_headers = sign_request(key=self.key, http_method=method, @@ -228,13 +213,13 @@ def api_request(self, method, path, headers={}, data=None): headers['content-type'] = 'application/json' data = json.dumps(data) response = self.request(method, path, headers, data) - return json.loads(response.decode()) + return response.json() def __getitem__(self, path): return self.api_request('GET', path) -def autoconfigure(base_path=None): +def autoconfigure(base_path=None, verify_ssl=True): """Try to find a knife or chef-client config file to load parameters from, starting from either the given base path or the current working directory. @@ -253,16 +238,19 @@ def autoconfigure(base_path=None): config_path = os.path.join(path, '.chef', 'knife.rb') api = ChefAPI.from_config_file(config_path) if api is not None: + api.verify_ssl = verify_ssl return api # The walk didn't work, try ~/.chef/knife.rb config_path = os.path.expanduser(os.path.join('~', '.chef', 'knife.rb')) api = ChefAPI.from_config_file(config_path) if api is not None: + api.verify_ssl = verify_ssl return api # Nothing in the home dir, try /etc/chef/client.rb config_path = os.path.join(os.path.sep, 'etc', 'chef', 'client.rb') api = ChefAPI.from_config_file(config_path) if api is not None: + api.verify_ssl = verify_ssl return api diff --git a/setup.py b/setup.py index 9a774f2..47ee477 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ 'Programming Language :: Python', ], zip_safe = False, - install_requires = ['six>=1.9.0'], + install_requires = ['six>=1.9.0','requests>=2.7.0'], tests_require = ['unittest2', 'mock'], test_suite = 'unittest2.collector', ) From 28d95011f80fdf25316955ea9e7c7e32f6553fc7 Mon Sep 17 00:00:00 2001 From: cread Date: Tue, 1 Sep 2015 09:22:51 -0500 Subject: [PATCH 2/2] Base SSL verification on ssl_verify_mode option in knife.rb --- chef/api.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/chef/api.py b/chef/api.py index a82bd03..3aaaedd 100644 --- a/chef/api.py +++ b/chef/api.py @@ -56,9 +56,7 @@ class ChefAPI(object): env_value_re = re.compile(r'ENV\[(.+)\]') ruby_string_re = re.compile(r'^\s*(["\'])(.*?)\1\s*$') - verify_ssl = True - - def __init__(self, url, key, client, version='0.10.8', headers={}): + def __init__(self, url, key, client, version='0.10.8', headers={}, ssl_verify=True): self.url = url.rstrip('/') self.parsed_url = six.moves.urllib.parse.urlparse(self.url) if not isinstance(key, Key): @@ -71,6 +69,7 @@ def __init__(self, url, key, client, version='0.10.8', headers={}): self.headers = dict((k.lower(), v) for k, v in six.iteritems(headers)) self.version_parsed = pkg_resources.parse_version(self.version) self.platform = self.parsed_url.hostname == 'api.opscode.com' + self.ssl_verify = ssl_verify if not api_stack_value(): self.set_default() @@ -85,6 +84,7 @@ def from_config_file(cls, path): log.debug('Unable to read config file "%s"', path) return url = key_path = client_name = None + ssl_verify = True for line in open(path): if not line.strip() or line.startswith('#'): continue # Skip blanks and comments @@ -95,6 +95,10 @@ def from_config_file(cls, path): md = cls.ruby_string_re.search(value) if md: value = md.group(2) + elif key == 'ssl_verify_mode': + log.debug('Found ssl_verify_mode: %r', value) + ssl_verify = (value.strip() != ':verify_none') + log.debug('ssl_verify = %s', ssl_verify) else: # Not a string, don't even try log.debug('Value for {0} does not look like a string: {1}'.format(key, value)) @@ -125,6 +129,7 @@ def _ruby_value(match): if not os.path.isabs(key_path): # Relative paths are relative to the config file key_path = os.path.abspath(os.path.join(os.path.dirname(path), key_path)) + if not (url and client_name and key_path): # No URL, no chance this was valid, try running Ruby log.debug('No Chef server config found, trying Ruby parse') @@ -153,7 +158,7 @@ def _ruby_value(match): return if not client_name: client_name = socket.getfqdn() - return cls(url, key_path, client_name) + return cls(url, key_path, client_name, ssl_verify=ssl_verify) @staticmethod def get_global(): @@ -180,7 +185,7 @@ def __exit__(self, type, value, traceback): del api_stack_value()[-1] def _request(self, method, url, data, headers): - request = requests.api.request(method, url, headers=headers, data=data, verify=self.verify_ssl) + request = requests.api.request(method, url, headers=headers, data=data, verify=self.ssl_verify) return request def request(self, method, path, headers={}, data=None): @@ -219,7 +224,7 @@ def __getitem__(self, path): return self.api_request('GET', path) -def autoconfigure(base_path=None, verify_ssl=True): +def autoconfigure(base_path=None): """Try to find a knife or chef-client config file to load parameters from, starting from either the given base path or the current working directory. @@ -238,19 +243,16 @@ def autoconfigure(base_path=None, verify_ssl=True): config_path = os.path.join(path, '.chef', 'knife.rb') api = ChefAPI.from_config_file(config_path) if api is not None: - api.verify_ssl = verify_ssl return api # The walk didn't work, try ~/.chef/knife.rb config_path = os.path.expanduser(os.path.join('~', '.chef', 'knife.rb')) api = ChefAPI.from_config_file(config_path) if api is not None: - api.verify_ssl = verify_ssl return api # Nothing in the home dir, try /etc/chef/client.rb config_path = os.path.join(os.path.sep, 'etc', 'chef', 'client.rb') api = ChefAPI.from_config_file(config_path) if api is not None: - api.verify_ssl = verify_ssl return api