diff --git a/lib/ansible/module_utils/urls.py b/lib/ansible/module_utils/urls.py index 41613f6cb6..a930483463 100644 --- a/lib/ansible/module_utils/urls.py +++ b/lib/ansible/module_utils/urls.py @@ -417,7 +417,7 @@ class RequestWithMethod(urllib2.Request): def __init__(self, url, method, data=None, headers=None): if headers is None: headers = {} - self._method = method + self._method = method.upper() urllib2.Request.__init__(self, url, data, headers) def get_method(self): @@ -427,6 +427,55 @@ class RequestWithMethod(urllib2.Request): return urllib2.Request.get_method(self) +def RedirectHandlerFactory(follow_redirects=None): + """This is a class factory that closes over the value of + ``follow_redirects`` so that the RedirectHandler class has access to + that value without having to use globals, and potentially cause problems + where ``open_url`` or ``fetch_url`` are used multiple times in a module. + """ + + class RedirectHandler(urllib2.HTTPRedirectHandler): + """This is an implementation of a RedirectHandler to match the + functionality provided by httplib2. It will utilize the value of + ``follow_redirects`` that is passed into ``RedirectHandlerFactory`` + to determine how redirects should be handled in urllib2. + """ + + def redirect_request(self, req, fp, code, msg, hdrs, newurl): + if follow_redirects == 'urllib2': + return urllib2.HTTPRedirectHandler.redirect_request(self, req, + fp, code, + msg, hdrs, + newurl) + + if follow_redirects in [None, 'no', 'none']: + raise urllib2.HTTPError(newurl, code, msg, hdrs, fp) + + do_redirect = False + if follow_redirects in ['all', 'yes']: + do_redirect = (code >= 300 and code < 400) + + elif follow_redirects == 'safe': + m = req.get_method() + do_redirect = (code >= 300 and code < 400 and m in ('GET', 'HEAD')) + + if do_redirect: + # be conciliant with URIs containing a space + newurl = newurl.replace(' ', '%20') + newheaders = dict((k,v) for k,v in req.headers.items() + if k.lower() not in ("content-length", "content-type") + ) + return urllib2.Request(newurl, + headers=newheaders, + origin_req_host=req.get_origin_req_host(), + unverifiable=True) + else: + raise urllib2.HTTPError(req.get_full_url(), code, msg, hdrs, + fp) + + return RedirectHandler + + class SSLValidationHandler(urllib2.BaseHandler): ''' A custom handler class for SSL validation. @@ -604,7 +653,8 @@ class SSLValidationHandler(urllib2.BaseHandler): # Rewrite of fetch_url to not require the module environment 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, force_basic_auth=False): + url_username=None, url_password=None, http_agent=None, + force_basic_auth=False, follow_redirects='urllib2'): ''' Fetches a file from an HTTP/FTP server using urllib2 ''' @@ -681,6 +731,9 @@ def open_url(url, data=None, headers=None, method=None, use_proxy=True, if hasattr(socket, 'create_connection') and CustomHTTPSHandler: handlers.append(CustomHTTPSHandler) + if follow_redirects != 'urllib2': + handlers.append(RedirectHandlerFactory(follow_redirects)) + opener = urllib2.build_opener(*handlers) urllib2.install_opener(opener) @@ -750,7 +803,8 @@ def url_argument_spec(): ) def fetch_url(module, url, data=None, headers=None, method=None, - use_proxy=True, force=False, last_mod_time=None, timeout=10): + use_proxy=True, force=False, last_mod_time=None, timeout=10, + follow_redirects=False): ''' Fetches a file from an HTTP/FTP server using urllib2. Requires the module environment ''' @@ -767,14 +821,16 @@ def fetch_url(module, url, data=None, headers=None, method=None, password = module.params.get('url_password', '') http_agent = module.params.get('http_agent', None) force_basic_auth = module.params.get('force_basic_auth', '') + follow_redirects = follow_redirects or module.params.get('follow_redirects', 'urllib2') r = None info = dict(url=url) try: r = open_url(url, data=data, headers=headers, method=method, - use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, - validate_certs=validate_certs, url_username=username, - url_password=password, http_agent=http_agent, force_basic_auth=force_basic_auth) + use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, + validate_certs=validate_certs, url_username=username, + url_password=password, http_agent=http_agent, force_basic_auth=force_basic_auth, + follow_redirects=follow_redirects) info.update(r.info()) info['url'] = r.geturl() # The URL goes in too, because of redirects. info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200)) @@ -787,7 +843,7 @@ def fetch_url(module, url, data=None, headers=None, method=None, except (ConnectionError, ValueError), e: module.fail_json(msg=str(e)) except urllib2.HTTPError, e: - info.update(dict(msg=str(e), status=e.code)) + info.update(dict(msg=str(e), status=e.code, **e.info())) except urllib2.URLError, e: code = int(getattr(e, 'code', -1)) info.update(dict(msg="Request failed: %s" % str(e), status=code)) diff --git a/test/integration/roles/test_uri/tasks/main.yml b/test/integration/roles/test_uri/tasks/main.yml index c58024e315..4c06ea1ad7 100644 --- a/test/integration/roles/test_uri/tasks/main.yml +++ b/test/integration/roles/test_uri/tasks/main.yml @@ -113,7 +113,7 @@ assert: that: - "result.failed == true" - - "'certificate does not match ' in result.msg" + - "'SSL Certificate does not belong' in result.msg" - "stat_result.stat.exists == false" - name: Clean up any cruft from the results directory @@ -140,3 +140,67 @@ that: - "stat_result.stat.exists == true" - "result.changed == true" + +- name: test redirect without follow_redirects + uri: + url: 'http://httpbin.org/redirect/2' + follow_redirects: 'none' + status_code: 302 + register: result + +- name: Assert location header + assert: + that: + - 'result.location|default("") == "http://httpbin.org/relative-redirect/1"' + +- name: Check SSL with redirect + uri: + url: 'https://httpbin.org/redirect/2' + register: result + +- name: Assert SSL with redirect + assert: + that: + - 'result.url|default("") == "https://httpbin.org/get"' + +- name: redirect to bad SSL site + uri: + url: 'http://wrong.host.badssl.com' + register: result + ignore_errors: true + +- name: Ensure bad SSL site reidrect fails + assert: + that: + - result|failed + - '"wrong.host.badssl.com" in result.msg' + +- name: test basic auth + uri: + url: 'http://httpbin.org/basic-auth/user/passwd' + user: user + password: passwd + +- name: test basic forced auth + uri: + url: 'http://httpbin.org/hidden-basic-auth/user/passwd' + force_basic_auth: true + user: user + password: passwd + +- name: test PUT + uri: + url: 'http://httpbin.org/put' + method: PUT + body: 'foo=bar' + +- name: test OPTIONS + uri: + url: 'http://httpbin.org/' + method: OPTIONS + register: result + +- name: Assert we got an allow header + assert: + that: + - 'result.allow|default("") == "HEAD, OPTIONS, GET"'