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