From 85d817bc84352bb6654a3b380d422628d632c84b Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Tue, 16 Sep 2025 09:46:58 -0700 Subject: [PATCH 1/2] uri: Set cookies and cookies_string * Set cookies and cookies_string when follow_redirects is None Fixes: #85780 Signed-off-by: Abhijeet Kasurde --- changelogs/fragments/uri_cookies.yml | 3 ++ lib/ansible/module_utils/urls.py | 34 ++++++++++++------- .../targets/uri/files/testserver.py | 23 +++++++++++++ test/integration/targets/uri/tasks/main.yml | 21 ++++++++++++ 4 files changed, 68 insertions(+), 13 deletions(-) create mode 100644 changelogs/fragments/uri_cookies.yml diff --git a/changelogs/fragments/uri_cookies.yml b/changelogs/fragments/uri_cookies.yml new file mode 100644 index 00000000000..d27d60d1118 --- /dev/null +++ b/changelogs/fragments/uri_cookies.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - uri - return cookies and cookies_string even if follow_redirects is set to None (https://github.com/ansible/ansible/issues/85780). diff --git a/lib/ansible/module_utils/urls.py b/lib/ansible/module_utils/urls.py index a1e36a6c5ce..f6cab68056b 100644 --- a/lib/ansible/module_utils/urls.py +++ b/lib/ansible/module_utils/urls.py @@ -1163,6 +1163,25 @@ def url_redirect_argument_spec(): ) +def _process_cookies(cookies: cookiejar.CookieJar) -> tuple[dict[str, str], str]: + """ + Parse the cookies into a nice dictionary and string representation of the cookies + :arg cookies: a CookieJar object + :return: a dictionary of cookies and a string representation of the cookies + """ + cookie_list = [] + cookie_dict = {} + # Python sorts cookies in order of most specific (ie. longest) path first. See ``CookieJar._cookie_attrs`` + # Cookies with the same path are reversed from response order. + # This code makes no assumptions about that, and accepts the order given by python + for cookie in cookies: + cookie_dict[cookie.name] = cookie.value + cookie_list.append((cookie.name, cookie.value)) + cookies_string = '; '.join('%s=%s' % c for c in cookie_list) + + return cookie_dict, cookies_string + + def fetch_url(module, url, data=None, headers=None, method=None, use_proxy=None, force=False, last_mod_time=None, timeout=10, use_gssapi=False, unix_socket=None, ca_path=None, cookies=None, unredirected_headers=None, @@ -1258,19 +1277,7 @@ def fetch_url(module, url, data=None, headers=None, method=None, else: temp_headers[name] = value info.update(temp_headers) - - # parse the cookies into a nice dictionary - cookie_list = [] - cookie_dict = {} - # Python sorts cookies in order of most specific (ie. longest) path first. See ``CookieJar._cookie_attrs`` - # Cookies with the same path are reversed from response order. - # This code makes no assumptions about that, and accepts the order given by python - for cookie in cookies: - cookie_dict[cookie.name] = cookie.value - cookie_list.append((cookie.name, cookie.value)) - info['cookies_string'] = '; '.join('%s=%s' % c for c in cookie_list) - - info['cookies'] = cookie_dict + info['cookies'], info['cookies_string'] = _process_cookies(cookies) # finally update the result with a message about the fetch info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), url=r.geturl(), status=r.code)) except (ConnectionError, ValueError) as e: @@ -1295,6 +1302,7 @@ def fetch_url(module, url, data=None, headers=None, method=None, try: # Lowercase keys, to conform to py2 behavior, so that py3 and py2 are predictable info.update({k.lower(): v for k, v in e.info().items()}) + info['cookies'], info['cookies_string'] = _process_cookies(cookies) except Exception: pass diff --git a/test/integration/targets/uri/files/testserver.py b/test/integration/targets/uri/files/testserver.py index 1792829091b..09e6d87b952 100644 --- a/test/integration/targets/uri/files/testserver.py +++ b/test/integration/targets/uri/files/testserver.py @@ -1,14 +1,37 @@ from __future__ import annotations +import http.cookies import http.server import socketserver import sys +import urllib.parse if __name__ == '__main__': PORT = int(sys.argv[1]) content_type_json = "application/json" class Handler(http.server.SimpleHTTPRequestHandler): + def do_POST(self): + if self.path == '/login': + # For testcase - follow_redirects is None and Cookies are set + # https://github.com/ansible/ansible/issues/85780 + content_length = int(self.headers['Content-Length']) + post_data_bytes = self.rfile.read(content_length) + post_data_str = post_data_bytes.decode('utf-8') + parsed_data = urllib.parse.parse_qs(post_data_str) + username = parsed_data.get('username', [None])[0] + password = parsed_data.get('password', [None])[0] + if username == "user" and password == "pass": + # Set a cookie + cookie = http.cookies.SimpleCookie() + cookie['session_id'] = 'secure_session_token_123' + self.send_response(302) + self.send_header('Location', '/success.html') + self.send_header('Set-Cookie', cookie.output(header='')) + self.end_headers() + else: + self.send_error(404) + def do_GET(self): if self.path == '/chunked': self.request.sendall( diff --git a/test/integration/targets/uri/tasks/main.yml b/test/integration/targets/uri/tasks/main.yml index 62748d7591f..8302c6492ea 100644 --- a/test/integration/targets/uri/tasks/main.yml +++ b/test/integration/targets/uri/tasks/main.yml @@ -774,3 +774,24 @@ assert: that: - uri_check.msg == "This action (uri) does not support check mode." + +# https://github.com/ansible/ansible/issues/85780 +- name: Test if cookies are returned when follow_redirects is none + ansible.builtin.uri: + method: POST + url: "http://localhost:{{ http_port }}/login" + follow_redirects: none + body_format: form-urlencoded + body: + username: user + password: pass + status_code: [302] + register: cookies_return + +- name: Check if cookies are returned if follow_redirects is None + assert: + that: + - cookies_return.cookies_string is defined + - cookies_return.cookies is defined + - cookies_return.cookies_string != '' + - cookies_return['cookies']['session_id'] == 'secure_session_token_123' From 977c697c8f55118cb28245413a5fb0459796e585 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Tue, 16 Sep 2025 10:37:28 -0700 Subject: [PATCH 2/2] Make CI green Signed-off-by: Abhijeet Kasurde --- test/units/module_utils/urls/test_fetch_url.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/units/module_utils/urls/test_fetch_url.py b/test/units/module_utils/urls/test_fetch_url.py index aaa93b151aa..968cad10a8f 100644 --- a/test/units/module_utils/urls/test_fetch_url.py +++ b/test/units/module_utils/urls/test_fetch_url.py @@ -170,7 +170,8 @@ def test_fetch_url_httperror(open_url_mock, fake_ansible_module): r, info = fetch_url(fake_ansible_module, BASE_URL) assert info == {'msg': 'HTTP Error 500: Internal Server Error', 'body': 'TESTS', - 'status': 500, 'url': BASE_URL, 'content-type': 'application/json'} + 'status': 500, 'url': BASE_URL, 'content-type': 'application/json', + 'cookies': {}, 'cookies_string': ''} def test_fetch_url_urlerror(open_url_mock, fake_ansible_module):