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