1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2013, Romeo Theriault <romeot () hawaii.edu>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = r'''
12---
13module: uri
14short_description: Interacts with webservices
15description:
16  - Interacts with HTTP and HTTPS web services and supports Digest, Basic and WSSE
17    HTTP authentication mechanisms.
18  - For Windows targets, use the M(ansible.windows.win_uri) module instead.
19version_added: "1.1"
20options:
21  url:
22    description:
23      - HTTP or HTTPS URL in the form (http|https)://host.domain[:port]/path
24    type: str
25    required: true
26  dest:
27    description:
28      - A path of where to download the file to (if desired). If I(dest) is a
29        directory, the basename of the file on the remote server will be used.
30    type: path
31  url_username:
32    description:
33      - A username for the module to use for Digest, Basic or WSSE authentication.
34    type: str
35    aliases: [ user ]
36  url_password:
37    description:
38      - A password for the module to use for Digest, Basic or WSSE authentication.
39    type: str
40    aliases: [ password ]
41  body:
42    description:
43      - The body of the http request/response to the web service. If C(body_format) is set
44        to 'json' it will take an already formatted JSON string or convert a data structure
45        into JSON.
46      - If C(body_format) is set to 'form-urlencoded' it will convert a dictionary
47        or list of tuples into an 'application/x-www-form-urlencoded' string. (Added in v2.7)
48      - If C(body_format) is set to 'form-multipart' it will convert a dictionary
49        into 'multipart/form-multipart' body. (Added in v2.10)
50    type: raw
51  body_format:
52    description:
53      - The serialization format of the body. When set to C(json), C(form-multipart), or C(form-urlencoded), encodes
54        the body argument, if needed, and automatically sets the Content-Type header accordingly.
55      - As of C(2.3) it is possible to override the `Content-Type` header, when
56        set to C(json) or C(form-urlencoded) via the I(headers) option.
57      - The 'Content-Type' header cannot be overridden when using C(form-multipart)
58      - C(form-urlencoded) was added in v2.7.
59      - C(form-multipart) was added in v2.10.
60    type: str
61    choices: [ form-urlencoded, json, raw, form-multipart ]
62    default: raw
63    version_added: "2.0"
64  method:
65    description:
66      - The HTTP method of the request or response.
67      - In more recent versions we do not restrict the method at the module level anymore
68        but it still must be a valid method accepted by the service handling the request.
69    type: str
70    default: GET
71  return_content:
72    description:
73      - Whether or not to return the body of the response as a "content" key in
74        the dictionary result no matter it succeeded or failed.
75      - Independently of this option, if the reported Content-type is "application/json", then the JSON is
76        always loaded into a key called C(json) in the dictionary results.
77    type: bool
78    default: no
79  force_basic_auth:
80    description:
81      - Force the sending of the Basic authentication header upon initial request.
82      - The library used by the uri module only sends authentication information when a webservice
83        responds to an initial request with a 401 status. Since some basic auth services do not properly
84        send a 401, logins will fail.
85    type: bool
86    default: no
87  follow_redirects:
88    description:
89      - Whether or not the URI module should follow redirects. C(all) will follow all redirects.
90        C(safe) will follow only "safe" redirects, where "safe" means that the client is only
91        doing a GET or HEAD on the URI to which it is being redirected. C(none) will not follow
92        any redirects. Note that C(yes) and C(no) choices are accepted for backwards compatibility,
93        where C(yes) is the equivalent of C(all) and C(no) is the equivalent of C(safe). C(yes) and C(no)
94        are deprecated and will be removed in some future version of Ansible.
95    type: str
96    choices: ['all', 'no', 'none', 'safe', 'urllib2', 'yes']
97    default: safe
98  creates:
99    description:
100      - A filename, when it already exists, this step will not be run.
101    type: path
102  removes:
103    description:
104      - A filename, when it does not exist, this step will not be run.
105    type: path
106  status_code:
107    description:
108      - A list of valid, numeric, HTTP status codes that signifies success of the request.
109    type: list
110    default: [ 200 ]
111  timeout:
112    description:
113      - The socket level timeout in seconds
114    type: int
115    default: 30
116  headers:
117    description:
118        - Add custom HTTP headers to a request in the format of a YAML hash. As
119          of C(2.3) supplying C(Content-Type) here will override the header
120          generated by supplying C(json) or C(form-urlencoded) for I(body_format).
121    type: dict
122    version_added: '2.1'
123  validate_certs:
124    description:
125      - If C(no), SSL certificates will not be validated.
126      - This should only set to C(no) used on personally controlled sites using self-signed certificates.
127      - Prior to 1.9.2 the code defaulted to C(no).
128    type: bool
129    default: yes
130    version_added: '1.9.2'
131  client_cert:
132    description:
133      - PEM formatted certificate chain file to be used for SSL client authentication.
134      - This file can also include the key as well, and if the key is included, I(client_key) is not required
135    type: path
136    version_added: '2.4'
137  client_key:
138    description:
139      - PEM formatted file that contains your private key to be used for SSL client authentication.
140      - If I(client_cert) contains both the certificate and key, this option is not required.
141    type: path
142    version_added: '2.4'
143  src:
144    description:
145      - Path to file to be submitted to the remote server.
146      - Cannot be used with I(body).
147    type: path
148    version_added: '2.7'
149  remote_src:
150    description:
151      - If C(no), the module will search for src on originating/master machine.
152      - If C(yes) the module will use the C(src) path on the remote/target machine.
153    type: bool
154    default: no
155    version_added: '2.7'
156  force:
157    description:
158      - If C(yes) do not get a cached copy.
159      - Alias C(thirsty) has been deprecated and will be removed in 2.13.
160    type: bool
161    default: no
162    aliases: [ thirsty ]
163  use_proxy:
164    description:
165      - If C(no), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
166    type: bool
167    default: yes
168  unix_socket:
169    description:
170    - Path to Unix domain socket to use for connection
171    version_added: '2.8'
172  http_agent:
173    description:
174      - Header to identify as, generally appears in web server logs.
175    type: str
176    default: ansible-httpget
177notes:
178  - The dependency on httplib2 was removed in Ansible 2.1.
179  - The module returns all the HTTP headers in lower-case.
180  - For Windows targets, use the M(ansible.windows.win_uri) module instead.
181seealso:
182- module: ansible.builtin.get_url
183- module: ansible.windows.win_uri
184author:
185- Romeo Theriault (@romeotheriault)
186extends_documentation_fragment: files
187'''
188
189EXAMPLES = r'''
190- name: Check that you can connect (GET) to a page and it returns a status 200
191  uri:
192    url: http://www.example.com
193
194- name: Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents
195  uri:
196    url: http://www.example.com
197    return_content: yes
198  register: this
199  failed_when: "'AWESOME' not in this.content"
200
201- name: Create a JIRA issue
202  uri:
203    url: https://your.jira.example.com/rest/api/2/issue/
204    user: your_username
205    password: your_pass
206    method: POST
207    body: "{{ lookup('file','issue.json') }}"
208    force_basic_auth: yes
209    status_code: 201
210    body_format: json
211
212- name: Login to a form based webpage, then use the returned cookie to access the app in later tasks
213  uri:
214    url: https://your.form.based.auth.example.com/index.php
215    method: POST
216    body_format: form-urlencoded
217    body:
218      name: your_username
219      password: your_password
220      enter: Sign in
221    status_code: 302
222  register: login
223
224- name: Login to a form based webpage using a list of tuples
225  uri:
226    url: https://your.form.based.auth.example.com/index.php
227    method: POST
228    body_format: form-urlencoded
229    body:
230    - [ name, your_username ]
231    - [ password, your_password ]
232    - [ enter, Sign in ]
233    status_code: 302
234  register: login
235
236- name: Upload a file via multipart/form-multipart
237  uri:
238    url: https://httpbin.org/post
239    method: POST
240    body_format: form-multipart
241    body:
242      file1:
243        filename: /bin/true
244        mime_type: application/octet-stream
245      file2:
246        content: text based file content
247        filename: fake.txt
248        mime_type: text/plain
249      text_form_field: value
250
251- name: Connect to website using a previously stored cookie
252  uri:
253    url: https://your.form.based.auth.example.com/dashboard.php
254    method: GET
255    return_content: yes
256    headers:
257      Cookie: "{{ login.cookies_string }}"
258
259- name: Queue build of a project in Jenkins
260  uri:
261    url: http://{{ jenkins.host }}/job/{{ jenkins.job }}/build?token={{ jenkins.token }}
262    user: "{{ jenkins.user }}"
263    password: "{{ jenkins.password }}"
264    method: GET
265    force_basic_auth: yes
266    status_code: 201
267
268- name: POST from contents of local file
269  uri:
270    url: https://httpbin.org/post
271    method: POST
272    src: file.json
273
274- name: POST from contents of remote file
275  uri:
276    url: https://httpbin.org/post
277    method: POST
278    src: /path/to/my/file.json
279    remote_src: yes
280
281- name: Create workspaces in Log analytics Azure
282  uri:
283    url: https://www.mms.microsoft.com/Embedded/Api/ConfigDataSources/LogManagementData/Save
284    method: POST
285    body_format: json
286    status_code: [200, 202]
287    return_content: true
288    headers:
289      Content-Type: application/json
290      x-ms-client-workspace-path: /subscriptions/{{ sub_id }}/resourcegroups/{{ res_group }}/providers/microsoft.operationalinsights/workspaces/{{ w_spaces }}
291      x-ms-client-platform: ibiza
292      x-ms-client-auth-token: "{{ token_az }}"
293    body:
294
295- name: Pause play until a URL is reachable from this host
296  uri:
297    url: "http://192.0.2.1/some/test"
298    follow_redirects: none
299    method: GET
300  register: _result
301  until: _result.status == 200
302  retries: 720 # 720 * 5 seconds = 1hour (60*60/5)
303  delay: 5 # Every 5 seconds
304
305# There are issues in a supporting Python library that is discussed in
306# https://github.com/ansible/ansible/issues/52705 where a proxy is defined
307# but you want to bypass proxy use on CIDR masks by using no_proxy
308- name: Work around a python issue that doesn't support no_proxy envvar
309  uri:
310    follow_redirects: none
311    validate_certs: false
312    timeout: 5
313    url: "http://{{ ip_address }}:{{ port | default(80) }}"
314  register: uri_data
315  failed_when: false
316  changed_when: false
317  vars:
318    ip_address: 192.0.2.1
319  environment: |
320      {
321        {% for no_proxy in (lookup('env', 'no_proxy') | regex_replace('\s*,\s*', ' ') ).split() %}
322          {% if no_proxy | regex_search('\/') and
323                no_proxy | ipaddr('net') != '' and
324                no_proxy | ipaddr('net') != false and
325                ip_address | ipaddr(no_proxy) is not none and
326                ip_address | ipaddr(no_proxy) != false %}
327            'no_proxy': '{{ ip_address }}'
328          {% elif no_proxy | regex_search(':') != '' and
329                  no_proxy | regex_search(':') != false and
330                  no_proxy == ip_address + ':' + (port | default(80)) %}
331            'no_proxy': '{{ ip_address }}:{{ port | default(80) }}'
332          {% elif no_proxy | ipaddr('host') != '' and
333                  no_proxy | ipaddr('host') != false and
334                  no_proxy == ip_address %}
335            'no_proxy': '{{ ip_address }}'
336          {% elif no_proxy | regex_search('^(\*|)\.') != '' and
337                  no_proxy | regex_search('^(\*|)\.') != false and
338                  no_proxy | regex_replace('\*', '') in ip_address %}
339            'no_proxy': '{{ ip_address }}'
340          {% endif %}
341        {% endfor %}
342      }
343'''
344
345RETURN = r'''
346# The return information includes all the HTTP headers in lower-case.
347content:
348  description: The response body content.
349  returned: status not in status_code or return_content is true
350  type: str
351  sample: "{}"
352cookies:
353  description: The cookie values placed in cookie jar.
354  returned: on success
355  type: dict
356  sample: {"SESSIONID": "[SESSIONID]"}
357  version_added: "2.4"
358cookies_string:
359  description: The value for future request Cookie headers.
360  returned: on success
361  type: str
362  sample: "SESSIONID=[SESSIONID]"
363  version_added: "2.6"
364elapsed:
365  description: The number of seconds that elapsed while performing the download.
366  returned: on success
367  type: int
368  sample: 23
369msg:
370  description: The HTTP message from the request.
371  returned: always
372  type: str
373  sample: OK (unknown bytes)
374redirected:
375  description: Whether the request was redirected.
376  returned: on success
377  type: bool
378  sample: false
379status:
380  description: The HTTP status code from the request.
381  returned: always
382  type: int
383  sample: 200
384url:
385  description: The actual URL used for the request.
386  returned: always
387  type: str
388  sample: https://www.ansible.com/
389'''
390
391import cgi
392import datetime
393import json
394import os
395import re
396import shutil
397import sys
398import tempfile
399
400from ansible.module_utils.basic import AnsibleModule, sanitize_keys
401from ansible.module_utils.six import PY2, iteritems, string_types
402from ansible.module_utils.six.moves.urllib.parse import urlencode, urlsplit
403from ansible.module_utils._text import to_native, to_text
404from ansible.module_utils.common._collections_compat import Mapping, Sequence
405from ansible.module_utils.urls import fetch_url, prepare_multipart, url_argument_spec
406
407JSON_CANDIDATES = ('text', 'json', 'javascript')
408
409# List of response key names we do not want sanitize_keys() to change.
410NO_MODIFY_KEYS = frozenset(
411    ('msg', 'exception', 'warnings', 'deprecations', 'failed', 'skipped',
412     'changed', 'rc', 'stdout', 'stderr', 'elapsed', 'path', 'location',
413     'content_type')
414)
415
416
417def format_message(err, resp):
418    msg = resp.pop('msg')
419    return err + (' %s' % msg if msg else '')
420
421
422def write_file(module, url, dest, content, resp):
423    # create a tempfile with some test content
424    fd, tmpsrc = tempfile.mkstemp(dir=module.tmpdir)
425    f = open(tmpsrc, 'wb')
426    try:
427        f.write(content)
428    except Exception as e:
429        os.remove(tmpsrc)
430        msg = format_message("Failed to create temporary content file: %s" % to_native(e), resp)
431        module.fail_json(msg=msg, **resp)
432    f.close()
433
434    checksum_src = None
435    checksum_dest = None
436
437    # raise an error if there is no tmpsrc file
438    if not os.path.exists(tmpsrc):
439        os.remove(tmpsrc)
440        msg = format_message("Source '%s' does not exist" % tmpsrc, resp)
441        module.fail_json(msg=msg, **resp)
442    if not os.access(tmpsrc, os.R_OK):
443        os.remove(tmpsrc)
444        msg = format_message("Source '%s' not readable" % tmpsrc, resp)
445        module.fail_json(msg=msg, **resp)
446    checksum_src = module.sha1(tmpsrc)
447
448    # check if there is no dest file
449    if os.path.exists(dest):
450        # raise an error if copy has no permission on dest
451        if not os.access(dest, os.W_OK):
452            os.remove(tmpsrc)
453            msg = format_message("Destination '%s' not writable" % dest, resp)
454            module.fail_json(msg=msg, **resp)
455        if not os.access(dest, os.R_OK):
456            os.remove(tmpsrc)
457            msg = format_message("Destination '%s' not readable" % dest, resp)
458            module.fail_json(msg=msg, **resp)
459        checksum_dest = module.sha1(dest)
460    else:
461        if not os.access(os.path.dirname(dest), os.W_OK):
462            os.remove(tmpsrc)
463            msg = format_message("Destination dir '%s' not writable" % os.path.dirname(dest), resp)
464            module.fail_json(msg=msg, **resp)
465
466    if checksum_src != checksum_dest:
467        try:
468            shutil.copyfile(tmpsrc, dest)
469        except Exception as e:
470            os.remove(tmpsrc)
471            msg = format_message("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(e)), resp)
472            module.fail_json(msg=msg, **resp)
473
474    os.remove(tmpsrc)
475
476
477def url_filename(url):
478    fn = os.path.basename(urlsplit(url)[2])
479    if fn == '':
480        return 'index.html'
481    return fn
482
483
484def absolute_location(url, location):
485    """Attempts to create an absolute URL based on initial URL, and
486    next URL, specifically in the case of a ``Location`` header.
487    """
488
489    if '://' in location:
490        return location
491
492    elif location.startswith('/'):
493        parts = urlsplit(url)
494        base = url.replace(parts[2], '')
495        return '%s%s' % (base, location)
496
497    elif not location.startswith('/'):
498        base = os.path.dirname(url)
499        return '%s/%s' % (base, location)
500
501    else:
502        return location
503
504
505def kv_list(data):
506    ''' Convert data into a list of key-value tuples '''
507    if data is None:
508        return None
509
510    if isinstance(data, Sequence):
511        return list(data)
512
513    if isinstance(data, Mapping):
514        return list(data.items())
515
516    raise TypeError('cannot form-urlencode body, expect list or dict')
517
518
519def form_urlencoded(body):
520    ''' Convert data into a form-urlencoded string '''
521    if isinstance(body, string_types):
522        return body
523
524    if isinstance(body, (Mapping, Sequence)):
525        result = []
526        # Turn a list of lists into a list of tuples that urlencode accepts
527        for key, values in kv_list(body):
528            if isinstance(values, string_types) or not isinstance(values, (Mapping, Sequence)):
529                values = [values]
530            for value in values:
531                if value is not None:
532                    result.append((to_text(key), to_text(value)))
533        return urlencode(result, doseq=True)
534
535    return body
536
537
538def uri(module, url, dest, body, body_format, method, headers, socket_timeout):
539    # is dest is set and is a directory, let's check if we get redirected and
540    # set the filename from that url
541    redirected = False
542    redir_info = {}
543    r = {}
544
545    src = module.params['src']
546    if src:
547        try:
548            headers.update({
549                'Content-Length': os.stat(src).st_size
550            })
551            data = open(src, 'rb')
552        except OSError:
553            module.fail_json(msg='Unable to open source file %s' % src, elapsed=0)
554    else:
555        data = body
556
557    kwargs = {}
558    if dest is not None:
559        # Stash follow_redirects, in this block we don't want to follow
560        # we'll reset back to the supplied value soon
561        follow_redirects = module.params['follow_redirects']
562        module.params['follow_redirects'] = False
563        if os.path.isdir(dest):
564            # first check if we are redirected to a file download
565            _, redir_info = fetch_url(module, url, data=body,
566                                      headers=headers,
567                                      method=method,
568                                      timeout=socket_timeout, unix_socket=module.params['unix_socket'])
569            # if we are redirected, update the url with the location header,
570            # and update dest with the new url filename
571            if redir_info['status'] in (301, 302, 303, 307):
572                url = redir_info['location']
573                redirected = True
574            dest = os.path.join(dest, url_filename(url))
575        # if destination file already exist, only download if file newer
576        if os.path.exists(dest):
577            kwargs['last_mod_time'] = datetime.datetime.utcfromtimestamp(os.path.getmtime(dest))
578
579        # Reset follow_redirects back to the stashed value
580        module.params['follow_redirects'] = follow_redirects
581
582    resp, info = fetch_url(module, url, data=data, headers=headers,
583                           method=method, timeout=socket_timeout, unix_socket=module.params['unix_socket'],
584                           **kwargs)
585
586    try:
587        content = resp.read()
588    except AttributeError:
589        # there was no content, but the error read()
590        # may have been stored in the info as 'body'
591        content = info.pop('body', '')
592
593    if src:
594        # Try to close the open file handle
595        try:
596            data.close()
597        except Exception:
598            pass
599
600    r['redirected'] = redirected or info['url'] != url
601    r.update(redir_info)
602    r.update(info)
603
604    return r, content, dest
605
606
607def main():
608    argument_spec = url_argument_spec()
609    argument_spec.update(
610        dest=dict(type='path'),
611        url_username=dict(type='str', aliases=['user']),
612        url_password=dict(type='str', aliases=['password'], no_log=True),
613        body=dict(type='raw'),
614        body_format=dict(type='str', default='raw', choices=['form-urlencoded', 'json', 'raw', 'form-multipart']),
615        src=dict(type='path'),
616        method=dict(type='str', default='GET'),
617        return_content=dict(type='bool', default=False),
618        follow_redirects=dict(type='str', default='safe', choices=['all', 'no', 'none', 'safe', 'urllib2', 'yes']),
619        creates=dict(type='path'),
620        removes=dict(type='path'),
621        status_code=dict(type='list', default=[200]),
622        timeout=dict(type='int', default=30),
623        headers=dict(type='dict', default={}),
624        unix_socket=dict(type='path'),
625        remote_src=dict(type='bool', default=False),
626    )
627
628    module = AnsibleModule(
629        argument_spec=argument_spec,
630        add_file_common_args=True,
631        mutually_exclusive=[['body', 'src']],
632    )
633
634    if module.params.get('thirsty'):
635        module.deprecate('The alias "thirsty" has been deprecated and will be removed, use "force" instead',
636                         version='2.13', collection_name='ansible.builtin')
637
638    url = module.params['url']
639    body = module.params['body']
640    body_format = module.params['body_format'].lower()
641    method = module.params['method'].upper()
642    dest = module.params['dest']
643    return_content = module.params['return_content']
644    creates = module.params['creates']
645    removes = module.params['removes']
646    status_code = [int(x) for x in list(module.params['status_code'])]
647    socket_timeout = module.params['timeout']
648
649    dict_headers = module.params['headers']
650
651    if not re.match('^[A-Z]+$', method):
652        module.fail_json(msg="Parameter 'method' needs to be a single word in uppercase, like GET or POST.")
653
654    if body_format == 'json':
655        # Encode the body unless its a string, then assume it is pre-formatted JSON
656        if not isinstance(body, string_types):
657            body = json.dumps(body)
658        if 'content-type' not in [header.lower() for header in dict_headers]:
659            dict_headers['Content-Type'] = 'application/json'
660    elif body_format == 'form-urlencoded':
661        if not isinstance(body, string_types):
662            try:
663                body = form_urlencoded(body)
664            except ValueError as e:
665                module.fail_json(msg='failed to parse body as form_urlencoded: %s' % to_native(e), elapsed=0)
666        if 'content-type' not in [header.lower() for header in dict_headers]:
667            dict_headers['Content-Type'] = 'application/x-www-form-urlencoded'
668    elif body_format == 'form-multipart':
669        try:
670            content_type, body = prepare_multipart(body)
671        except (TypeError, ValueError) as e:
672            module.fail_json(msg='failed to parse body as form-multipart: %s' % to_native(e))
673        dict_headers['Content-Type'] = content_type
674
675    if creates is not None:
676        # do not run the command if the line contains creates=filename
677        # and the filename already exists.  This allows idempotence
678        # of uri executions.
679        if os.path.exists(creates):
680            module.exit_json(stdout="skipped, since '%s' exists" % creates, changed=False)
681
682    if removes is not None:
683        # do not run the command if the line contains removes=filename
684        # and the filename does not exist.  This allows idempotence
685        # of uri executions.
686        if not os.path.exists(removes):
687            module.exit_json(stdout="skipped, since '%s' does not exist" % removes, changed=False)
688
689    # Make the request
690    start = datetime.datetime.utcnow()
691    resp, content, dest = uri(module, url, dest, body, body_format, method,
692                              dict_headers, socket_timeout)
693    resp['elapsed'] = (datetime.datetime.utcnow() - start).seconds
694    resp['status'] = int(resp['status'])
695    resp['changed'] = False
696
697    # Write the file out if requested
698    if dest is not None:
699        if resp['status'] in status_code and resp['status'] != 304:
700            write_file(module, url, dest, content, resp)
701            # allow file attribute changes
702            resp['changed'] = True
703            module.params['path'] = dest
704            file_args = module.load_file_common_arguments(module.params, path=dest)
705            resp['changed'] = module.set_fs_attributes_if_different(file_args, resp['changed'])
706        resp['path'] = dest
707
708    # Transmogrify the headers, replacing '-' with '_', since variables don't
709    # work with dashes.
710    # In python3, the headers are title cased.  Lowercase them to be
711    # compatible with the python2 behaviour.
712    uresp = {}
713    for key, value in iteritems(resp):
714        ukey = key.replace("-", "_").lower()
715        uresp[ukey] = value
716
717    if 'location' in uresp:
718        uresp['location'] = absolute_location(url, uresp['location'])
719
720    # Default content_encoding to try
721    content_encoding = 'utf-8'
722    if 'content_type' in uresp:
723        # Handle multiple Content-Type headers
724        charsets = []
725        content_types = []
726        for value in uresp['content_type'].split(','):
727            ct, params = cgi.parse_header(value)
728            if ct not in content_types:
729                content_types.append(ct)
730            if 'charset' in params:
731                if params['charset'] not in charsets:
732                    charsets.append(params['charset'])
733
734        if content_types:
735            content_type = content_types[0]
736            if len(content_types) > 1:
737                module.warn(
738                    'Received multiple conflicting Content-Type values (%s), using %s' % (', '.join(content_types), content_type)
739                )
740        if charsets:
741            content_encoding = charsets[0]
742            if len(charsets) > 1:
743                module.warn(
744                    'Received multiple conflicting charset values (%s), using %s' % (', '.join(charsets), content_encoding)
745                )
746
747        u_content = to_text(content, encoding=content_encoding)
748        if any(candidate in content_type for candidate in JSON_CANDIDATES):
749            try:
750                js = json.loads(u_content)
751                uresp['json'] = js
752            except Exception:
753                if PY2:
754                    sys.exc_clear()  # Avoid false positive traceback in fail_json() on Python 2
755    else:
756        u_content = to_text(content, encoding=content_encoding)
757
758    if module.no_log_values:
759        uresp = sanitize_keys(uresp, module.no_log_values, NO_MODIFY_KEYS)
760
761    if resp['status'] not in status_code:
762        uresp['msg'] = 'Status code was %s and not %s: %s' % (resp['status'], status_code, uresp.get('msg', ''))
763        if return_content:
764            module.fail_json(content=u_content, **uresp)
765        else:
766            module.fail_json(**uresp)
767    elif return_content:
768        module.exit_json(content=u_content, **uresp)
769    else:
770        module.exit_json(**uresp)
771
772
773if __name__ == '__main__':
774    main()
775