mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-23 10:51:24 -07:00
Add requests.Session like class (#37622)
* Adds requests.Session like class * py2 syntax fix * Add a few examples to the Request docstrings * Add helper methods and docs * Fix test failures * Switch tests to test Request instead of open_url, add simple open_url test to validate funcitonality * Fix filename in replace-urlopen code smell test
This commit is contained in:
parent
c86fd6e2df
commit
d4930e6692
3 changed files with 440 additions and 172 deletions
|
@ -816,6 +816,311 @@ def maybe_add_ssl_handler(url, validate_certs):
|
|||
return SSLValidationHandler(hostname, port)
|
||||
|
||||
|
||||
class Request:
|
||||
def __init__(self, headers=None, use_proxy=True, force=False, timeout=10, validate_certs=True,
|
||||
url_username=None, url_password=None, http_agent=None, force_basic_auth=False,
|
||||
follow_redirects='urllib2', client_cert=None, client_key=None, cookies=None):
|
||||
"""This class works somewhat similarly to the ``Session`` class of from requests
|
||||
by defining a cookiejar that an be used across requests as well as cascaded defaults that
|
||||
can apply to repeated requests
|
||||
|
||||
For documentation of params, see ``Request.open``
|
||||
|
||||
>>> from ansible.module_utils.urls import Request
|
||||
>>> r = Request()
|
||||
>>> r.open('GET', 'http://httpbin.org/cookies/set?k1=v1').read()
|
||||
'{\n "cookies": {\n "k1": "v1"\n }\n}\n'
|
||||
>>> r = Request(url_username='user', url_password='passwd')
|
||||
>>> r.open('GET', 'http://httpbin.org/basic-auth/user/passwd').read()
|
||||
'{\n "authenticated": true, \n "user": "user"\n}\n'
|
||||
>>> r = Request(headers=dict(foo='bar'))
|
||||
>>> r.open('GET', 'http://httpbin.org/get', headers=dict(baz='qux')).read()
|
||||
|
||||
"""
|
||||
|
||||
self.headers = headers or {}
|
||||
if not isinstance(self.headers, dict):
|
||||
raise ValueError("headers must be a dict")
|
||||
self.use_proxy = use_proxy
|
||||
self.force = force
|
||||
self.timeout = timeout
|
||||
self.validate_certs = validate_certs
|
||||
self.url_username = url_username
|
||||
self.url_password = url_password
|
||||
self.http_agent = http_agent
|
||||
self.force_basic_auth = force_basic_auth
|
||||
self.follow_redirects = follow_redirects
|
||||
self.client_cert = client_cert
|
||||
self.client_key = client_key
|
||||
if isinstance(cookies, cookiejar.CookieJar):
|
||||
self.cookies = cookies
|
||||
else:
|
||||
self.cookies = cookiejar.CookieJar()
|
||||
|
||||
def _fallback(self, value, fallback):
|
||||
if value is None:
|
||||
return fallback
|
||||
return value
|
||||
|
||||
def open(self, method, url, data=None, headers=None, use_proxy=None,
|
||||
force=None, last_mod_time=None, timeout=None, validate_certs=None,
|
||||
url_username=None, url_password=None, http_agent=None,
|
||||
force_basic_auth=None, follow_redirects=None,
|
||||
client_cert=None, client_key=None, cookies=None):
|
||||
"""
|
||||
Sends a request via HTTP(S) or FTP using urllib2 (Python2) or urllib (Python3)
|
||||
|
||||
Does not require the module environment
|
||||
|
||||
Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg method: method for the request
|
||||
:arg url: URL to request
|
||||
|
||||
:kwarg data: (optional) bytes, or file-like object to send
|
||||
in the body of the request
|
||||
:kwarg headers: (optional) Dictionary of HTTP Headers to send with the
|
||||
request
|
||||
:kwarg use_proxy: (optional) Boolean of whether or not to use proxy
|
||||
:kwarg force: (optional) Boolean of whether or not to set `cache-control: no-cache` header
|
||||
:kwarg last_mod_time: (optional) Datetime object to use when setting If-Modified-Since header
|
||||
:kwarg timeout: (optional) How long to wait for the server to send
|
||||
data before giving up, as a float
|
||||
:kwarg validate_certs: (optional) Booleani that controls whether we verify
|
||||
the server's TLS certificate
|
||||
:kwarg url_username: (optional) String of the user to use when authenticating
|
||||
:kwarg url_password: (optional) String of the password to use when authenticating
|
||||
:kwarg http_agent: (optional) String of the User-Agent to use in the request
|
||||
:kwarg force_basic_auth: (optional) Boolean determining if auth header should be sent in the initial request
|
||||
:kwarg follow_redirects: (optional) String of urllib2, all/yes, safe, none to determine how redirects are
|
||||
followed, see RedirectHandlerFactory for more information
|
||||
:kwarg client_cert: (optional) PEM formatted certificate chain file to be used for SSL client authentication.
|
||||
This file can also include the key as well, and if the key is included, client_key is not required
|
||||
:kwarg client_key: (optional) PEM formatted file that contains your private key to be used for SSL client
|
||||
authentication. If client_cert contains both the certificate and key, this option is not required
|
||||
:kwarg cookies: (optional) CookieJar object to send with the
|
||||
request
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
method = method.upper()
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
elif not isinstance(headers, dict):
|
||||
raise ValueError("headers must be a dict")
|
||||
headers = dict(self.headers, **headers)
|
||||
|
||||
use_proxy = self._fallback(use_proxy, self.use_proxy)
|
||||
force = self._fallback(force, self.force)
|
||||
timeout = self._fallback(timeout, self.timeout)
|
||||
validate_certs = self._fallback(validate_certs, self.validate_certs)
|
||||
url_username = self._fallback(url_username, self.url_username)
|
||||
url_password = self._fallback(url_password, self.url_password)
|
||||
http_agent = self._fallback(http_agent, self.http_agent)
|
||||
force_basic_auth = self._fallback(force_basic_auth, self.force_basic_auth)
|
||||
follow_redirects = self._fallback(follow_redirects, self.follow_redirects)
|
||||
client_cert = self._fallback(client_cert, self.client_cert)
|
||||
client_key = self._fallback(client_key, self.client_key)
|
||||
cookies = self._fallback(cookies, self.cookies)
|
||||
|
||||
handlers = []
|
||||
ssl_handler = maybe_add_ssl_handler(url, validate_certs)
|
||||
if ssl_handler:
|
||||
handlers.append(ssl_handler)
|
||||
|
||||
parsed = generic_urlparse(urlparse(url))
|
||||
if parsed.scheme != 'ftp':
|
||||
username = url_username
|
||||
|
||||
if username:
|
||||
password = url_password
|
||||
netloc = parsed.netloc
|
||||
elif '@' in parsed.netloc:
|
||||
credentials, netloc = parsed.netloc.split('@', 1)
|
||||
if ':' in credentials:
|
||||
username, password = credentials.split(':', 1)
|
||||
else:
|
||||
username = credentials
|
||||
password = ''
|
||||
|
||||
parsed_list = parsed.as_list()
|
||||
parsed_list[1] = netloc
|
||||
|
||||
# reconstruct url without credentials
|
||||
url = urlunparse(parsed_list)
|
||||
|
||||
if username and not force_basic_auth:
|
||||
passman = urllib_request.HTTPPasswordMgrWithDefaultRealm()
|
||||
|
||||
# this creates a password manager
|
||||
passman.add_password(None, netloc, username, password)
|
||||
|
||||
# because we have put None at the start it will always
|
||||
# use this username/password combination for urls
|
||||
# for which `theurl` is a super-url
|
||||
authhandler = urllib_request.HTTPBasicAuthHandler(passman)
|
||||
digest_authhandler = urllib_request.HTTPDigestAuthHandler(passman)
|
||||
|
||||
# create the AuthHandler
|
||||
handlers.append(authhandler)
|
||||
handlers.append(digest_authhandler)
|
||||
|
||||
elif username and force_basic_auth:
|
||||
headers["Authorization"] = basic_auth_header(username, password)
|
||||
|
||||
else:
|
||||
try:
|
||||
rc = netrc.netrc(os.environ.get('NETRC'))
|
||||
login = rc.authenticators(parsed.hostname)
|
||||
except IOError:
|
||||
login = None
|
||||
|
||||
if login:
|
||||
username, _, password = login
|
||||
if username and password:
|
||||
headers["Authorization"] = basic_auth_header(username, password)
|
||||
|
||||
if not use_proxy:
|
||||
proxyhandler = urllib_request.ProxyHandler({})
|
||||
handlers.append(proxyhandler)
|
||||
|
||||
if HAS_SSLCONTEXT and not validate_certs:
|
||||
# In 2.7.9, the default context validates certificates
|
||||
context = SSLContext(ssl.PROTOCOL_SSLv23)
|
||||
if ssl.OP_NO_SSLv2:
|
||||
context.options |= ssl.OP_NO_SSLv2
|
||||
context.options |= ssl.OP_NO_SSLv3
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
context.check_hostname = False
|
||||
handlers.append(HTTPSClientAuthHandler(client_cert=client_cert,
|
||||
client_key=client_key,
|
||||
context=context))
|
||||
elif client_cert:
|
||||
handlers.append(HTTPSClientAuthHandler(client_cert=client_cert,
|
||||
client_key=client_key))
|
||||
|
||||
# pre-2.6 versions of python cannot use the custom https
|
||||
# handler, since the socket class is lacking create_connection.
|
||||
# Some python builds lack HTTPS support.
|
||||
if hasattr(socket, 'create_connection') and CustomHTTPSHandler:
|
||||
handlers.append(CustomHTTPSHandler)
|
||||
|
||||
handlers.append(RedirectHandlerFactory(follow_redirects, validate_certs))
|
||||
|
||||
# add some nicer cookie handling
|
||||
if cookies is not None:
|
||||
handlers.append(urllib_request.HTTPCookieProcessor(cookies))
|
||||
|
||||
opener = urllib_request.build_opener(*handlers)
|
||||
urllib_request.install_opener(opener)
|
||||
|
||||
data = to_bytes(data, nonstring='passthru')
|
||||
if method not in ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'PATCH'):
|
||||
raise ConnectionError('invalid HTTP request method; %s' % method)
|
||||
request = RequestWithMethod(url, method, data)
|
||||
|
||||
# add the custom agent header, to help prevent issues
|
||||
# with sites that block the default urllib agent string
|
||||
if http_agent:
|
||||
request.add_header('User-agent', http_agent)
|
||||
|
||||
# Cache control
|
||||
# Either we directly force a cache refresh
|
||||
if force:
|
||||
request.add_header('cache-control', 'no-cache')
|
||||
# or we do it if the original is more recent than our copy
|
||||
elif last_mod_time:
|
||||
tstamp = last_mod_time.strftime('%a, %d %b %Y %H:%M:%S +0000')
|
||||
request.add_header('If-Modified-Since', tstamp)
|
||||
|
||||
# user defined headers now, which may override things we've set above
|
||||
for header in headers:
|
||||
request.add_header(header, headers[header])
|
||||
|
||||
urlopen_args = [request, None]
|
||||
if sys.version_info >= (2, 6, 0):
|
||||
# urlopen in python prior to 2.6.0 did not
|
||||
# have a timeout parameter
|
||||
urlopen_args.append(timeout)
|
||||
|
||||
r = urllib_request.urlopen(*urlopen_args)
|
||||
return r
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
r"""Sends a GET request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('GET', url, **kwargs)
|
||||
|
||||
def options(self, url, **kwargs):
|
||||
r"""Sends a OPTIONS request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('OPTIONS', url, **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
r"""Sends a HEAD request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('HEAD', url, **kwargs)
|
||||
|
||||
def post(self, url, data=None, **kwargs):
|
||||
r"""Sends a POST request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request.
|
||||
:kwarg data: (optional) bytes, or file-like object to send in the body of the request.
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('POST', url, data=data, **kwargs)
|
||||
|
||||
def put(self, url, data=None, **kwargs):
|
||||
r"""Sends a PUT request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request.
|
||||
:kwarg data: (optional) bytes, or file-like object to send in the body of the request.
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('PUT', url, data=data, **kwargs)
|
||||
|
||||
def patch(self, url, data=None, **kwargs):
|
||||
r"""Sends a PATCH request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request.
|
||||
:kwarg data: (optional) bytes, or file-like object to send in the body of the request.
|
||||
:kwarg \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('PATCH', url, data=data, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
r"""Sends a DELETE request. Returns :class:`HTTPResponse` object.
|
||||
|
||||
:arg url: URL to request
|
||||
:kwargs \*\*kwargs: Optional arguments that ``open`` takes.
|
||||
:returns: HTTPResponse
|
||||
"""
|
||||
|
||||
return self.open('DELETE', url, **kwargs)
|
||||
|
||||
|
||||
def open_url(url, data=None, headers=None, method=None, use_proxy=True,
|
||||
force=False, last_mod_time=None, timeout=10, validate_certs=True,
|
||||
url_username=None, url_password=None, http_agent=None,
|
||||
|
@ -826,137 +1131,13 @@ def open_url(url, data=None, headers=None, method=None, use_proxy=True,
|
|||
|
||||
Does not require the module environment
|
||||
'''
|
||||
handlers = []
|
||||
ssl_handler = maybe_add_ssl_handler(url, validate_certs)
|
||||
if ssl_handler:
|
||||
handlers.append(ssl_handler)
|
||||
method = method or 'GET'
|
||||
return Request().open(method, url, data=data, headers=headers, use_proxy=use_proxy,
|
||||
force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs,
|
||||
url_username=url_username, url_password=url_password, http_agent=http_agent,
|
||||
force_basic_auth=force_basic_auth, follow_redirects=follow_redirects,
|
||||
client_cert=client_cert, client_key=client_key, cookies=cookies)
|
||||
|
||||
parsed = generic_urlparse(urlparse(url))
|
||||
if parsed.scheme != 'ftp':
|
||||
username = url_username
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if username:
|
||||
password = url_password
|
||||
netloc = parsed.netloc
|
||||
elif '@' in parsed.netloc:
|
||||
credentials, netloc = parsed.netloc.split('@', 1)
|
||||
if ':' in credentials:
|
||||
username, password = credentials.split(':', 1)
|
||||
else:
|
||||
username = credentials
|
||||
password = ''
|
||||
|
||||
parsed_list = parsed.as_list()
|
||||
parsed_list[1] = netloc
|
||||
|
||||
# reconstruct url without credentials
|
||||
url = urlunparse(parsed_list)
|
||||
|
||||
if username and not force_basic_auth:
|
||||
passman = urllib_request.HTTPPasswordMgrWithDefaultRealm()
|
||||
|
||||
# this creates a password manager
|
||||
passman.add_password(None, netloc, username, password)
|
||||
|
||||
# because we have put None at the start it will always
|
||||
# use this username/password combination for urls
|
||||
# for which `theurl` is a super-url
|
||||
authhandler = urllib_request.HTTPBasicAuthHandler(passman)
|
||||
digest_authhandler = urllib_request.HTTPDigestAuthHandler(passman)
|
||||
|
||||
# create the AuthHandler
|
||||
handlers.append(authhandler)
|
||||
handlers.append(digest_authhandler)
|
||||
|
||||
elif username and force_basic_auth:
|
||||
headers["Authorization"] = basic_auth_header(username, password)
|
||||
|
||||
else:
|
||||
try:
|
||||
rc = netrc.netrc(os.environ.get('NETRC'))
|
||||
login = rc.authenticators(parsed.hostname)
|
||||
except IOError:
|
||||
login = None
|
||||
|
||||
if login:
|
||||
username, _, password = login
|
||||
if username and password:
|
||||
headers["Authorization"] = basic_auth_header(username, password)
|
||||
|
||||
if not use_proxy:
|
||||
proxyhandler = urllib_request.ProxyHandler({})
|
||||
handlers.append(proxyhandler)
|
||||
|
||||
if HAS_SSLCONTEXT and not validate_certs:
|
||||
# In 2.7.9, the default context validates certificates
|
||||
context = SSLContext(ssl.PROTOCOL_SSLv23)
|
||||
if ssl.OP_NO_SSLv2:
|
||||
context.options |= ssl.OP_NO_SSLv2
|
||||
context.options |= ssl.OP_NO_SSLv3
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
context.check_hostname = False
|
||||
handlers.append(HTTPSClientAuthHandler(client_cert=client_cert,
|
||||
client_key=client_key,
|
||||
context=context))
|
||||
elif client_cert:
|
||||
handlers.append(HTTPSClientAuthHandler(client_cert=client_cert,
|
||||
client_key=client_key))
|
||||
|
||||
# pre-2.6 versions of python cannot use the custom https
|
||||
# handler, since the socket class is lacking create_connection.
|
||||
# Some python builds lack HTTPS support.
|
||||
if hasattr(socket, 'create_connection') and CustomHTTPSHandler:
|
||||
handlers.append(CustomHTTPSHandler)
|
||||
|
||||
handlers.append(RedirectHandlerFactory(follow_redirects, validate_certs))
|
||||
|
||||
# add some nicer cookie handling
|
||||
if cookies is not None:
|
||||
handlers.append(urllib_request.HTTPCookieProcessor(cookies))
|
||||
|
||||
opener = urllib_request.build_opener(*handlers)
|
||||
urllib_request.install_opener(opener)
|
||||
|
||||
data = to_bytes(data, nonstring='passthru')
|
||||
if method:
|
||||
if method.upper() not in ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'PATCH'):
|
||||
raise ConnectionError('invalid HTTP request method; %s' % method.upper())
|
||||
request = RequestWithMethod(url, method.upper(), data)
|
||||
else:
|
||||
request = urllib_request.Request(url, data)
|
||||
|
||||
# add the custom agent header, to help prevent issues
|
||||
# with sites that block the default urllib agent string
|
||||
if http_agent:
|
||||
request.add_header('User-agent', http_agent)
|
||||
|
||||
# Cache control
|
||||
# Either we directly force a cache refresh
|
||||
if force:
|
||||
request.add_header('cache-control', 'no-cache')
|
||||
# or we do it if the original is more recent than our copy
|
||||
elif last_mod_time:
|
||||
tstamp = last_mod_time.strftime('%a, %d %b %Y %H:%M:%S +0000')
|
||||
request.add_header('If-Modified-Since', tstamp)
|
||||
|
||||
# user defined headers now, which may override things we've set above
|
||||
if headers:
|
||||
if not isinstance(headers, dict):
|
||||
raise ValueError("headers provided to open_url() must be a dict")
|
||||
for header in headers:
|
||||
request.add_header(header, headers[header])
|
||||
|
||||
urlopen_args = [request, None]
|
||||
if sys.version_info >= (2, 6, 0):
|
||||
# urlopen in python prior to 2.6.0 did not
|
||||
# have a timeout parameter
|
||||
urlopen_args.append(timeout)
|
||||
|
||||
r = urllib_request.urlopen(*urlopen_args)
|
||||
return r
|
||||
|
||||
#
|
||||
# Module-related functions
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue