ansible-core 2.16.6__py3-none-any.whl → 2.17.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansible-core might be problematic. Click here for more details.
- ansible/__init__.py +1 -3
- ansible/__main__.py +1 -0
- ansible/_vendor/__init__.py +1 -2
- ansible/cli/__init__.py +7 -8
- ansible/cli/adhoc.py +1 -2
- ansible/cli/arguments/__init__.py +1 -2
- ansible/cli/arguments/option_helpers.py +1 -2
- ansible/cli/config.py +1 -2
- ansible/cli/console.py +1 -2
- ansible/cli/doc.py +320 -198
- ansible/cli/galaxy.py +9 -3
- ansible/cli/inventory.py +4 -6
- ansible/cli/playbook.py +1 -2
- ansible/cli/pull.py +4 -5
- ansible/cli/scripts/ansible_connection_cli_stub.py +1 -4
- ansible/cli/vault.py +1 -2
- ansible/collections/list.py +1 -0
- ansible/compat/__init__.py +1 -4
- ansible/compat/importlib_resources.py +1 -2
- ansible/compat/{selectors/__init__.py → selectors.py} +12 -12
- ansible/config/ansible_builtin_runtime.yml +2 -0
- ansible/config/base.yml +154 -149
- ansible/config/manager.py +33 -25
- ansible/constants.py +1 -2
- ansible/context.py +1 -4
- ansible/errors/__init__.py +1 -3
- ansible/errors/yaml_strings.py +1 -3
- ansible/executor/__init__.py +1 -3
- ansible/executor/discovery/python_target.py +1 -2
- ansible/executor/interpreter_discovery.py +9 -8
- ansible/executor/module_common.py +1 -3
- ansible/executor/play_iterator.py +2 -4
- ansible/executor/playbook_executor.py +1 -3
- ansible/executor/powershell/module_manifest.py +2 -3
- ansible/executor/process/__init__.py +1 -3
- ansible/executor/process/worker.py +1 -3
- ansible/executor/stats.py +1 -3
- ansible/executor/task_executor.py +2 -3
- ansible/executor/task_queue_manager.py +1 -3
- ansible/executor/task_result.py +2 -3
- ansible/galaxy/__init__.py +1 -2
- ansible/galaxy/api.py +10 -2
- ansible/galaxy/collection/__init__.py +38 -24
- ansible/galaxy/collection/concrete_artifact_manager.py +1 -2
- ansible/galaxy/collection/galaxy_api_proxy.py +1 -2
- ansible/galaxy/collection/gpg.py +4 -2
- ansible/galaxy/data/apb/meta/main.yml.j2 +0 -15
- ansible/galaxy/data/container/meta/main.yml.j2 +0 -18
- ansible/galaxy/data/default/role/meta/main.yml.j2 +0 -18
- ansible/galaxy/data/network/cliconf_plugins/example.py.j2 +1 -2
- ansible/galaxy/data/network/library/example_command.py.j2 +1 -2
- ansible/galaxy/data/network/library/example_config.py.j2 +1 -2
- ansible/galaxy/data/network/library/example_facts.py.j2 +1 -2
- ansible/galaxy/data/network/meta/main.yml.j2 +0 -15
- ansible/galaxy/data/network/module_utils/example.py.j2 +1 -2
- ansible/galaxy/data/network/netconf_plugins/example.py.j2 +1 -2
- ansible/galaxy/data/network/terminal_plugins/example.py.j2 +1 -2
- ansible/galaxy/dependency_resolution/__init__.py +1 -2
- ansible/galaxy/dependency_resolution/dataclasses.py +5 -6
- ansible/galaxy/dependency_resolution/errors.py +1 -2
- ansible/galaxy/dependency_resolution/providers.py +3 -4
- ansible/galaxy/dependency_resolution/reporters.py +2 -3
- ansible/galaxy/dependency_resolution/resolvers.py +1 -2
- ansible/galaxy/dependency_resolution/versioning.py +1 -2
- ansible/galaxy/role.py +12 -15
- ansible/galaxy/token.py +1 -2
- ansible/galaxy/user_agent.py +1 -2
- ansible/inventory/data.py +1 -2
- ansible/inventory/group.py +1 -2
- ansible/inventory/helpers.py +1 -2
- ansible/inventory/host.py +1 -3
- ansible/inventory/manager.py +1 -2
- ansible/module_utils/_text.py +1 -2
- ansible/module_utils/ansible_release.py +3 -5
- ansible/module_utils/api.py +1 -2
- ansible/module_utils/basic.py +113 -158
- ansible/module_utils/common/_collections_compat.py +1 -2
- ansible/module_utils/common/_utils.py +1 -3
- ansible/module_utils/common/arg_spec.py +1 -2
- ansible/module_utils/common/collections.py +1 -2
- ansible/module_utils/common/dict_transformations.py +1 -2
- ansible/module_utils/common/file.py +10 -5
- ansible/module_utils/common/json.py +1 -3
- ansible/module_utils/common/locale.py +1 -2
- ansible/module_utils/common/network.py +2 -3
- ansible/module_utils/common/parameters.py +9 -9
- ansible/module_utils/common/process.py +12 -5
- ansible/module_utils/common/respawn.py +2 -3
- ansible/module_utils/common/sys_info.py +1 -2
- ansible/module_utils/common/text/converters.py +1 -2
- ansible/module_utils/common/text/formatters.py +1 -2
- ansible/module_utils/common/validation.py +5 -6
- ansible/module_utils/common/warnings.py +1 -2
- ansible/module_utils/common/yaml.py +1 -2
- ansible/module_utils/compat/datetime.py +1 -3
- ansible/module_utils/compat/importlib.py +21 -13
- ansible/module_utils/compat/paramiko.py +1 -2
- ansible/module_utils/compat/selectors.py +10 -34
- ansible/module_utils/compat/selinux.py +1 -2
- ansible/module_utils/compat/typing.py +1 -2
- ansible/module_utils/compat/version.py +1 -2
- ansible/module_utils/connection.py +1 -2
- ansible/module_utils/csharp/Ansible.Basic.cs +20 -3
- ansible/module_utils/distro/__init__.py +3 -4
- ansible/module_utils/distro/_distro.py +230 -234
- ansible/module_utils/errors.py +1 -2
- ansible/module_utils/facts/__init__.py +1 -2
- ansible/module_utils/facts/ansible_collector.py +1 -2
- ansible/module_utils/facts/collector.py +1 -2
- ansible/module_utils/facts/compat.py +1 -2
- ansible/module_utils/facts/default_collectors.py +1 -2
- ansible/module_utils/facts/hardware/aix.py +1 -2
- ansible/module_utils/facts/hardware/base.py +1 -2
- ansible/module_utils/facts/hardware/darwin.py +1 -2
- ansible/module_utils/facts/hardware/dragonfly.py +1 -2
- ansible/module_utils/facts/hardware/freebsd.py +1 -2
- ansible/module_utils/facts/hardware/hpux.py +1 -2
- ansible/module_utils/facts/hardware/hurd.py +1 -2
- ansible/module_utils/facts/hardware/linux.py +8 -6
- ansible/module_utils/facts/hardware/netbsd.py +1 -2
- ansible/module_utils/facts/hardware/openbsd.py +1 -2
- ansible/module_utils/facts/hardware/sunos.py +2 -3
- ansible/module_utils/facts/namespace.py +1 -2
- ansible/module_utils/facts/network/aix.py +1 -2
- ansible/module_utils/facts/network/base.py +1 -2
- ansible/module_utils/facts/network/darwin.py +1 -2
- ansible/module_utils/facts/network/dragonfly.py +1 -2
- ansible/module_utils/facts/network/fc_wwn.py +1 -2
- ansible/module_utils/facts/network/freebsd.py +1 -2
- ansible/module_utils/facts/network/generic_bsd.py +1 -2
- ansible/module_utils/facts/network/hpux.py +1 -2
- ansible/module_utils/facts/network/hurd.py +1 -2
- ansible/module_utils/facts/network/iscsi.py +1 -2
- ansible/module_utils/facts/network/linux.py +1 -2
- ansible/module_utils/facts/network/netbsd.py +1 -2
- ansible/module_utils/facts/network/nvme.py +1 -2
- ansible/module_utils/facts/network/openbsd.py +1 -2
- ansible/module_utils/facts/network/sunos.py +1 -2
- ansible/module_utils/facts/other/facter.py +1 -2
- ansible/module_utils/facts/other/ohai.py +1 -2
- ansible/module_utils/facts/packages.py +1 -2
- ansible/module_utils/facts/sysctl.py +1 -2
- ansible/module_utils/facts/system/apparmor.py +1 -2
- ansible/module_utils/facts/system/caps.py +1 -2
- ansible/module_utils/facts/system/chroot.py +1 -2
- ansible/module_utils/facts/system/cmdline.py +1 -2
- ansible/module_utils/facts/system/date_time.py +1 -2
- ansible/module_utils/facts/system/distribution.py +4 -5
- ansible/module_utils/facts/system/dns.py +1 -2
- ansible/module_utils/facts/system/env.py +1 -2
- ansible/module_utils/facts/system/fips.py +1 -2
- ansible/module_utils/facts/system/loadavg.py +1 -2
- ansible/module_utils/facts/system/local.py +1 -2
- ansible/module_utils/facts/system/lsb.py +1 -2
- ansible/module_utils/facts/system/pkg_mgr.py +7 -30
- ansible/module_utils/facts/system/platform.py +1 -2
- ansible/module_utils/facts/system/python.py +1 -2
- ansible/module_utils/facts/system/selinux.py +1 -2
- ansible/module_utils/facts/system/service_mgr.py +1 -2
- ansible/module_utils/facts/system/ssh_pub_keys.py +1 -2
- ansible/module_utils/facts/system/user.py +1 -2
- ansible/module_utils/facts/timeout.py +1 -2
- ansible/module_utils/facts/utils.py +1 -2
- ansible/module_utils/facts/virtual/base.py +1 -2
- ansible/module_utils/facts/virtual/dragonfly.py +1 -2
- ansible/module_utils/facts/virtual/freebsd.py +1 -2
- ansible/module_utils/facts/virtual/hpux.py +1 -2
- ansible/module_utils/facts/virtual/linux.py +1 -2
- ansible/module_utils/facts/virtual/netbsd.py +1 -2
- ansible/module_utils/facts/virtual/openbsd.py +1 -2
- ansible/module_utils/facts/virtual/sunos.py +1 -2
- ansible/module_utils/facts/virtual/sysctl.py +1 -2
- ansible/module_utils/json_utils.py +1 -2
- ansible/module_utils/parsing/convert_bool.py +1 -2
- ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 +5 -2
- ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
- ansible/module_utils/pycompat24.py +24 -4
- ansible/module_utils/service.py +31 -5
- ansible/module_utils/six/__init__.py +1 -1
- ansible/module_utils/splitter.py +1 -2
- ansible/module_utils/urls.py +335 -1052
- ansible/module_utils/yumdnf.py +13 -35
- ansible/modules/add_host.py +1 -2
- ansible/modules/apt.py +38 -22
- ansible/modules/apt_key.py +1 -2
- ansible/modules/apt_repository.py +18 -10
- ansible/modules/assemble.py +1 -2
- ansible/modules/assert.py +8 -4
- ansible/modules/async_status.py +17 -14
- ansible/modules/async_wrapper.py +11 -9
- ansible/modules/blockinfile.py +2 -3
- ansible/modules/command.py +1 -2
- ansible/modules/copy.py +4 -76
- ansible/modules/cron.py +3 -3
- ansible/modules/deb822_repository.py +5 -5
- ansible/modules/debconf.py +19 -8
- ansible/modules/debug.py +1 -2
- ansible/modules/dnf.py +59 -186
- ansible/modules/dnf5.py +57 -43
- ansible/modules/dpkg_selections.py +1 -2
- ansible/modules/expect.py +23 -15
- ansible/modules/fail.py +1 -2
- ansible/modules/fetch.py +1 -2
- ansible/modules/file.py +4 -3
- ansible/modules/find.py +18 -5
- ansible/modules/gather_facts.py +1 -2
- ansible/modules/get_url.py +2 -3
- ansible/modules/getent.py +2 -3
- ansible/modules/git.py +28 -17
- ansible/modules/group.py +1 -2
- ansible/modules/group_by.py +1 -2
- ansible/modules/hostname.py +2 -19
- ansible/modules/import_playbook.py +1 -2
- ansible/modules/import_role.py +9 -2
- ansible/modules/import_tasks.py +1 -2
- ansible/modules/include_role.py +1 -2
- ansible/modules/include_tasks.py +1 -2
- ansible/modules/include_vars.py +1 -2
- ansible/modules/iptables.py +38 -21
- ansible/modules/known_hosts.py +15 -8
- ansible/modules/lineinfile.py +1 -6
- ansible/modules/meta.py +3 -2
- ansible/modules/package.py +6 -5
- ansible/modules/package_facts.py +1 -2
- ansible/modules/pause.py +2 -3
- ansible/modules/ping.py +1 -2
- ansible/modules/pip.py +40 -20
- ansible/modules/raw.py +1 -2
- ansible/modules/reboot.py +1 -2
- ansible/modules/replace.py +7 -5
- ansible/modules/rpm_key.py +1 -2
- ansible/modules/script.py +1 -2
- ansible/modules/service.py +3 -21
- ansible/modules/service_facts.py +3 -12
- ansible/modules/set_fact.py +1 -2
- ansible/modules/set_stats.py +1 -2
- ansible/modules/setup.py +1 -2
- ansible/modules/shell.py +1 -2
- ansible/modules/slurp.py +1 -2
- ansible/modules/stat.py +1 -2
- ansible/modules/subversion.py +2 -3
- ansible/modules/systemd.py +12 -9
- ansible/modules/systemd_service.py +12 -9
- ansible/modules/sysvinit.py +7 -2
- ansible/modules/tempfile.py +7 -2
- ansible/modules/template.py +2 -3
- ansible/modules/unarchive.py +13 -3
- ansible/modules/uri.py +5 -8
- ansible/modules/user.py +11 -10
- ansible/modules/validate_argument_spec.py +15 -16
- ansible/modules/wait_for.py +1 -2
- ansible/modules/wait_for_connection.py +1 -2
- ansible/modules/yum_repository.py +2 -3
- ansible/parsing/__init__.py +1 -3
- ansible/parsing/ajson.py +1 -3
- ansible/parsing/dataloader.py +23 -12
- ansible/parsing/mod_args.py +14 -8
- ansible/parsing/plugin_docs.py +1 -2
- ansible/parsing/quoting.py +1 -3
- ansible/parsing/splitter.py +1 -3
- ansible/parsing/utils/__init__.py +1 -3
- ansible/parsing/utils/addresses.py +1 -3
- ansible/parsing/utils/jsonify.py +1 -3
- ansible/parsing/utils/yaml.py +1 -3
- ansible/parsing/vault/__init__.py +6 -8
- ansible/parsing/yaml/__init__.py +1 -3
- ansible/parsing/yaml/constructor.py +1 -3
- ansible/parsing/yaml/dumper.py +1 -3
- ansible/parsing/yaml/loader.py +1 -3
- ansible/parsing/yaml/objects.py +1 -3
- ansible/playbook/__init__.py +1 -3
- ansible/playbook/attribute.py +1 -3
- ansible/playbook/base.py +2 -3
- ansible/playbook/block.py +1 -3
- ansible/playbook/collectionsearch.py +1 -2
- ansible/playbook/conditional.py +1 -3
- ansible/playbook/delegatable.py +1 -0
- ansible/playbook/handler.py +2 -4
- ansible/playbook/handler_task_include.py +1 -3
- ansible/playbook/helpers.py +1 -4
- ansible/playbook/included_file.py +4 -4
- ansible/playbook/loop_control.py +1 -3
- ansible/playbook/notifiable.py +1 -0
- ansible/playbook/play.py +1 -3
- ansible/playbook/play_context.py +1 -3
- ansible/playbook/playbook_include.py +1 -3
- ansible/playbook/role/__init__.py +1 -3
- ansible/playbook/role/definition.py +1 -3
- ansible/playbook/role/include.py +1 -3
- ansible/playbook/role/metadata.py +1 -3
- ansible/playbook/role/requirement.py +1 -3
- ansible/playbook/role_include.py +2 -10
- ansible/playbook/taggable.py +1 -3
- ansible/playbook/task.py +2 -4
- ansible/playbook/task_include.py +1 -3
- ansible/plugins/__init__.py +4 -5
- ansible/plugins/action/__init__.py +20 -14
- ansible/plugins/action/add_host.py +2 -4
- ansible/plugins/action/assemble.py +2 -3
- ansible/plugins/action/assert.py +1 -2
- ansible/plugins/action/async_status.py +1 -2
- ansible/plugins/action/command.py +1 -2
- ansible/plugins/action/copy.py +12 -9
- ansible/plugins/action/debug.py +1 -2
- ansible/plugins/action/dnf.py +4 -4
- ansible/plugins/action/fail.py +1 -2
- ansible/plugins/action/fetch.py +2 -3
- ansible/plugins/action/gather_facts.py +3 -4
- ansible/plugins/action/group_by.py +1 -2
- ansible/plugins/action/include_vars.py +1 -2
- ansible/plugins/action/normal.py +1 -2
- ansible/plugins/action/package.py +36 -21
- ansible/plugins/action/pause.py +1 -2
- ansible/plugins/action/raw.py +2 -3
- ansible/plugins/action/reboot.py +30 -15
- ansible/plugins/action/script.py +3 -4
- ansible/plugins/action/service.py +1 -2
- ansible/plugins/action/set_fact.py +1 -2
- ansible/plugins/action/set_stats.py +1 -2
- ansible/plugins/action/shell.py +1 -2
- ansible/plugins/action/template.py +1 -2
- ansible/plugins/action/unarchive.py +1 -2
- ansible/plugins/action/uri.py +2 -3
- ansible/plugins/action/validate_argument_spec.py +1 -2
- ansible/plugins/action/wait_for_connection.py +2 -3
- ansible/plugins/become/__init__.py +1 -2
- ansible/plugins/become/runas.py +1 -2
- ansible/plugins/become/su.py +1 -2
- ansible/plugins/become/sudo.py +1 -2
- ansible/plugins/cache/__init__.py +3 -3
- ansible/plugins/cache/base.py +1 -2
- ansible/plugins/cache/jsonfile.py +1 -3
- ansible/plugins/cache/memory.py +1 -2
- ansible/plugins/callback/__init__.py +2 -4
- ansible/plugins/callback/default.py +2 -3
- ansible/plugins/callback/junit.py +1 -2
- ansible/plugins/callback/minimal.py +1 -3
- ansible/plugins/callback/oneline.py +1 -3
- ansible/plugins/callback/tree.py +1 -2
- ansible/plugins/cliconf/__init__.py +1 -2
- ansible/plugins/connection/__init__.py +4 -5
- ansible/plugins/connection/local.py +3 -4
- ansible/plugins/connection/paramiko_ssh.py +1 -2
- ansible/plugins/connection/psrp.py +1 -2
- ansible/plugins/connection/ssh.py +18 -54
- ansible/plugins/connection/winrm.py +3 -4
- ansible/plugins/doc_fragments/action_common_attributes.py +2 -3
- ansible/plugins/doc_fragments/action_core.py +3 -4
- ansible/plugins/doc_fragments/backup.py +1 -2
- ansible/plugins/doc_fragments/connection_pipelining.py +1 -2
- ansible/plugins/doc_fragments/constructed.py +1 -2
- ansible/plugins/doc_fragments/decrypt.py +2 -3
- ansible/plugins/doc_fragments/default_callback.py +1 -2
- ansible/plugins/doc_fragments/files.py +1 -2
- ansible/plugins/doc_fragments/inventory_cache.py +1 -2
- ansible/plugins/doc_fragments/result_format_callback.py +1 -2
- ansible/plugins/doc_fragments/return_common.py +1 -2
- ansible/plugins/doc_fragments/shell_common.py +1 -2
- ansible/plugins/doc_fragments/shell_windows.py +1 -2
- ansible/plugins/doc_fragments/template_common.py +1 -2
- ansible/plugins/doc_fragments/url.py +1 -2
- ansible/plugins/doc_fragments/url_windows.py +1 -2
- ansible/plugins/doc_fragments/validate.py +1 -2
- ansible/plugins/doc_fragments/vars_plugin_staging.py +1 -2
- ansible/plugins/filter/__init__.py +1 -2
- ansible/plugins/filter/b64decode.yml +8 -8
- ansible/plugins/filter/b64encode.yml +4 -4
- ansible/plugins/filter/comment.yml +1 -1
- ansible/plugins/filter/core.py +11 -9
- ansible/plugins/filter/encryption.py +1 -3
- ansible/plugins/filter/extract.yml +1 -1
- ansible/plugins/filter/from_yaml_all.yml +1 -1
- ansible/plugins/filter/human_readable.yml +3 -3
- ansible/plugins/filter/human_to_bytes.yml +2 -2
- ansible/plugins/filter/mandatory.yml +1 -1
- ansible/plugins/filter/mathstuff.py +2 -2
- ansible/plugins/filter/password_hash.yml +3 -2
- ansible/plugins/filter/regex_replace.yml +15 -0
- ansible/plugins/filter/regex_search.yml +12 -0
- ansible/plugins/filter/strftime.yml +2 -9
- ansible/plugins/filter/to_datetime.yml +17 -2
- ansible/plugins/filter/to_nice_json.yml +4 -0
- ansible/plugins/filter/union.yml +1 -1
- ansible/plugins/filter/urls.py +1 -2
- ansible/plugins/filter/urlsplit.py +1 -2
- ansible/plugins/filter/zip.yml +2 -2
- ansible/plugins/filter/zip_longest.yml +1 -1
- ansible/plugins/httpapi/__init__.py +1 -2
- ansible/plugins/inventory/__init__.py +2 -4
- ansible/plugins/inventory/advanced_host_list.py +1 -2
- ansible/plugins/inventory/auto.py +2 -3
- ansible/plugins/inventory/constructed.py +3 -2
- ansible/plugins/inventory/generator.py +1 -2
- ansible/plugins/inventory/host_list.py +1 -2
- ansible/plugins/inventory/ini.py +1 -2
- ansible/plugins/inventory/script.py +122 -3
- ansible/plugins/inventory/toml.py +1 -2
- ansible/plugins/inventory/yaml.py +3 -4
- ansible/plugins/list.py +2 -3
- ansible/plugins/loader.py +10 -11
- ansible/plugins/lookup/__init__.py +1 -3
- ansible/plugins/lookup/config.py +19 -15
- ansible/plugins/lookup/csvfile.py +16 -7
- ansible/plugins/lookup/dict.py +2 -3
- ansible/plugins/lookup/env.py +4 -4
- ansible/plugins/lookup/file.py +1 -2
- ansible/plugins/lookup/fileglob.py +1 -2
- ansible/plugins/lookup/first_found.py +8 -7
- ansible/plugins/lookup/indexed_items.py +1 -2
- ansible/plugins/lookup/ini.py +6 -3
- ansible/plugins/lookup/inventory_hostnames.py +1 -2
- ansible/plugins/lookup/items.py +1 -2
- ansible/plugins/lookup/lines.py +1 -2
- ansible/plugins/lookup/list.py +1 -3
- ansible/plugins/lookup/nested.py +1 -2
- ansible/plugins/lookup/password.py +16 -14
- ansible/plugins/lookup/pipe.py +1 -2
- ansible/plugins/lookup/random_choice.py +1 -2
- ansible/plugins/lookup/sequence.py +21 -52
- ansible/plugins/lookup/subelements.py +1 -2
- ansible/plugins/lookup/template.py +1 -2
- ansible/plugins/lookup/together.py +1 -2
- ansible/plugins/lookup/unvault.py +1 -2
- ansible/plugins/lookup/url.py +9 -3
- ansible/plugins/lookup/varnames.py +1 -2
- ansible/plugins/lookup/vars.py +1 -2
- ansible/plugins/netconf/__init__.py +1 -2
- ansible/plugins/shell/__init__.py +3 -4
- ansible/plugins/shell/cmd.py +1 -2
- ansible/plugins/shell/powershell.py +1 -2
- ansible/plugins/shell/sh.py +1 -2
- ansible/plugins/strategy/__init__.py +33 -21
- ansible/plugins/strategy/debug.py +1 -2
- ansible/plugins/strategy/free.py +17 -7
- ansible/plugins/strategy/host_pinned.py +1 -3
- ansible/plugins/strategy/linear.py +22 -22
- ansible/plugins/terminal/__init__.py +2 -3
- ansible/plugins/test/__init__.py +1 -2
- ansible/plugins/test/change.yml +1 -1
- ansible/plugins/test/changed.yml +1 -1
- ansible/plugins/test/contains.yml +1 -1
- ansible/plugins/test/core.py +2 -4
- ansible/plugins/test/exists.yml +1 -1
- ansible/plugins/test/failed.yml +1 -1
- ansible/plugins/test/failure.yml +1 -1
- ansible/plugins/test/files.py +1 -3
- ansible/plugins/test/finished.yml +3 -3
- ansible/plugins/test/issuperset.yml +1 -1
- ansible/plugins/test/match.yml +1 -1
- ansible/plugins/test/mathstuff.py +1 -2
- ansible/plugins/test/reachable.yml +1 -1
- ansible/plugins/test/regex.yml +1 -1
- ansible/plugins/test/search.yml +1 -1
- ansible/plugins/test/skip.yml +1 -1
- ansible/plugins/test/skipped.yml +1 -1
- ansible/plugins/test/started.yml +1 -1
- ansible/plugins/test/succeeded.yml +1 -1
- ansible/plugins/test/success.yml +1 -1
- ansible/plugins/test/successful.yml +1 -1
- ansible/plugins/test/superset.yml +1 -1
- ansible/plugins/test/unreachable.yml +1 -1
- ansible/plugins/test/uri.py +1 -3
- ansible/plugins/vars/__init__.py +1 -2
- ansible/plugins/vars/host_group_vars.py +2 -3
- ansible/release.py +3 -5
- ansible/template/__init__.py +24 -37
- ansible/template/native_helpers.py +1 -3
- ansible/template/template.py +1 -3
- ansible/template/vars.py +1 -0
- ansible/utils/__init__.py +1 -3
- ansible/utils/cmd_functions.py +1 -2
- ansible/utils/collection_loader/__init__.py +1 -2
- ansible/utils/collection_loader/_collection_config.py +1 -2
- ansible/utils/collection_loader/_collection_finder.py +1 -2
- ansible/utils/collection_loader/_collection_meta.py +1 -2
- ansible/utils/color.py +1 -2
- ansible/utils/context_objects.py +1 -4
- ansible/utils/display.py +88 -30
- ansible/utils/encrypt.py +4 -105
- ansible/utils/fqcn.py +1 -2
- ansible/utils/galaxy.py +1 -3
- ansible/utils/hashing.py +1 -3
- ansible/utils/helpers.py +1 -3
- ansible/utils/jsonrpc.py +1 -2
- ansible/utils/listify.py +1 -3
- ansible/utils/lock.py +1 -3
- ansible/utils/multiprocessing.py +1 -3
- ansible/utils/native_jinja.py +1 -3
- ansible/utils/path.py +1 -2
- ansible/utils/plugin_docs.py +7 -6
- ansible/utils/py3compat.py +17 -55
- ansible/utils/sentinel.py +1 -3
- ansible/utils/shlex.py +1 -3
- ansible/utils/singleton.py +1 -3
- ansible/utils/ssh_functions.py +1 -3
- ansible/utils/unicode.py +1 -3
- ansible/utils/unsafe_proxy.py +1 -2
- ansible/utils/vars.py +6 -8
- ansible/utils/version.py +1 -3
- ansible/vars/clean.py +1 -3
- ansible/vars/fact_cache.py +1 -2
- ansible/vars/hostvars.py +3 -7
- ansible/vars/manager.py +24 -6
- ansible/vars/reserved.py +1 -3
- {ansible_core-2.16.6.data → ansible_core-2.17.0rc1.data}/scripts/ansible-test +1 -2
- {ansible_core-2.16.6.dist-info → ansible_core-2.17.0rc1.dist-info}/METADATA +3 -3
- ansible_core-2.17.0rc1.dist-info/RECORD +987 -0
- ansible_test/__init__.py +1 -2
- ansible_test/_data/completion/docker.txt +7 -9
- ansible_test/_data/completion/remote.txt +6 -7
- ansible_test/_data/requirements/ansible-test.txt +0 -2
- ansible_test/_data/requirements/constraints.txt +1 -6
- ansible_test/_data/requirements/sanity.ansible-doc.txt +3 -3
- ansible_test/_data/requirements/sanity.changelog.txt +3 -3
- ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
- ansible_test/_data/requirements/sanity.mypy.txt +12 -12
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +7 -7
- ansible_test/_data/requirements/sanity.runtime-metadata.txt +1 -1
- ansible_test/_data/requirements/sanity.validate-modules.txt +3 -3
- ansible_test/_data/requirements/sanity.yamllint.txt +2 -2
- ansible_test/_internal/bootstrap.py +2 -2
- ansible_test/_internal/classification/__init__.py +0 -5
- ansible_test/_internal/commands/integration/cloud/cs.py +1 -1
- ansible_test/_internal/commands/integration/cloud/nios.py +1 -1
- ansible_test/_internal/commands/sanity/__init__.py +1 -27
- ansible_test/_internal/commands/sanity/import.py +0 -18
- ansible_test/_internal/commands/sanity/mypy.py +7 -10
- ansible_test/_internal/commands/units/__init__.py +1 -1
- ansible_test/_internal/config.py +0 -1
- ansible_test/_internal/content_config.py +0 -5
- ansible_test/_internal/coverage_util.py +0 -1
- ansible_test/_internal/docker_util.py +1 -1
- ansible_test/_internal/host_profiles.py +5 -4
- ansible_test/_internal/python_requirements.py +1 -119
- ansible_test/_internal/ssh.py +1 -0
- ansible_test/_internal/util.py +1 -1
- ansible_test/_internal/util_common.py +1 -1
- ansible_test/_internal/venv.py +10 -108
- ansible_test/_util/__init__.py +1 -2
- ansible_test/_util/controller/sanity/mypy/ansible-core.ini +0 -6
- ansible_test/_util/controller/sanity/mypy/modules.ini +0 -6
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +0 -1
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +0 -1
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +0 -1
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +0 -1
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +0 -1
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +1 -2
- ansible_test/_util/controller/sanity/shellcheck/exclude.txt +0 -1
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -59
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +0 -7
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +10 -2
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -1
- ansible_test/_util/controller/sanity/yamllint/yamllinter.py +16 -4
- ansible_test/_util/target/__init__.py +1 -2
- ansible_test/_util/target/cli/ansible_test_cli_stub.py +1 -2
- ansible_test/_util/target/common/constants.py +1 -4
- ansible_test/_util/target/injector/python.py +4 -19
- ansible_test/_util/target/pytest/plugins/ansible_forked.py +2 -9
- ansible_test/_util/target/pytest/plugins/ansible_pytest_collections.py +1 -5
- ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py +1 -2
- ansible_test/_util/target/sanity/compile/compile.py +3 -12
- ansible_test/_util/target/sanity/import/importer.py +1 -12
- ansible_test/_util/target/setup/bootstrap.sh +49 -105
- ansible_test/_util/target/setup/probe_cgroups.py +1 -2
- ansible_test/_util/target/setup/quiet_pip.py +1 -16
- ansible_test/_util/target/setup/requirements.py +9 -2
- ansible_test/_util/target/tools/virtualenvcheck.py +1 -2
- ansible_test/_util/target/tools/yamlcheck.py +1 -2
- ansible/module_utils/common/_json_compat.py +0 -16
- ansible/module_utils/compat/_selectors2.py +0 -655
- ansible/modules/yum.py +0 -1821
- ansible/plugins/action/yum.py +0 -111
- ansible_core-2.16.6.dist-info/RECORD +0 -1009
- ansible_test/_util/controller/sanity/code-smell/future-import-boilerplate.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/future-import-boilerplate.py +0 -46
- ansible_test/_util/controller/sanity/code-smell/metaclass-boilerplate.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/metaclass-boilerplate.py +0 -44
- ansible_test/_util/controller/sanity/code-smell/no-basestring.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/no-basestring.py +0 -21
- ansible_test/_util/controller/sanity/code-smell/no-dict-iteritems.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/no-dict-iteritems.py +0 -21
- ansible_test/_util/controller/sanity/code-smell/no-dict-iterkeys.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/no-dict-iterkeys.py +0 -21
- ansible_test/_util/controller/sanity/code-smell/no-dict-itervalues.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/no-dict-itervalues.py +0 -21
- ansible_test/_util/controller/sanity/code-smell/no-main-display.json +0 -10
- ansible_test/_util/controller/sanity/code-smell/no-main-display.py +0 -21
- ansible_test/_util/controller/sanity/code-smell/no-unicode-literals.json +0 -7
- ansible_test/_util/controller/sanity/code-smell/no-unicode-literals.py +0 -21
- ansible_test/_util/controller/tools/sslcheck.py +0 -22
- ansible_test/_util/target/common/__init__.py +0 -2
- {ansible_core-2.16.6.dist-info → ansible_core-2.17.0rc1.dist-info}/COPYING +0 -0
- {ansible_core-2.16.6.dist-info → ansible_core-2.17.0rc1.dist-info}/WHEEL +0 -0
- {ansible_core-2.16.6.dist-info → ansible_core-2.17.0rc1.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.16.6.dist-info → ansible_core-2.17.0rc1.dist-info}/top_level.txt +0 -0
ansible/module_utils/urls.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
1
2
|
# This code is part of Ansible, but is an independent component.
|
|
2
3
|
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
3
4
|
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
@@ -6,56 +7,51 @@
|
|
|
6
7
|
#
|
|
7
8
|
# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
|
|
8
9
|
# Copyright (c), Toshio Kuratomi <tkuratomi@ansible.com>, 2015
|
|
10
|
+
# Copyright: Contributors to the Ansible project
|
|
9
11
|
#
|
|
10
12
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
|
11
|
-
#
|
|
12
|
-
# The match_hostname function and supporting code is under the terms and
|
|
13
|
-
# conditions of the Python Software Foundation License. They were taken from
|
|
14
|
-
# the Python3 standard library and adapted for use in Python2. See comments in the
|
|
15
|
-
# source for which code precisely is under this License.
|
|
16
|
-
#
|
|
17
|
-
# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0)
|
|
18
13
|
|
|
19
14
|
|
|
20
15
|
'''
|
|
21
|
-
The **urls** utils module offers a replacement for the
|
|
16
|
+
The **urls** utils module offers a replacement for the urllib python library.
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
urllib is the python stdlib way to retrieve files from the Internet but it
|
|
24
19
|
lacks some security features (around verifying SSL certificates) that users
|
|
25
20
|
should care about in most situations. Using the functions in this module corrects
|
|
26
|
-
deficiencies in the
|
|
21
|
+
deficiencies in the urllib module wherever possible.
|
|
27
22
|
|
|
28
23
|
There are also third-party libraries (for instance, requests) which can be used
|
|
29
|
-
to replace
|
|
24
|
+
to replace urllib with a more secure library. However, all third party libraries
|
|
30
25
|
require that the library be installed on the managed machine. That is an extra step
|
|
31
26
|
for users making use of a module. If possible, avoid third party libraries by using
|
|
32
27
|
this code instead.
|
|
33
28
|
'''
|
|
34
29
|
|
|
35
|
-
from __future__ import
|
|
36
|
-
__metaclass__ = type
|
|
30
|
+
from __future__ import annotations
|
|
37
31
|
|
|
38
|
-
import atexit
|
|
39
32
|
import base64
|
|
33
|
+
import email.mime.application
|
|
40
34
|
import email.mime.multipart
|
|
41
35
|
import email.mime.nonmultipart
|
|
42
|
-
import email.mime.application
|
|
43
36
|
import email.parser
|
|
37
|
+
import email.policy
|
|
44
38
|
import email.utils
|
|
45
|
-
import
|
|
46
|
-
import io
|
|
39
|
+
import http.client
|
|
47
40
|
import mimetypes
|
|
48
41
|
import netrc
|
|
49
42
|
import os
|
|
50
43
|
import platform
|
|
51
44
|
import re
|
|
52
45
|
import socket
|
|
53
|
-
import sys
|
|
54
46
|
import tempfile
|
|
55
47
|
import traceback
|
|
56
48
|
import types # pylint: disable=unused-import
|
|
57
|
-
|
|
49
|
+
import urllib.error
|
|
50
|
+
import urllib.request
|
|
58
51
|
from contextlib import contextmanager
|
|
52
|
+
from http import cookiejar
|
|
53
|
+
from urllib.parse import unquote, urlparse, urlunparse
|
|
54
|
+
from urllib.request import BaseHandler
|
|
59
55
|
|
|
60
56
|
try:
|
|
61
57
|
import gzip
|
|
@@ -68,123 +64,16 @@ except ImportError:
|
|
|
68
64
|
else:
|
|
69
65
|
GzipFile = gzip.GzipFile # type: ignore[assignment,misc]
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
import email.policy
|
|
73
|
-
except ImportError:
|
|
74
|
-
# Py2
|
|
75
|
-
import email.generator
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
import httplib
|
|
79
|
-
except ImportError:
|
|
80
|
-
# Python 3
|
|
81
|
-
import http.client as httplib # type: ignore[no-redef]
|
|
82
|
-
|
|
83
|
-
import ansible.module_utils.compat.typing as t
|
|
84
|
-
import ansible.module_utils.six.moves.http_cookiejar as cookiejar
|
|
85
|
-
import ansible.module_utils.six.moves.urllib.error as urllib_error
|
|
86
|
-
|
|
67
|
+
from ansible.module_utils.basic import missing_required_lib
|
|
87
68
|
from ansible.module_utils.common.collections import Mapping, is_sequence
|
|
88
|
-
from ansible.module_utils.six import PY2, PY3, string_types
|
|
89
|
-
from ansible.module_utils.six.moves import cStringIO
|
|
90
|
-
from ansible.module_utils.basic import get_distribution, missing_required_lib
|
|
91
69
|
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
|
92
70
|
|
|
93
|
-
try:
|
|
94
|
-
# python3
|
|
95
|
-
import urllib.request as urllib_request
|
|
96
|
-
from urllib.request import AbstractHTTPHandler, BaseHandler
|
|
97
|
-
except ImportError:
|
|
98
|
-
# python2
|
|
99
|
-
import urllib2 as urllib_request # type: ignore[no-redef]
|
|
100
|
-
from urllib2 import AbstractHTTPHandler, BaseHandler # type: ignore[no-redef]
|
|
101
|
-
|
|
102
|
-
urllib_request.HTTPRedirectHandler.http_error_308 = urllib_request.HTTPRedirectHandler.http_error_307 # type: ignore[attr-defined,assignment]
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, unquote
|
|
106
|
-
HAS_URLPARSE = True
|
|
107
|
-
except Exception:
|
|
108
|
-
HAS_URLPARSE = False
|
|
109
|
-
|
|
110
71
|
try:
|
|
111
72
|
import ssl
|
|
112
73
|
HAS_SSL = True
|
|
113
74
|
except Exception:
|
|
114
75
|
HAS_SSL = False
|
|
115
76
|
|
|
116
|
-
try:
|
|
117
|
-
# SNI Handling needs python2.7.9's SSLContext
|
|
118
|
-
from ssl import create_default_context, SSLContext # pylint: disable=unused-import
|
|
119
|
-
HAS_SSLCONTEXT = True
|
|
120
|
-
except ImportError:
|
|
121
|
-
HAS_SSLCONTEXT = False
|
|
122
|
-
|
|
123
|
-
# SNI Handling for python < 2.7.9 with urllib3 support
|
|
124
|
-
HAS_URLLIB3_PYOPENSSLCONTEXT = False
|
|
125
|
-
HAS_URLLIB3_SSL_WRAP_SOCKET = False
|
|
126
|
-
if not HAS_SSLCONTEXT:
|
|
127
|
-
try:
|
|
128
|
-
# urllib3>=1.15
|
|
129
|
-
try:
|
|
130
|
-
from urllib3.contrib.pyopenssl import PyOpenSSLContext
|
|
131
|
-
except Exception:
|
|
132
|
-
from requests.packages.urllib3.contrib.pyopenssl import PyOpenSSLContext # type: ignore[no-redef]
|
|
133
|
-
HAS_URLLIB3_PYOPENSSLCONTEXT = True
|
|
134
|
-
except Exception:
|
|
135
|
-
# urllib3<1.15,>=1.6
|
|
136
|
-
try:
|
|
137
|
-
try:
|
|
138
|
-
from urllib3.contrib.pyopenssl import ssl_wrap_socket # type: ignore[attr-defined]
|
|
139
|
-
except Exception:
|
|
140
|
-
from requests.packages.urllib3.contrib.pyopenssl import ssl_wrap_socket
|
|
141
|
-
HAS_URLLIB3_SSL_WRAP_SOCKET = True
|
|
142
|
-
except Exception:
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
# Select a protocol that includes all secure tls protocols
|
|
146
|
-
# Exclude insecure ssl protocols if possible
|
|
147
|
-
|
|
148
|
-
if HAS_SSL:
|
|
149
|
-
# If we can't find extra tls methods, ssl.PROTOCOL_TLSv1 is sufficient
|
|
150
|
-
PROTOCOL = ssl.PROTOCOL_TLSv1
|
|
151
|
-
if not HAS_SSLCONTEXT and HAS_SSL:
|
|
152
|
-
try:
|
|
153
|
-
import ctypes
|
|
154
|
-
import ctypes.util
|
|
155
|
-
except ImportError:
|
|
156
|
-
# python 2.4 (likely rhel5 which doesn't have tls1.1 support in its openssl)
|
|
157
|
-
pass
|
|
158
|
-
else:
|
|
159
|
-
libssl_name = ctypes.util.find_library('ssl')
|
|
160
|
-
libssl = ctypes.CDLL(libssl_name)
|
|
161
|
-
for method in ('TLSv1_1_method', 'TLSv1_2_method'):
|
|
162
|
-
try:
|
|
163
|
-
libssl[method] # pylint: disable=pointless-statement
|
|
164
|
-
# Found something - we'll let openssl autonegotiate and hope
|
|
165
|
-
# the server has disabled sslv2 and 3. best we can do.
|
|
166
|
-
PROTOCOL = ssl.PROTOCOL_SSLv23
|
|
167
|
-
break
|
|
168
|
-
except AttributeError:
|
|
169
|
-
pass
|
|
170
|
-
del libssl
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# The following makes it easier for us to script updates of the bundled backports.ssl_match_hostname
|
|
174
|
-
# The bundled backports.ssl_match_hostname should really be moved into its own file for processing
|
|
175
|
-
_BUNDLED_METADATA = {"pypi_name": "backports.ssl_match_hostname", "version": "3.7.0.1"}
|
|
176
|
-
|
|
177
|
-
LOADED_VERIFY_LOCATIONS = set() # type: t.Set[str]
|
|
178
|
-
|
|
179
|
-
HAS_MATCH_HOSTNAME = True
|
|
180
|
-
try:
|
|
181
|
-
from ssl import match_hostname, CertificateError
|
|
182
|
-
except ImportError:
|
|
183
|
-
try:
|
|
184
|
-
from backports.ssl_match_hostname import match_hostname, CertificateError # type: ignore[assignment]
|
|
185
|
-
except ImportError:
|
|
186
|
-
HAS_MATCH_HOSTNAME = False
|
|
187
|
-
|
|
188
77
|
HAS_CRYPTOGRAPHY = True
|
|
189
78
|
try:
|
|
190
79
|
from cryptography import x509
|
|
@@ -226,7 +115,7 @@ try:
|
|
|
226
115
|
if self._context:
|
|
227
116
|
return
|
|
228
117
|
|
|
229
|
-
parsed =
|
|
118
|
+
parsed = urlparse(req.get_full_url())
|
|
230
119
|
|
|
231
120
|
auth_header = self.get_auth_value(headers)
|
|
232
121
|
if not auth_header:
|
|
@@ -259,7 +148,7 @@ try:
|
|
|
259
148
|
cbt = gssapi.raw.ChannelBindings(application_data=b"tls-server-end-point:" + cert_hash)
|
|
260
149
|
|
|
261
150
|
# TODO: We could add another option that is set to include the port in the SPN if desired in the future.
|
|
262
|
-
target = gssapi.Name("HTTP@%s" % parsed
|
|
151
|
+
target = gssapi.Name("HTTP@%s" % parsed.hostname, gssapi.NameType.hostbased_service)
|
|
263
152
|
self._context = gssapi.SecurityContext(usage="initiate", name=target, creds=cred, channel_bindings=cbt)
|
|
264
153
|
|
|
265
154
|
resp = None
|
|
@@ -284,213 +173,9 @@ except ImportError:
|
|
|
284
173
|
GSSAPI_IMP_ERR = traceback.format_exc()
|
|
285
174
|
HTTPGSSAPIAuthHandler = None # type: types.ModuleType | None # type: ignore[no-redef]
|
|
286
175
|
|
|
287
|
-
if not HAS_MATCH_HOSTNAME:
|
|
288
|
-
# The following block of code is under the terms and conditions of the
|
|
289
|
-
# Python Software Foundation License
|
|
290
|
-
|
|
291
|
-
# The match_hostname() function from Python 3.4, essential when using SSL.
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
# Divergence: Python-3.7+'s _ssl has this exception type but older Pythons do not
|
|
295
|
-
from _ssl import SSLCertVerificationError
|
|
296
|
-
CertificateError = SSLCertVerificationError # type: ignore[misc]
|
|
297
|
-
except ImportError:
|
|
298
|
-
class CertificateError(ValueError): # type: ignore[no-redef]
|
|
299
|
-
pass
|
|
300
|
-
|
|
301
|
-
def _dnsname_match(dn, hostname):
|
|
302
|
-
"""Matching according to RFC 6125, section 6.4.3
|
|
303
|
-
|
|
304
|
-
- Hostnames are compared lower case.
|
|
305
|
-
- For IDNA, both dn and hostname must be encoded as IDN A-label (ACE).
|
|
306
|
-
- Partial wildcards like 'www*.example.org', multiple wildcards, sole
|
|
307
|
-
wildcard or wildcards in labels other then the left-most label are not
|
|
308
|
-
supported and a CertificateError is raised.
|
|
309
|
-
- A wildcard must match at least one character.
|
|
310
|
-
"""
|
|
311
|
-
if not dn:
|
|
312
|
-
return False
|
|
313
|
-
|
|
314
|
-
wildcards = dn.count('*')
|
|
315
|
-
# speed up common case w/o wildcards
|
|
316
|
-
if not wildcards:
|
|
317
|
-
return dn.lower() == hostname.lower()
|
|
318
|
-
|
|
319
|
-
if wildcards > 1:
|
|
320
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
321
|
-
raise CertificateError(
|
|
322
|
-
"too many wildcards in certificate DNS name: %s" % repr(dn))
|
|
323
|
-
|
|
324
|
-
dn_leftmost, sep, dn_remainder = dn.partition('.')
|
|
325
|
-
|
|
326
|
-
if '*' in dn_remainder:
|
|
327
|
-
# Only match wildcard in leftmost segment.
|
|
328
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
329
|
-
raise CertificateError(
|
|
330
|
-
"wildcard can only be present in the leftmost label: "
|
|
331
|
-
"%s." % repr(dn))
|
|
332
|
-
|
|
333
|
-
if not sep:
|
|
334
|
-
# no right side
|
|
335
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
336
|
-
raise CertificateError(
|
|
337
|
-
"sole wildcard without additional labels are not support: "
|
|
338
|
-
"%s." % repr(dn))
|
|
339
|
-
|
|
340
|
-
if dn_leftmost != '*':
|
|
341
|
-
# no partial wildcard matching
|
|
342
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
343
|
-
raise CertificateError(
|
|
344
|
-
"partial wildcards in leftmost label are not supported: "
|
|
345
|
-
"%s." % repr(dn))
|
|
346
|
-
|
|
347
|
-
hostname_leftmost, sep, hostname_remainder = hostname.partition('.')
|
|
348
|
-
if not hostname_leftmost or not sep:
|
|
349
|
-
# wildcard must match at least one char
|
|
350
|
-
return False
|
|
351
|
-
return dn_remainder.lower() == hostname_remainder.lower()
|
|
352
|
-
|
|
353
|
-
def _inet_paton(ipname):
|
|
354
|
-
"""Try to convert an IP address to packed binary form
|
|
355
|
-
|
|
356
|
-
Supports IPv4 addresses on all platforms and IPv6 on platforms with IPv6
|
|
357
|
-
support.
|
|
358
|
-
"""
|
|
359
|
-
# inet_aton() also accepts strings like '1'
|
|
360
|
-
# Divergence: We make sure we have native string type for all python versions
|
|
361
|
-
try:
|
|
362
|
-
b_ipname = to_bytes(ipname, errors='strict')
|
|
363
|
-
except UnicodeError:
|
|
364
|
-
raise ValueError("%s must be an all-ascii string." % repr(ipname))
|
|
365
|
-
|
|
366
|
-
# Set ipname in native string format
|
|
367
|
-
if sys.version_info < (3,):
|
|
368
|
-
n_ipname = b_ipname
|
|
369
|
-
else:
|
|
370
|
-
n_ipname = ipname
|
|
371
|
-
|
|
372
|
-
if n_ipname.count('.') == 3:
|
|
373
|
-
try:
|
|
374
|
-
return socket.inet_aton(n_ipname)
|
|
375
|
-
# Divergence: OSError on late python3. socket.error earlier.
|
|
376
|
-
# Null bytes generate ValueError on python3(we want to raise
|
|
377
|
-
# ValueError anyway), TypeError # earlier
|
|
378
|
-
except (OSError, socket.error, TypeError):
|
|
379
|
-
pass
|
|
380
|
-
|
|
381
|
-
try:
|
|
382
|
-
return socket.inet_pton(socket.AF_INET6, n_ipname)
|
|
383
|
-
# Divergence: OSError on late python3. socket.error earlier.
|
|
384
|
-
# Null bytes generate ValueError on python3(we want to raise
|
|
385
|
-
# ValueError anyway), TypeError # earlier
|
|
386
|
-
except (OSError, socket.error, TypeError):
|
|
387
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
388
|
-
raise ValueError("%s is neither an IPv4 nor an IP6 "
|
|
389
|
-
"address." % repr(ipname))
|
|
390
|
-
except AttributeError:
|
|
391
|
-
# AF_INET6 not available
|
|
392
|
-
pass
|
|
393
|
-
|
|
394
|
-
# Divergence .format() to percent formatting for Python < 2.6
|
|
395
|
-
raise ValueError("%s is not an IPv4 address." % repr(ipname))
|
|
396
|
-
|
|
397
|
-
def _ipaddress_match(ipname, host_ip):
|
|
398
|
-
"""Exact matching of IP addresses.
|
|
399
176
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
"""
|
|
403
|
-
# OpenSSL may add a trailing newline to a subjectAltName's IP address
|
|
404
|
-
ip = _inet_paton(ipname.rstrip())
|
|
405
|
-
return ip == host_ip
|
|
406
|
-
|
|
407
|
-
def match_hostname(cert, hostname): # type: ignore[misc]
|
|
408
|
-
"""Verify that *cert* (in decoded format as returned by
|
|
409
|
-
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
|
|
410
|
-
rules are followed.
|
|
411
|
-
|
|
412
|
-
The function matches IP addresses rather than dNSNames if hostname is a
|
|
413
|
-
valid ipaddress string. IPv4 addresses are supported on all platforms.
|
|
414
|
-
IPv6 addresses are supported on platforms with IPv6 support (AF_INET6
|
|
415
|
-
and inet_pton).
|
|
416
|
-
|
|
417
|
-
CertificateError is raised on failure. On success, the function
|
|
418
|
-
returns nothing.
|
|
419
|
-
"""
|
|
420
|
-
if not cert:
|
|
421
|
-
raise ValueError("empty or no certificate, match_hostname needs a "
|
|
422
|
-
"SSL socket or SSL context with either "
|
|
423
|
-
"CERT_OPTIONAL or CERT_REQUIRED")
|
|
424
|
-
try:
|
|
425
|
-
# Divergence: Deal with hostname as bytes
|
|
426
|
-
host_ip = _inet_paton(to_text(hostname, errors='strict'))
|
|
427
|
-
except UnicodeError:
|
|
428
|
-
# Divergence: Deal with hostname as byte strings.
|
|
429
|
-
# IP addresses should be all ascii, so we consider it not
|
|
430
|
-
# an IP address if this fails
|
|
431
|
-
host_ip = None
|
|
432
|
-
except ValueError:
|
|
433
|
-
# Not an IP address (common case)
|
|
434
|
-
host_ip = None
|
|
435
|
-
dnsnames = []
|
|
436
|
-
san = cert.get('subjectAltName', ())
|
|
437
|
-
for key, value in san:
|
|
438
|
-
if key == 'DNS':
|
|
439
|
-
if host_ip is None and _dnsname_match(value, hostname):
|
|
440
|
-
return
|
|
441
|
-
dnsnames.append(value)
|
|
442
|
-
elif key == 'IP Address':
|
|
443
|
-
if host_ip is not None and _ipaddress_match(value, host_ip):
|
|
444
|
-
return
|
|
445
|
-
dnsnames.append(value)
|
|
446
|
-
if not dnsnames:
|
|
447
|
-
# The subject is only checked when there is no dNSName entry
|
|
448
|
-
# in subjectAltName
|
|
449
|
-
for sub in cert.get('subject', ()):
|
|
450
|
-
for key, value in sub:
|
|
451
|
-
# XXX according to RFC 2818, the most specific Common Name
|
|
452
|
-
# must be used.
|
|
453
|
-
if key == 'commonName':
|
|
454
|
-
if _dnsname_match(value, hostname):
|
|
455
|
-
return
|
|
456
|
-
dnsnames.append(value)
|
|
457
|
-
if len(dnsnames) > 1:
|
|
458
|
-
raise CertificateError("hostname %r doesn't match either of %s" % (hostname, ', '.join(map(repr, dnsnames))))
|
|
459
|
-
elif len(dnsnames) == 1:
|
|
460
|
-
raise CertificateError("hostname %r doesn't match %r" % (hostname, dnsnames[0]))
|
|
461
|
-
else:
|
|
462
|
-
raise CertificateError("no appropriate commonName or subjectAltName fields were found")
|
|
463
|
-
|
|
464
|
-
# End of Python Software Foundation Licensed code
|
|
465
|
-
|
|
466
|
-
HAS_MATCH_HOSTNAME = True
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
# This is a dummy cacert provided for macOS since you need at least 1
|
|
470
|
-
# ca cert, regardless of validity, for Python on macOS to use the
|
|
471
|
-
# keychain functionality in OpenSSL for validating SSL certificates.
|
|
472
|
-
# See: http://mercurial.selenic.com/wiki/CACertificates#Mac_OS_X_10.6_and_higher
|
|
473
|
-
b_DUMMY_CA_CERT = b"""-----BEGIN CERTIFICATE-----
|
|
474
|
-
MIICvDCCAiWgAwIBAgIJAO8E12S7/qEpMA0GCSqGSIb3DQEBBQUAMEkxCzAJBgNV
|
|
475
|
-
BAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFt
|
|
476
|
-
MRAwDgYDVQQKEwdBbnNpYmxlMB4XDTE0MDMxODIyMDAyMloXDTI0MDMxNTIyMDAy
|
|
477
|
-
MlowSTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMQ8wDQYD
|
|
478
|
-
VQQHEwZEdXJoYW0xEDAOBgNVBAoTB0Fuc2libGUwgZ8wDQYJKoZIhvcNAQEBBQAD
|
|
479
|
-
gY0AMIGJAoGBANtvpPq3IlNlRbCHhZAcP6WCzhc5RbsDqyh1zrkmLi0GwcQ3z/r9
|
|
480
|
-
gaWfQBYhHpobK2Tiq11TfraHeNB3/VfNImjZcGpN8Fl3MWwu7LfVkJy3gNNnxkA1
|
|
481
|
-
4Go0/LmIvRFHhbzgfuo9NFgjPmmab9eqXJceqZIlz2C8xA7EeG7ku0+vAgMBAAGj
|
|
482
|
-
gaswgagwHQYDVR0OBBYEFPnN1nPRqNDXGlCqCvdZchRNi/FaMHkGA1UdIwRyMHCA
|
|
483
|
-
FPnN1nPRqNDXGlCqCvdZchRNi/FaoU2kSzBJMQswCQYDVQQGEwJVUzEXMBUGA1UE
|
|
484
|
-
CBMOTm9ydGggQ2Fyb2xpbmExDzANBgNVBAcTBkR1cmhhbTEQMA4GA1UEChMHQW5z
|
|
485
|
-
aWJsZYIJAO8E12S7/qEpMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA
|
|
486
|
-
MUB80IR6knq9K/tY+hvPsZer6eFMzO3JGkRFBh2kn6JdMDnhYGX7AXVHGflrwNQH
|
|
487
|
-
qFy+aenWXsC0ZvrikFxbQnX8GVtDADtVznxOi7XzFw7JOxdsVrpXgSN0eh0aMzvV
|
|
488
|
-
zKPZsZ2miVGclicJHzm5q080b1p/sZtuKIEZk6vZqEg=
|
|
489
|
-
-----END CERTIFICATE-----
|
|
490
|
-
"""
|
|
491
|
-
|
|
492
|
-
b_PEM_CERT_RE = re.compile(
|
|
493
|
-
br'^-----BEGIN CERTIFICATE-----\n.+?-----END CERTIFICATE-----$',
|
|
177
|
+
PEM_CERT_RE = re.compile(
|
|
178
|
+
r'^-----BEGIN CERTIFICATE-----\n.+?-----END CERTIFICATE-----$',
|
|
494
179
|
flags=re.M | re.S
|
|
495
180
|
)
|
|
496
181
|
|
|
@@ -510,142 +195,81 @@ class ProxyError(ConnectionError):
|
|
|
510
195
|
|
|
511
196
|
|
|
512
197
|
class SSLValidationError(ConnectionError):
|
|
513
|
-
"""Failure to connect due to SSL validation failing
|
|
198
|
+
"""Failure to connect due to SSL validation failing
|
|
199
|
+
|
|
200
|
+
No longer used, but kept for backwards compatibility
|
|
201
|
+
"""
|
|
514
202
|
pass
|
|
515
203
|
|
|
516
204
|
|
|
517
205
|
class NoSSLError(SSLValidationError):
|
|
518
|
-
"""Needed to connect to an HTTPS url but no ssl library available to verify the certificate
|
|
206
|
+
"""Needed to connect to an HTTPS url but no ssl library available to verify the certificate
|
|
207
|
+
|
|
208
|
+
No longer used, but kept for backwards compatibility
|
|
209
|
+
"""
|
|
519
210
|
pass
|
|
520
211
|
|
|
521
212
|
|
|
522
213
|
class MissingModuleError(Exception):
|
|
523
214
|
"""Failed to import 3rd party module required by the caller"""
|
|
524
215
|
def __init__(self, message, import_traceback, module=None):
|
|
525
|
-
super(
|
|
216
|
+
super().__init__(message)
|
|
526
217
|
self.import_traceback = import_traceback
|
|
527
218
|
self.module = module
|
|
528
219
|
|
|
529
220
|
|
|
530
|
-
|
|
531
|
-
# against openssl and thus do not have any HTTPS support.
|
|
532
|
-
CustomHTTPSConnection = None
|
|
533
|
-
CustomHTTPSHandler = None
|
|
534
|
-
HTTPSClientAuthHandler = None
|
|
221
|
+
UnixHTTPSHandler = None
|
|
535
222
|
UnixHTTPSConnection = None
|
|
536
|
-
if
|
|
537
|
-
class CustomHTTPSConnection(httplib.HTTPSConnection): # type: ignore[no-redef]
|
|
538
|
-
def __init__(self, client_cert=None, client_key=None, *args, **kwargs):
|
|
539
|
-
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
|
540
|
-
self.context = None
|
|
541
|
-
if HAS_SSLCONTEXT:
|
|
542
|
-
self.context = self._context
|
|
543
|
-
elif HAS_URLLIB3_PYOPENSSLCONTEXT:
|
|
544
|
-
self.context = self._context = PyOpenSSLContext(PROTOCOL)
|
|
545
|
-
|
|
546
|
-
self._client_cert = client_cert
|
|
547
|
-
self._client_key = client_key
|
|
548
|
-
if self.context and self._client_cert:
|
|
549
|
-
self.context.load_cert_chain(self._client_cert, self._client_key)
|
|
550
|
-
|
|
551
|
-
def connect(self):
|
|
552
|
-
"Connect to a host on a given (SSL) port."
|
|
553
|
-
|
|
554
|
-
if hasattr(self, 'source_address'):
|
|
555
|
-
sock = socket.create_connection((self.host, self.port), self.timeout, self.source_address)
|
|
556
|
-
else:
|
|
557
|
-
sock = socket.create_connection((self.host, self.port), self.timeout)
|
|
558
|
-
|
|
559
|
-
server_hostname = self.host
|
|
560
|
-
# Note: self._tunnel_host is not available on py < 2.6 but this code
|
|
561
|
-
# isn't used on py < 2.6 (lack of create_connection)
|
|
562
|
-
if self._tunnel_host:
|
|
563
|
-
self.sock = sock
|
|
564
|
-
self._tunnel()
|
|
565
|
-
server_hostname = self._tunnel_host
|
|
566
|
-
|
|
567
|
-
if HAS_SSLCONTEXT or HAS_URLLIB3_PYOPENSSLCONTEXT:
|
|
568
|
-
self.sock = self.context.wrap_socket(sock, server_hostname=server_hostname)
|
|
569
|
-
elif HAS_URLLIB3_SSL_WRAP_SOCKET:
|
|
570
|
-
self.sock = ssl_wrap_socket(sock, keyfile=self._client_key, cert_reqs=ssl.CERT_NONE, # pylint: disable=used-before-assignment
|
|
571
|
-
certfile=self._client_cert, ssl_version=PROTOCOL, server_hostname=server_hostname)
|
|
572
|
-
else:
|
|
573
|
-
self.sock = ssl.wrap_socket(sock, keyfile=self._client_key, certfile=self._client_cert, ssl_version=PROTOCOL)
|
|
574
|
-
|
|
575
|
-
class CustomHTTPSHandler(urllib_request.HTTPSHandler): # type: ignore[no-redef]
|
|
576
|
-
|
|
577
|
-
def https_open(self, req):
|
|
578
|
-
kwargs = {}
|
|
579
|
-
if HAS_SSLCONTEXT:
|
|
580
|
-
kwargs['context'] = self._context
|
|
581
|
-
return self.do_open(
|
|
582
|
-
functools.partial(
|
|
583
|
-
CustomHTTPSConnection,
|
|
584
|
-
**kwargs
|
|
585
|
-
),
|
|
586
|
-
req
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
https_request = AbstractHTTPHandler.do_request_
|
|
590
|
-
|
|
591
|
-
class HTTPSClientAuthHandler(urllib_request.HTTPSHandler): # type: ignore[no-redef]
|
|
592
|
-
'''Handles client authentication via cert/key
|
|
593
|
-
|
|
594
|
-
This is a fairly lightweight extension on HTTPSHandler, and can be used
|
|
595
|
-
in place of HTTPSHandler
|
|
596
|
-
'''
|
|
597
|
-
|
|
598
|
-
def __init__(self, client_cert=None, client_key=None, unix_socket=None, **kwargs):
|
|
599
|
-
urllib_request.HTTPSHandler.__init__(self, **kwargs)
|
|
600
|
-
self.client_cert = client_cert
|
|
601
|
-
self.client_key = client_key
|
|
602
|
-
self._unix_socket = unix_socket
|
|
603
|
-
|
|
604
|
-
def https_open(self, req):
|
|
605
|
-
return self.do_open(self._build_https_connection, req)
|
|
606
|
-
|
|
607
|
-
def _build_https_connection(self, host, **kwargs):
|
|
608
|
-
try:
|
|
609
|
-
kwargs['context'] = self._context
|
|
610
|
-
except AttributeError:
|
|
611
|
-
pass
|
|
612
|
-
if self._unix_socket:
|
|
613
|
-
return UnixHTTPSConnection(self._unix_socket)(host, **kwargs)
|
|
614
|
-
if not HAS_SSLCONTEXT:
|
|
615
|
-
return CustomHTTPSConnection(host, client_cert=self.client_cert, client_key=self.client_key, **kwargs)
|
|
616
|
-
return httplib.HTTPSConnection(host, **kwargs)
|
|
617
|
-
|
|
223
|
+
if HAS_SSL:
|
|
618
224
|
@contextmanager
|
|
619
225
|
def unix_socket_patch_httpconnection_connect():
|
|
620
|
-
'''Monkey patch ``
|
|
226
|
+
'''Monkey patch ``http.client.HTTPConnection.connect`` to be ``UnixHTTPConnection.connect``
|
|
621
227
|
so that when calling ``super(UnixHTTPSConnection, self).connect()`` we get the
|
|
622
228
|
correct behavior of creating self.sock for the unix socket
|
|
623
229
|
'''
|
|
624
|
-
_connect =
|
|
625
|
-
|
|
230
|
+
_connect = http.client.HTTPConnection.connect
|
|
231
|
+
http.client.HTTPConnection.connect = UnixHTTPConnection.connect
|
|
626
232
|
yield
|
|
627
|
-
|
|
233
|
+
http.client.HTTPConnection.connect = _connect
|
|
628
234
|
|
|
629
|
-
class UnixHTTPSConnection(
|
|
235
|
+
class UnixHTTPSConnection(http.client.HTTPSConnection): # type: ignore[no-redef]
|
|
630
236
|
def __init__(self, unix_socket):
|
|
631
237
|
self._unix_socket = unix_socket
|
|
632
238
|
|
|
633
239
|
def connect(self):
|
|
634
240
|
# This method exists simply to ensure we monkeypatch
|
|
635
|
-
#
|
|
241
|
+
# http.client.HTTPConnection.connect to call UnixHTTPConnection.connect
|
|
636
242
|
with unix_socket_patch_httpconnection_connect():
|
|
637
243
|
# Disable pylint check for the super() call. It complains about UnixHTTPSConnection
|
|
638
244
|
# being a NoneType because of the initial definition above, but it won't actually
|
|
639
245
|
# be a NoneType when this code runs
|
|
640
|
-
|
|
641
|
-
super(UnixHTTPSConnection, self).connect()
|
|
246
|
+
super().connect()
|
|
642
247
|
|
|
643
248
|
def __call__(self, *args, **kwargs):
|
|
644
|
-
|
|
249
|
+
super().__init__(*args, **kwargs)
|
|
645
250
|
return self
|
|
646
251
|
|
|
252
|
+
class UnixHTTPSHandler(urllib.request.HTTPSHandler): # type: ignore[no-redef]
|
|
253
|
+
def __init__(self, unix_socket, **kwargs):
|
|
254
|
+
super().__init__(**kwargs)
|
|
255
|
+
self._unix_socket = unix_socket
|
|
256
|
+
|
|
257
|
+
def https_open(self, req):
|
|
258
|
+
kwargs = {}
|
|
259
|
+
try:
|
|
260
|
+
# deprecated: description='deprecated check_hostname' python_version='3.12'
|
|
261
|
+
kwargs['check_hostname'] = self._check_hostname
|
|
262
|
+
except AttributeError:
|
|
263
|
+
pass
|
|
264
|
+
return self.do_open(
|
|
265
|
+
UnixHTTPSConnection(self._unix_socket),
|
|
266
|
+
req,
|
|
267
|
+
context=self._context,
|
|
268
|
+
**kwargs
|
|
269
|
+
)
|
|
270
|
+
|
|
647
271
|
|
|
648
|
-
class UnixHTTPConnection(
|
|
272
|
+
class UnixHTTPConnection(http.client.HTTPConnection):
|
|
649
273
|
'''Handles http requests to a unix socket file'''
|
|
650
274
|
|
|
651
275
|
def __init__(self, unix_socket):
|
|
@@ -661,15 +285,15 @@ class UnixHTTPConnection(httplib.HTTPConnection):
|
|
|
661
285
|
self.sock.settimeout(self.timeout)
|
|
662
286
|
|
|
663
287
|
def __call__(self, *args, **kwargs):
|
|
664
|
-
|
|
288
|
+
super().__init__(*args, **kwargs)
|
|
665
289
|
return self
|
|
666
290
|
|
|
667
291
|
|
|
668
|
-
class UnixHTTPHandler(
|
|
292
|
+
class UnixHTTPHandler(urllib.request.HTTPHandler):
|
|
669
293
|
'''Handler for Unix urls'''
|
|
670
294
|
|
|
671
295
|
def __init__(self, unix_socket, **kwargs):
|
|
672
|
-
|
|
296
|
+
super().__init__(**kwargs)
|
|
673
297
|
self._unix_socket = unix_socket
|
|
674
298
|
|
|
675
299
|
def http_open(self, req):
|
|
@@ -681,7 +305,7 @@ class ParseResultDottedDict(dict):
|
|
|
681
305
|
A dict that acts similarly to the ParseResult named tuple from urllib
|
|
682
306
|
'''
|
|
683
307
|
def __init__(self, *args, **kwargs):
|
|
684
|
-
super(
|
|
308
|
+
super().__init__(*args, **kwargs)
|
|
685
309
|
self.__dict__ = self
|
|
686
310
|
|
|
687
311
|
def as_list(self):
|
|
@@ -696,93 +320,25 @@ def generic_urlparse(parts):
|
|
|
696
320
|
Returns a dictionary of url parts as parsed by urlparse,
|
|
697
321
|
but accounts for the fact that older versions of that
|
|
698
322
|
library do not support named attributes (ie. .netloc)
|
|
699
|
-
'''
|
|
700
|
-
generic_parts = ParseResultDottedDict()
|
|
701
|
-
if hasattr(parts, 'netloc'):
|
|
702
|
-
# urlparse is newer, just read the fields straight
|
|
703
|
-
# from the parts object
|
|
704
|
-
generic_parts['scheme'] = parts.scheme
|
|
705
|
-
generic_parts['netloc'] = parts.netloc
|
|
706
|
-
generic_parts['path'] = parts.path
|
|
707
|
-
generic_parts['params'] = parts.params
|
|
708
|
-
generic_parts['query'] = parts.query
|
|
709
|
-
generic_parts['fragment'] = parts.fragment
|
|
710
|
-
generic_parts['username'] = parts.username
|
|
711
|
-
generic_parts['password'] = parts.password
|
|
712
|
-
hostname = parts.hostname
|
|
713
|
-
if hostname and hostname[0] == '[' and '[' in parts.netloc and ']' in parts.netloc:
|
|
714
|
-
# Py2.6 doesn't parse IPv6 addresses correctly
|
|
715
|
-
hostname = parts.netloc.split(']')[0][1:].lower()
|
|
716
|
-
generic_parts['hostname'] = hostname
|
|
717
|
-
|
|
718
|
-
try:
|
|
719
|
-
port = parts.port
|
|
720
|
-
except ValueError:
|
|
721
|
-
# Py2.6 doesn't parse IPv6 addresses correctly
|
|
722
|
-
netloc = parts.netloc.split('@')[-1].split(']')[-1]
|
|
723
|
-
if ':' in netloc:
|
|
724
|
-
port = netloc.split(':')[1]
|
|
725
|
-
if port:
|
|
726
|
-
port = int(port)
|
|
727
|
-
else:
|
|
728
|
-
port = None
|
|
729
|
-
generic_parts['port'] = port
|
|
730
|
-
else:
|
|
731
|
-
# we have to use indexes, and then parse out
|
|
732
|
-
# the other parts not supported by indexing
|
|
733
|
-
generic_parts['scheme'] = parts[0]
|
|
734
|
-
generic_parts['netloc'] = parts[1]
|
|
735
|
-
generic_parts['path'] = parts[2]
|
|
736
|
-
generic_parts['params'] = parts[3]
|
|
737
|
-
generic_parts['query'] = parts[4]
|
|
738
|
-
generic_parts['fragment'] = parts[5]
|
|
739
|
-
# get the username, password, etc.
|
|
740
|
-
try:
|
|
741
|
-
netloc_re = re.compile(r'^((?:\w)+(?::(?:\w)+)?@)?([A-Za-z0-9.-]+)(:\d+)?$')
|
|
742
|
-
match = netloc_re.match(parts[1])
|
|
743
|
-
auth = match.group(1)
|
|
744
|
-
hostname = match.group(2)
|
|
745
|
-
port = match.group(3)
|
|
746
|
-
if port:
|
|
747
|
-
# the capture group for the port will include the ':',
|
|
748
|
-
# so remove it and convert the port to an integer
|
|
749
|
-
port = int(port[1:])
|
|
750
|
-
if auth:
|
|
751
|
-
# the capture group above includes the @, so remove it
|
|
752
|
-
# and then split it up based on the first ':' found
|
|
753
|
-
auth = auth[:-1]
|
|
754
|
-
username, password = auth.split(':', 1)
|
|
755
|
-
else:
|
|
756
|
-
username = password = None
|
|
757
|
-
generic_parts['username'] = username
|
|
758
|
-
generic_parts['password'] = password
|
|
759
|
-
generic_parts['hostname'] = hostname
|
|
760
|
-
generic_parts['port'] = port
|
|
761
|
-
except Exception:
|
|
762
|
-
generic_parts['username'] = None
|
|
763
|
-
generic_parts['password'] = None
|
|
764
|
-
generic_parts['hostname'] = parts[1]
|
|
765
|
-
generic_parts['port'] = None
|
|
766
|
-
return generic_parts
|
|
767
323
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
324
|
+
This method isn't of much use any longer, but is kept
|
|
325
|
+
in a minimal state for backwards compat.
|
|
326
|
+
'''
|
|
327
|
+
result = ParseResultDottedDict(parts._asdict())
|
|
328
|
+
result.update({
|
|
329
|
+
'username': parts.username,
|
|
330
|
+
'password': parts.password,
|
|
331
|
+
'hostname': parts.hostname,
|
|
332
|
+
'port': parts.port,
|
|
333
|
+
})
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def extract_pem_certs(data):
|
|
338
|
+
for match in PEM_CERT_RE.finditer(data):
|
|
771
339
|
yield match.group(0)
|
|
772
340
|
|
|
773
341
|
|
|
774
|
-
def _py2_get_param(headers, param, header='content-type'):
|
|
775
|
-
m = httplib.HTTPMessage(io.StringIO())
|
|
776
|
-
cd = headers.getheader(header) or ''
|
|
777
|
-
try:
|
|
778
|
-
m.plisttext = cd[cd.index(';'):]
|
|
779
|
-
m.parseplist()
|
|
780
|
-
except ValueError:
|
|
781
|
-
return None
|
|
782
|
-
|
|
783
|
-
return m.getparam(param)
|
|
784
|
-
|
|
785
|
-
|
|
786
342
|
def get_response_filename(response):
|
|
787
343
|
url = response.geturl()
|
|
788
344
|
path = urlparse(url)[2]
|
|
@@ -790,22 +346,12 @@ def get_response_filename(response):
|
|
|
790
346
|
if filename:
|
|
791
347
|
filename = unquote(filename)
|
|
792
348
|
|
|
793
|
-
|
|
794
|
-
get_param = functools.partial(_py2_get_param, response.headers)
|
|
795
|
-
else:
|
|
796
|
-
get_param = response.headers.get_param
|
|
797
|
-
|
|
798
|
-
return get_param('filename', header='content-disposition') or filename
|
|
349
|
+
return response.headers.get_param('filename', header='content-disposition') or filename
|
|
799
350
|
|
|
800
351
|
|
|
801
352
|
def parse_content_type(response):
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
get_param = response.headers.getparam
|
|
805
|
-
else:
|
|
806
|
-
get_type = response.headers.get_content_type
|
|
807
|
-
get_param = response.headers.get_param
|
|
808
|
-
|
|
353
|
+
get_type = response.headers.get_content_type
|
|
354
|
+
get_param = response.headers.get_param
|
|
809
355
|
content_type = (get_type() or 'application/octet-stream').split(',')[0]
|
|
810
356
|
main_type, sub_type = content_type.split('/')
|
|
811
357
|
charset = (get_param('charset') or 'utf-8').split(',')[0]
|
|
@@ -822,17 +368,8 @@ class GzipDecodedReader(GzipFile):
|
|
|
822
368
|
if not HAS_GZIP:
|
|
823
369
|
raise MissingModuleError(self.missing_gzip_error(), import_traceback=GZIP_IMP_ERR)
|
|
824
370
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
else:
|
|
828
|
-
# Py2 ``HTTPResponse``/``addinfourl`` doesn't support all of the file object
|
|
829
|
-
# functionality GzipFile requires
|
|
830
|
-
self._io = io.BytesIO()
|
|
831
|
-
for block in iter(functools.partial(fp.read, 65536), b''):
|
|
832
|
-
self._io.write(block)
|
|
833
|
-
self._io.seek(0)
|
|
834
|
-
fp.close()
|
|
835
|
-
gzip.GzipFile.__init__(self, mode='rb', fileobj=self._io) # pylint: disable=non-parent-init-called
|
|
371
|
+
self._io = fp
|
|
372
|
+
super().__init__(mode='rb', fileobj=self._io)
|
|
836
373
|
|
|
837
374
|
def close(self):
|
|
838
375
|
try:
|
|
@@ -849,432 +386,206 @@ class GzipDecodedReader(GzipFile):
|
|
|
849
386
|
)
|
|
850
387
|
|
|
851
388
|
|
|
852
|
-
class
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
def __init__(self, url, method, data=None, headers=None, origin_req_host=None, unverifiable=True):
|
|
859
|
-
if headers is None:
|
|
860
|
-
headers = {}
|
|
861
|
-
self._method = method.upper()
|
|
862
|
-
urllib_request.Request.__init__(self, url, data, headers, origin_req_host, unverifiable)
|
|
863
|
-
|
|
864
|
-
def get_method(self):
|
|
865
|
-
if self._method:
|
|
866
|
-
return self._method
|
|
867
|
-
else:
|
|
868
|
-
return urllib_request.Request.get_method(self)
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
def RedirectHandlerFactory(follow_redirects=None, validate_certs=True, ca_path=None, ciphers=None):
|
|
872
|
-
"""This is a class factory that closes over the value of
|
|
873
|
-
``follow_redirects`` so that the RedirectHandler class has access to
|
|
874
|
-
that value without having to use globals, and potentially cause problems
|
|
875
|
-
where ``open_url`` or ``fetch_url`` are used multiple times in a module.
|
|
389
|
+
class HTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
390
|
+
"""This is an implementation of a RedirectHandler to match the
|
|
391
|
+
functionality provided by httplib2. It will utilize the value of
|
|
392
|
+
``follow_redirects`` to determine how redirects should be handled in
|
|
393
|
+
urllib.
|
|
876
394
|
"""
|
|
877
395
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
functionality provided by httplib2. It will utilize the value of
|
|
881
|
-
``follow_redirects`` that is passed into ``RedirectHandlerFactory``
|
|
882
|
-
to determine how redirects should be handled in urllib2.
|
|
883
|
-
"""
|
|
884
|
-
|
|
885
|
-
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
886
|
-
if not any((HAS_SSLCONTEXT, HAS_URLLIB3_PYOPENSSLCONTEXT)):
|
|
887
|
-
handler = maybe_add_ssl_handler(newurl, validate_certs, ca_path=ca_path, ciphers=ciphers)
|
|
888
|
-
if handler:
|
|
889
|
-
urllib_request._opener.add_handler(handler)
|
|
890
|
-
|
|
891
|
-
# Preserve urllib2 compatibility
|
|
892
|
-
if follow_redirects == 'urllib2':
|
|
893
|
-
return urllib_request.HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, headers, newurl)
|
|
894
|
-
|
|
895
|
-
# Handle disabled redirects
|
|
896
|
-
elif follow_redirects in ['no', 'none', False]:
|
|
897
|
-
raise urllib_error.HTTPError(newurl, code, msg, headers, fp)
|
|
898
|
-
|
|
899
|
-
method = req.get_method()
|
|
900
|
-
|
|
901
|
-
# Handle non-redirect HTTP status or invalid follow_redirects
|
|
902
|
-
if follow_redirects in ['all', 'yes', True]:
|
|
903
|
-
if code < 300 or code >= 400:
|
|
904
|
-
raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
905
|
-
elif follow_redirects == 'safe':
|
|
906
|
-
if code < 300 or code >= 400 or method not in ('GET', 'HEAD'):
|
|
907
|
-
raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
908
|
-
else:
|
|
909
|
-
raise urllib_error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
396
|
+
def __init__(self, follow_redirects=None):
|
|
397
|
+
self.follow_redirects = follow_redirects
|
|
910
398
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
origin_req_host = req.get_origin_req_host()
|
|
915
|
-
except AttributeError:
|
|
916
|
-
# Python 3.4+
|
|
917
|
-
data = req.data
|
|
918
|
-
origin_req_host = req.origin_req_host
|
|
399
|
+
def __call__(self, *args, **kwargs):
|
|
400
|
+
super().__init__(*args, **kwargs)
|
|
401
|
+
return self
|
|
919
402
|
|
|
920
|
-
|
|
921
|
-
|
|
403
|
+
try:
|
|
404
|
+
urllib.request.HTTPRedirectHandler.http_error_308 # type: ignore[attr-defined]
|
|
405
|
+
except AttributeError:
|
|
406
|
+
# deprecated: description='urllib http 308 support' python_version='3.11'
|
|
407
|
+
http_error_308 = urllib.request.HTTPRedirectHandler.http_error_302
|
|
922
408
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
# Preserve payload and headers
|
|
926
|
-
req_headers = req.headers
|
|
927
|
-
else:
|
|
928
|
-
# Do not preserve payload and filter headers
|
|
929
|
-
data = None
|
|
930
|
-
req_headers = dict((k, v) for k, v in req.headers.items()
|
|
931
|
-
if k.lower() not in ("content-length", "content-type", "transfer-encoding"))
|
|
932
|
-
|
|
933
|
-
# http://tools.ietf.org/html/rfc7231#section-6.4.4
|
|
934
|
-
if code == 303 and method != 'HEAD':
|
|
935
|
-
method = 'GET'
|
|
936
|
-
|
|
937
|
-
# Do what the browsers do, despite standards...
|
|
938
|
-
# First, turn 302s into GETs.
|
|
939
|
-
if code == 302 and method != 'HEAD':
|
|
940
|
-
method = 'GET'
|
|
941
|
-
|
|
942
|
-
# Second, if a POST is responded to with a 301, turn it into a GET.
|
|
943
|
-
if code == 301 and method == 'POST':
|
|
944
|
-
method = 'GET'
|
|
945
|
-
|
|
946
|
-
return RequestWithMethod(newurl,
|
|
947
|
-
method=method,
|
|
948
|
-
headers=req_headers,
|
|
949
|
-
data=data,
|
|
950
|
-
origin_req_host=origin_req_host,
|
|
951
|
-
unverifiable=True,
|
|
952
|
-
)
|
|
953
|
-
|
|
954
|
-
return RedirectHandler
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
def build_ssl_validation_error(hostname, port, paths, exc=None):
|
|
958
|
-
'''Inteligently build out the SSLValidationError based on what support
|
|
959
|
-
you have installed
|
|
960
|
-
'''
|
|
409
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
410
|
+
follow_redirects = self.follow_redirects
|
|
961
411
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
' certificate installed.')
|
|
966
|
-
]
|
|
967
|
-
if not HAS_SSLCONTEXT:
|
|
968
|
-
msg.append('If the website serving the url uses SNI you need'
|
|
969
|
-
' python >= 2.7.9 on your managed machine')
|
|
970
|
-
msg.append(' (the python executable used (%s) is version: %s)' %
|
|
971
|
-
(sys.executable, ''.join(sys.version.splitlines())))
|
|
972
|
-
if not HAS_URLLIB3_PYOPENSSLCONTEXT and not HAS_URLLIB3_SSL_WRAP_SOCKET:
|
|
973
|
-
msg.append('or you can install the `urllib3`, `pyOpenSSL`,'
|
|
974
|
-
' `ndg-httpsclient`, and `pyasn1` python modules')
|
|
412
|
+
# Preserve urllib2 compatibility
|
|
413
|
+
if follow_redirects in ('urllib2', 'urllib'):
|
|
414
|
+
return urllib.request.HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, headers, newurl)
|
|
975
415
|
|
|
976
|
-
|
|
416
|
+
# Handle disabled redirects
|
|
417
|
+
elif follow_redirects in ('no', 'none', False):
|
|
418
|
+
raise urllib.error.HTTPError(newurl, code, msg, headers, fp)
|
|
977
419
|
|
|
978
|
-
|
|
979
|
-
' not need to confirm the servers identity but this is'
|
|
980
|
-
' unsafe and not recommended.'
|
|
981
|
-
' Paths checked for this platform: %s.')
|
|
420
|
+
method = req.get_method()
|
|
982
421
|
|
|
983
|
-
|
|
984
|
-
|
|
422
|
+
# Handle non-redirect HTTP status or invalid follow_redirects
|
|
423
|
+
if follow_redirects in ('all', 'yes', True):
|
|
424
|
+
if code < 300 or code >= 400:
|
|
425
|
+
raise urllib.error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
426
|
+
elif follow_redirects == 'safe':
|
|
427
|
+
if code < 300 or code >= 400 or method not in ('GET', 'HEAD'):
|
|
428
|
+
raise urllib.error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
429
|
+
else:
|
|
430
|
+
raise urllib.error.HTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
985
431
|
|
|
986
|
-
|
|
432
|
+
data = req.data
|
|
433
|
+
origin_req_host = req.origin_req_host
|
|
987
434
|
|
|
435
|
+
# Be conciliant with URIs containing a space
|
|
436
|
+
newurl = newurl.replace(' ', '%20')
|
|
988
437
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
#
|
|
995
|
-
|
|
438
|
+
# Support redirect with payload and original headers
|
|
439
|
+
if code in (307, 308):
|
|
440
|
+
# Preserve payload and headers
|
|
441
|
+
req_headers = req.headers
|
|
442
|
+
else:
|
|
443
|
+
# Do not preserve payload and filter headers
|
|
444
|
+
data = None
|
|
445
|
+
req_headers = {k: v for k, v in req.headers.items()
|
|
446
|
+
if k.lower() not in ("content-length", "content-type", "transfer-encoding")}
|
|
447
|
+
|
|
448
|
+
# http://tools.ietf.org/html/rfc7231#section-6.4.4
|
|
449
|
+
if code == 303 and method != 'HEAD':
|
|
450
|
+
method = 'GET'
|
|
451
|
+
|
|
452
|
+
# Do what the browsers do, despite standards...
|
|
453
|
+
# First, turn 302s into GETs.
|
|
454
|
+
if code == 302 and method != 'HEAD':
|
|
455
|
+
method = 'GET'
|
|
456
|
+
|
|
457
|
+
# Second, if a POST is responded to with a 301, turn it into a GET.
|
|
458
|
+
if code == 301 and method == 'POST':
|
|
459
|
+
method = 'GET'
|
|
460
|
+
|
|
461
|
+
return urllib.request.Request(
|
|
462
|
+
newurl,
|
|
463
|
+
data=data,
|
|
464
|
+
headers=req_headers,
|
|
465
|
+
origin_req_host=origin_req_host,
|
|
466
|
+
unverifiable=True,
|
|
467
|
+
method=method.upper(),
|
|
468
|
+
)
|
|
996
469
|
|
|
997
470
|
|
|
998
|
-
def make_context(cafile=None, cadata=None, ciphers=None, validate_certs=True, client_cert=None,
|
|
471
|
+
def make_context(cafile=None, cadata=None, capath=None, ciphers=None, validate_certs=True, client_cert=None,
|
|
472
|
+
client_key=None):
|
|
999
473
|
if ciphers is None:
|
|
1000
474
|
ciphers = []
|
|
1001
475
|
|
|
1002
476
|
if not is_sequence(ciphers):
|
|
1003
477
|
raise TypeError('Ciphers must be a list. Got %s.' % ciphers.__class__.__name__)
|
|
1004
478
|
|
|
1005
|
-
|
|
1006
|
-
context = create_default_context(cafile=cafile)
|
|
1007
|
-
elif HAS_URLLIB3_PYOPENSSLCONTEXT:
|
|
1008
|
-
context = PyOpenSSLContext(PROTOCOL)
|
|
1009
|
-
else:
|
|
1010
|
-
raise NotImplementedError('Host libraries are too old to support creating an sslcontext')
|
|
479
|
+
context = ssl.create_default_context(cafile=cafile)
|
|
1011
480
|
|
|
1012
481
|
if not validate_certs:
|
|
1013
|
-
if ssl.OP_NO_SSLv2:
|
|
1014
|
-
context.options |= ssl.OP_NO_SSLv2
|
|
1015
482
|
context.options |= ssl.OP_NO_SSLv3
|
|
1016
483
|
context.check_hostname = False
|
|
1017
484
|
context.verify_mode = ssl.CERT_NONE
|
|
1018
485
|
|
|
1019
|
-
|
|
1020
|
-
|
|
486
|
+
# If cafile is passed, we are only using that for verification,
|
|
487
|
+
# don't add additional ca certs
|
|
488
|
+
if validate_certs and not cafile:
|
|
489
|
+
if not cadata:
|
|
490
|
+
cadata = bytearray()
|
|
491
|
+
cadata.extend(get_ca_certs(capath=capath)[0])
|
|
492
|
+
if cadata:
|
|
493
|
+
context.load_verify_locations(cadata=cadata)
|
|
1021
494
|
|
|
1022
495
|
if ciphers:
|
|
1023
496
|
context.set_ciphers(':'.join(map(to_native, ciphers)))
|
|
1024
497
|
|
|
1025
498
|
if client_cert:
|
|
499
|
+
# TLS 1.3 needs this to be set to True to allow post handshake cert
|
|
500
|
+
# authentication. This functionality was added in Python 3.8 and was
|
|
501
|
+
# backported to 3.6.7, and 3.7.1 so needs a check for now.
|
|
502
|
+
if hasattr(context, "post_handshake_auth"):
|
|
503
|
+
context.post_handshake_auth = True
|
|
504
|
+
|
|
1026
505
|
context.load_cert_chain(client_cert, keyfile=client_key)
|
|
1027
506
|
|
|
1028
507
|
return context
|
|
1029
508
|
|
|
1030
509
|
|
|
1031
|
-
def get_ca_certs(cafile=None):
|
|
510
|
+
def get_ca_certs(cafile=None, capath=None):
|
|
1032
511
|
# tries to find a valid CA cert in one of the
|
|
1033
512
|
# standard locations for the current distribution
|
|
1034
513
|
|
|
1035
|
-
|
|
1036
|
-
|
|
514
|
+
# Using a dict, instead of a set for order, the value is meaningless and will be None
|
|
515
|
+
# Not directly using a bytearray to avoid duplicates with fast lookup
|
|
516
|
+
cadata = {}
|
|
1037
517
|
|
|
518
|
+
# If cafile is passed, we are only using that for verification,
|
|
519
|
+
# don't add additional ca certs
|
|
1038
520
|
if cafile:
|
|
1039
521
|
paths_checked = [cafile]
|
|
1040
|
-
with open(to_bytes(cafile, errors='surrogate_or_strict'), '
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
if
|
|
1051
|
-
paths_checked.
|
|
522
|
+
with open(to_bytes(cafile, errors='surrogate_or_strict'), 'r', errors='surrogateescape') as f:
|
|
523
|
+
for pem in extract_pem_certs(f.read()):
|
|
524
|
+
b_der = ssl.PEM_cert_to_DER_cert(pem)
|
|
525
|
+
cadata[b_der] = None
|
|
526
|
+
return bytearray().join(cadata), paths_checked
|
|
527
|
+
|
|
528
|
+
default_verify_paths = ssl.get_default_verify_paths()
|
|
529
|
+
default_capath = default_verify_paths.capath
|
|
530
|
+
paths_checked = {default_capath or default_verify_paths.cafile}
|
|
531
|
+
|
|
532
|
+
if capath:
|
|
533
|
+
paths_checked.add(capath)
|
|
1052
534
|
|
|
1053
535
|
system = to_text(platform.system(), errors='surrogate_or_strict')
|
|
1054
536
|
# build a list of paths to check for .crt/.pem files
|
|
1055
537
|
# based on the platform type
|
|
1056
538
|
if system == u'Linux':
|
|
1057
|
-
paths_checked.
|
|
1058
|
-
paths_checked.
|
|
1059
|
-
paths_checked.
|
|
539
|
+
paths_checked.add('/etc/pki/ca-trust/extracted/pem')
|
|
540
|
+
paths_checked.add('/etc/pki/tls/certs')
|
|
541
|
+
paths_checked.add('/usr/share/ca-certificates/cacert.org')
|
|
1060
542
|
elif system == u'FreeBSD':
|
|
1061
|
-
paths_checked.
|
|
543
|
+
paths_checked.add('/usr/local/share/certs')
|
|
1062
544
|
elif system == u'OpenBSD':
|
|
1063
|
-
paths_checked.
|
|
545
|
+
paths_checked.add('/etc/ssl')
|
|
1064
546
|
elif system == u'NetBSD':
|
|
1065
|
-
paths_checked.
|
|
547
|
+
paths_checked.add('/etc/openssl/certs')
|
|
1066
548
|
elif system == u'SunOS':
|
|
1067
|
-
paths_checked.
|
|
549
|
+
paths_checked.add('/opt/local/etc/openssl/certs')
|
|
1068
550
|
elif system == u'AIX':
|
|
1069
|
-
paths_checked.
|
|
1070
|
-
paths_checked.
|
|
551
|
+
paths_checked.add('/var/ssl/certs')
|
|
552
|
+
paths_checked.add('/opt/freeware/etc/ssl/certs')
|
|
553
|
+
elif system == u'Darwin':
|
|
554
|
+
paths_checked.add('/usr/local/etc/openssl')
|
|
1071
555
|
|
|
1072
556
|
# fall back to a user-deployed cert in a standard
|
|
1073
557
|
# location if the OS platform one is not available
|
|
1074
|
-
paths_checked.
|
|
1075
|
-
|
|
1076
|
-
tmp_path = None
|
|
1077
|
-
if not HAS_SSLCONTEXT:
|
|
1078
|
-
tmp_fd, tmp_path = tempfile.mkstemp()
|
|
1079
|
-
atexit.register(atexit_remove_file, tmp_path)
|
|
1080
|
-
|
|
1081
|
-
# Write the dummy ca cert if we are running on macOS
|
|
1082
|
-
if system == u'Darwin':
|
|
1083
|
-
if HAS_SSLCONTEXT:
|
|
1084
|
-
cadata.extend(
|
|
1085
|
-
ssl.PEM_cert_to_DER_cert(
|
|
1086
|
-
to_native(b_DUMMY_CA_CERT, errors='surrogate_or_strict')
|
|
1087
|
-
)
|
|
1088
|
-
)
|
|
1089
|
-
else:
|
|
1090
|
-
os.write(tmp_fd, b_DUMMY_CA_CERT)
|
|
1091
|
-
# Default Homebrew path for OpenSSL certs
|
|
1092
|
-
paths_checked.append('/usr/local/etc/openssl')
|
|
558
|
+
paths_checked.add('/etc/ansible')
|
|
1093
559
|
|
|
1094
560
|
# for all of the paths, find any .crt or .pem files
|
|
1095
561
|
# and compile them into single temp file for use
|
|
1096
562
|
# in the ssl check to speed up the test
|
|
1097
563
|
for path in paths_checked:
|
|
1098
|
-
if not os.path.isdir(path):
|
|
564
|
+
if not path or path == default_capath or not os.path.isdir(path):
|
|
1099
565
|
continue
|
|
1100
566
|
|
|
1101
|
-
|
|
1102
|
-
for f in dir_contents:
|
|
567
|
+
for f in os.listdir(path):
|
|
1103
568
|
full_path = os.path.join(path, f)
|
|
1104
|
-
if os.path.isfile(full_path) and os.path.splitext(f)[1] in
|
|
569
|
+
if os.path.isfile(full_path) and os.path.splitext(f)[1] in {'.pem', '.cer', '.crt'}:
|
|
1105
570
|
try:
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
to_native(b_pem, errors='surrogate_or_strict')
|
|
1115
|
-
)
|
|
1116
|
-
)
|
|
1117
|
-
except Exception:
|
|
1118
|
-
continue
|
|
1119
|
-
else:
|
|
1120
|
-
os.write(tmp_fd, b_cert)
|
|
1121
|
-
os.write(tmp_fd, b'\n')
|
|
571
|
+
with open(full_path, 'r', errors='surrogateescape') as cert_file:
|
|
572
|
+
cert = cert_file.read()
|
|
573
|
+
try:
|
|
574
|
+
for pem in extract_pem_certs(cert):
|
|
575
|
+
b_der = ssl.PEM_cert_to_DER_cert(pem)
|
|
576
|
+
cadata[b_der] = None
|
|
577
|
+
except Exception:
|
|
578
|
+
continue
|
|
1122
579
|
except (OSError, IOError):
|
|
1123
580
|
pass
|
|
1124
581
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
paths_checked[:0] = [default_verify_paths.capath]
|
|
1128
|
-
else:
|
|
1129
|
-
os.close(tmp_fd)
|
|
1130
|
-
|
|
1131
|
-
return (tmp_path, cadata, paths_checked)
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
class SSLValidationHandler(urllib_request.BaseHandler):
|
|
1135
|
-
'''
|
|
1136
|
-
A custom handler class for SSL validation.
|
|
1137
|
-
|
|
1138
|
-
Based on:
|
|
1139
|
-
http://stackoverflow.com/questions/1087227/validate-ssl-certificates-with-python
|
|
1140
|
-
http://techknack.net/python-urllib2-handlers/
|
|
1141
|
-
'''
|
|
1142
|
-
CONNECT_COMMAND = "CONNECT %s:%s HTTP/1.0\r\n"
|
|
1143
|
-
|
|
1144
|
-
def __init__(self, hostname, port, ca_path=None, ciphers=None, validate_certs=True):
|
|
1145
|
-
self.hostname = hostname
|
|
1146
|
-
self.port = port
|
|
1147
|
-
self.ca_path = ca_path
|
|
1148
|
-
self.ciphers = ciphers
|
|
1149
|
-
self.validate_certs = validate_certs
|
|
1150
|
-
|
|
1151
|
-
def get_ca_certs(self):
|
|
1152
|
-
return get_ca_certs(self.ca_path)
|
|
1153
|
-
|
|
1154
|
-
def validate_proxy_response(self, response, valid_codes=None):
|
|
1155
|
-
'''
|
|
1156
|
-
make sure we get back a valid code from the proxy
|
|
1157
|
-
'''
|
|
1158
|
-
valid_codes = [200] if valid_codes is None else valid_codes
|
|
1159
|
-
|
|
1160
|
-
try:
|
|
1161
|
-
(http_version, resp_code, msg) = re.match(br'(HTTP/\d\.\d) (\d\d\d) (.*)', response).groups()
|
|
1162
|
-
if int(resp_code) not in valid_codes:
|
|
1163
|
-
raise Exception
|
|
1164
|
-
except Exception:
|
|
1165
|
-
raise ProxyError('Connection to proxy failed')
|
|
1166
|
-
|
|
1167
|
-
def detect_no_proxy(self, url):
|
|
1168
|
-
'''
|
|
1169
|
-
Detect if the 'no_proxy' environment variable is set and honor those locations.
|
|
1170
|
-
'''
|
|
1171
|
-
env_no_proxy = os.environ.get('no_proxy')
|
|
1172
|
-
if env_no_proxy:
|
|
1173
|
-
env_no_proxy = env_no_proxy.split(',')
|
|
1174
|
-
netloc = urlparse(url).netloc
|
|
1175
|
-
|
|
1176
|
-
for host in env_no_proxy:
|
|
1177
|
-
if netloc.endswith(host) or netloc.split(':')[0].endswith(host):
|
|
1178
|
-
# Our requested URL matches something in no_proxy, so don't
|
|
1179
|
-
# use the proxy for this
|
|
1180
|
-
return False
|
|
1181
|
-
return True
|
|
1182
|
-
|
|
1183
|
-
def make_context(self, cafile, cadata, ciphers=None, validate_certs=True):
|
|
1184
|
-
cafile = self.ca_path or cafile
|
|
1185
|
-
if self.ca_path:
|
|
1186
|
-
cadata = None
|
|
1187
|
-
else:
|
|
1188
|
-
cadata = cadata or None
|
|
1189
|
-
|
|
1190
|
-
return make_context(cafile=cafile, cadata=cadata, ciphers=ciphers, validate_certs=validate_certs)
|
|
1191
|
-
|
|
1192
|
-
def http_request(self, req):
|
|
1193
|
-
tmp_ca_cert_path, cadata, paths_checked = self.get_ca_certs()
|
|
1194
|
-
|
|
1195
|
-
# Detect if 'no_proxy' environment variable is set and if our URL is included
|
|
1196
|
-
use_proxy = self.detect_no_proxy(req.get_full_url())
|
|
1197
|
-
https_proxy = os.environ.get('https_proxy')
|
|
1198
|
-
|
|
1199
|
-
context = None
|
|
1200
|
-
try:
|
|
1201
|
-
context = self.make_context(tmp_ca_cert_path, cadata, ciphers=self.ciphers, validate_certs=self.validate_certs)
|
|
1202
|
-
except NotImplementedError:
|
|
1203
|
-
# We'll make do with no context below
|
|
1204
|
-
pass
|
|
1205
|
-
|
|
1206
|
-
try:
|
|
1207
|
-
if use_proxy and https_proxy:
|
|
1208
|
-
proxy_parts = generic_urlparse(urlparse(https_proxy))
|
|
1209
|
-
port = proxy_parts.get('port') or 443
|
|
1210
|
-
proxy_hostname = proxy_parts.get('hostname', None)
|
|
1211
|
-
if proxy_hostname is None or proxy_parts.get('scheme') == '':
|
|
1212
|
-
raise ProxyError("Failed to parse https_proxy environment variable."
|
|
1213
|
-
" Please make sure you export https proxy as 'https_proxy=<SCHEME>://<IP_ADDRESS>:<PORT>'")
|
|
1214
|
-
|
|
1215
|
-
s = socket.create_connection((proxy_hostname, port))
|
|
1216
|
-
if proxy_parts.get('scheme') == 'http':
|
|
1217
|
-
s.sendall(to_bytes(self.CONNECT_COMMAND % (self.hostname, self.port), errors='surrogate_or_strict'))
|
|
1218
|
-
if proxy_parts.get('username'):
|
|
1219
|
-
credentials = "%s:%s" % (proxy_parts.get('username', ''), proxy_parts.get('password', ''))
|
|
1220
|
-
s.sendall(b'Proxy-Authorization: Basic %s\r\n' % base64.b64encode(to_bytes(credentials, errors='surrogate_or_strict')).strip())
|
|
1221
|
-
s.sendall(b'\r\n')
|
|
1222
|
-
connect_result = b""
|
|
1223
|
-
while connect_result.find(b"\r\n\r\n") <= 0:
|
|
1224
|
-
connect_result += s.recv(4096)
|
|
1225
|
-
# 128 kilobytes of headers should be enough for everyone.
|
|
1226
|
-
if len(connect_result) > 131072:
|
|
1227
|
-
raise ProxyError('Proxy sent too verbose headers. Only 128KiB allowed.')
|
|
1228
|
-
self.validate_proxy_response(connect_result)
|
|
1229
|
-
if context:
|
|
1230
|
-
ssl_s = context.wrap_socket(s, server_hostname=self.hostname)
|
|
1231
|
-
elif HAS_URLLIB3_SSL_WRAP_SOCKET:
|
|
1232
|
-
ssl_s = ssl_wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED, ssl_version=PROTOCOL, server_hostname=self.hostname)
|
|
1233
|
-
else:
|
|
1234
|
-
ssl_s = ssl.wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED, ssl_version=PROTOCOL)
|
|
1235
|
-
match_hostname(ssl_s.getpeercert(), self.hostname)
|
|
1236
|
-
else:
|
|
1237
|
-
raise ProxyError('Unsupported proxy scheme: %s. Currently ansible only supports HTTP proxies.' % proxy_parts.get('scheme'))
|
|
1238
|
-
else:
|
|
1239
|
-
s = socket.create_connection((self.hostname, self.port))
|
|
1240
|
-
if context:
|
|
1241
|
-
ssl_s = context.wrap_socket(s, server_hostname=self.hostname)
|
|
1242
|
-
elif HAS_URLLIB3_SSL_WRAP_SOCKET:
|
|
1243
|
-
ssl_s = ssl_wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED, ssl_version=PROTOCOL, server_hostname=self.hostname)
|
|
1244
|
-
else:
|
|
1245
|
-
ssl_s = ssl.wrap_socket(s, ca_certs=tmp_ca_cert_path, cert_reqs=ssl.CERT_REQUIRED, ssl_version=PROTOCOL)
|
|
1246
|
-
match_hostname(ssl_s.getpeercert(), self.hostname)
|
|
1247
|
-
# close the ssl connection
|
|
1248
|
-
# ssl_s.unwrap()
|
|
1249
|
-
s.close()
|
|
1250
|
-
except (ssl.SSLError, CertificateError) as e:
|
|
1251
|
-
build_ssl_validation_error(self.hostname, self.port, paths_checked, e)
|
|
1252
|
-
except socket.error as e:
|
|
1253
|
-
raise ConnectionError('Failed to connect to %s at port %s: %s' % (self.hostname, self.port, to_native(e)))
|
|
1254
|
-
|
|
1255
|
-
return req
|
|
1256
|
-
|
|
1257
|
-
https_request = http_request
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
def maybe_add_ssl_handler(url, validate_certs, ca_path=None, ciphers=None):
|
|
1261
|
-
parsed = generic_urlparse(urlparse(url))
|
|
1262
|
-
if parsed.scheme == 'https' and validate_certs:
|
|
1263
|
-
if not HAS_SSL:
|
|
1264
|
-
raise NoSSLError('SSL validation is not available in your version of python. You can use validate_certs=False,'
|
|
1265
|
-
' however this is unsafe and not recommended')
|
|
1266
|
-
|
|
1267
|
-
# create the SSL validation handler
|
|
1268
|
-
return SSLValidationHandler(parsed.hostname, parsed.port or 443, ca_path=ca_path, ciphers=ciphers, validate_certs=validate_certs)
|
|
582
|
+
# paths_checked isn't used any more, but is kept just for ease of debugging
|
|
583
|
+
return bytearray().join(cadata), list(paths_checked)
|
|
1269
584
|
|
|
1270
585
|
|
|
1271
586
|
def getpeercert(response, binary_form=False):
|
|
1272
587
|
""" Attempt to get the peer certificate of the response from urlopen. """
|
|
1273
|
-
|
|
1274
|
-
if PY3:
|
|
1275
|
-
socket = response.fp.raw._sock
|
|
1276
|
-
else:
|
|
1277
|
-
socket = response.fp._sock.fp._sock
|
|
588
|
+
socket = response.fp.raw._sock
|
|
1278
589
|
|
|
1279
590
|
try:
|
|
1280
591
|
return socket.getpeercert(binary_form)
|
|
@@ -1297,7 +608,7 @@ def get_channel_binding_cert_hash(certificate_der):
|
|
|
1297
608
|
pass
|
|
1298
609
|
|
|
1299
610
|
# If the signature hash algorithm is unknown/unsupported or md5/sha1 we must use SHA256.
|
|
1300
|
-
if not hash_algorithm or hash_algorithm.name in
|
|
611
|
+
if not hash_algorithm or hash_algorithm.name in ('md5', 'sha1'):
|
|
1301
612
|
hash_algorithm = hashes.SHA256()
|
|
1302
613
|
|
|
1303
614
|
digest = hashes.Hash(hash_algorithm, default_backend())
|
|
@@ -1322,11 +633,80 @@ def rfc2822_date_string(timetuple, zone='-0000'):
|
|
|
1322
633
|
zone)
|
|
1323
634
|
|
|
1324
635
|
|
|
636
|
+
def _configure_auth(url, url_username, url_password, use_gssapi, force_basic_auth, use_netrc):
|
|
637
|
+
headers = {}
|
|
638
|
+
handlers = []
|
|
639
|
+
|
|
640
|
+
parsed = urlparse(url)
|
|
641
|
+
if parsed.scheme == 'ftp':
|
|
642
|
+
return url, headers, handlers
|
|
643
|
+
|
|
644
|
+
username = url_username
|
|
645
|
+
password = url_password
|
|
646
|
+
|
|
647
|
+
if username:
|
|
648
|
+
netloc = parsed.netloc
|
|
649
|
+
elif '@' in parsed.netloc:
|
|
650
|
+
credentials, netloc = parsed.netloc.split('@', 1)
|
|
651
|
+
if ':' in credentials:
|
|
652
|
+
username, password = credentials.split(':', 1)
|
|
653
|
+
else:
|
|
654
|
+
username = credentials
|
|
655
|
+
password = ''
|
|
656
|
+
username = unquote(username)
|
|
657
|
+
password = unquote(password)
|
|
658
|
+
|
|
659
|
+
# reconstruct url without credentials
|
|
660
|
+
url = urlunparse(parsed._replace(netloc=netloc))
|
|
661
|
+
|
|
662
|
+
if use_gssapi:
|
|
663
|
+
if HTTPGSSAPIAuthHandler: # type: ignore[truthy-function]
|
|
664
|
+
handlers.append(HTTPGSSAPIAuthHandler(username, password))
|
|
665
|
+
else:
|
|
666
|
+
imp_err_msg = missing_required_lib('gssapi', reason='for use_gssapi=True',
|
|
667
|
+
url='https://pypi.org/project/gssapi/')
|
|
668
|
+
raise MissingModuleError(imp_err_msg, import_traceback=GSSAPI_IMP_ERR)
|
|
669
|
+
|
|
670
|
+
elif username and not force_basic_auth:
|
|
671
|
+
passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
|
672
|
+
|
|
673
|
+
# this creates a password manager
|
|
674
|
+
passman.add_password(None, netloc, username, password)
|
|
675
|
+
|
|
676
|
+
# because we have put None at the start it will always
|
|
677
|
+
# use this username/password combination for urls
|
|
678
|
+
# for which `theurl` is a super-url
|
|
679
|
+
authhandler = urllib.request.HTTPBasicAuthHandler(passman)
|
|
680
|
+
digest_authhandler = urllib.request.HTTPDigestAuthHandler(passman)
|
|
681
|
+
|
|
682
|
+
# create the AuthHandler
|
|
683
|
+
handlers.append(authhandler)
|
|
684
|
+
handlers.append(digest_authhandler)
|
|
685
|
+
|
|
686
|
+
elif username and force_basic_auth:
|
|
687
|
+
headers["Authorization"] = basic_auth_header(username, password)
|
|
688
|
+
|
|
689
|
+
elif use_netrc:
|
|
690
|
+
try:
|
|
691
|
+
rc = netrc.netrc(os.environ.get('NETRC'))
|
|
692
|
+
login = rc.authenticators(parsed.hostname)
|
|
693
|
+
except IOError:
|
|
694
|
+
login = None
|
|
695
|
+
|
|
696
|
+
if login:
|
|
697
|
+
username, dummy, password = login
|
|
698
|
+
if username and password:
|
|
699
|
+
headers["Authorization"] = basic_auth_header(username, password)
|
|
700
|
+
|
|
701
|
+
return url, headers, handlers
|
|
702
|
+
|
|
703
|
+
|
|
1325
704
|
class Request:
|
|
1326
705
|
def __init__(self, headers=None, use_proxy=True, force=False, timeout=10, validate_certs=True,
|
|
1327
706
|
url_username=None, url_password=None, http_agent=None, force_basic_auth=False,
|
|
1328
707
|
follow_redirects='urllib2', client_cert=None, client_key=None, cookies=None, unix_socket=None,
|
|
1329
|
-
ca_path=None, unredirected_headers=None, decompress=True, ciphers=None, use_netrc=True
|
|
708
|
+
ca_path=None, unredirected_headers=None, decompress=True, ciphers=None, use_netrc=True,
|
|
709
|
+
context=None):
|
|
1330
710
|
"""This class works somewhat similarly to the ``Session`` class of from requests
|
|
1331
711
|
by defining a cookiejar that can be used across requests as well as cascaded defaults that
|
|
1332
712
|
can apply to repeated requests
|
|
@@ -1365,6 +745,7 @@ class Request:
|
|
|
1365
745
|
self.decompress = decompress
|
|
1366
746
|
self.ciphers = ciphers
|
|
1367
747
|
self.use_netrc = use_netrc
|
|
748
|
+
self.context = context
|
|
1368
749
|
if isinstance(cookies, cookiejar.CookieJar):
|
|
1369
750
|
self.cookies = cookies
|
|
1370
751
|
else:
|
|
@@ -1381,9 +762,9 @@ class Request:
|
|
|
1381
762
|
force_basic_auth=None, follow_redirects=None,
|
|
1382
763
|
client_cert=None, client_key=None, cookies=None, use_gssapi=False,
|
|
1383
764
|
unix_socket=None, ca_path=None, unredirected_headers=None, decompress=None,
|
|
1384
|
-
ciphers=None, use_netrc=None):
|
|
765
|
+
ciphers=None, use_netrc=None, context=None):
|
|
1385
766
|
"""
|
|
1386
|
-
Sends a request via HTTP(S) or FTP using
|
|
767
|
+
Sends a request via HTTP(S) or FTP using urllib (Python3)
|
|
1387
768
|
|
|
1388
769
|
Does not require the module environment
|
|
1389
770
|
|
|
@@ -1408,7 +789,7 @@ class Request:
|
|
|
1408
789
|
:kwarg http_agent: (optional) String of the User-Agent to use in the request
|
|
1409
790
|
:kwarg force_basic_auth: (optional) Boolean determining if auth header should be sent in the initial request
|
|
1410
791
|
:kwarg follow_redirects: (optional) String of urllib2, all/yes, safe, none to determine how redirects are
|
|
1411
|
-
followed, see
|
|
792
|
+
followed, see HTTPRedirectHandler for more information
|
|
1412
793
|
:kwarg client_cert: (optional) PEM formatted certificate chain file to be used for SSL client authentication.
|
|
1413
794
|
This file can also include the key as well, and if the key is included, client_key is not required
|
|
1414
795
|
:kwarg client_key: (optional) PEM formatted file that contains your private key to be used for SSL client
|
|
@@ -1423,11 +804,11 @@ class Request:
|
|
|
1423
804
|
:kwarg decompress: (optional) Whether to attempt to decompress gzip content-encoded responses
|
|
1424
805
|
:kwarg ciphers: (optional) List of ciphers to use
|
|
1425
806
|
:kwarg use_netrc: (optional) Boolean determining whether to use credentials from ~/.netrc file
|
|
807
|
+
:kwarg context: (optional) ssl.Context object for SSL validation. When provided, all other SSL related
|
|
808
|
+
arguments are ignored. See make_context.
|
|
1426
809
|
:returns: HTTPResponse. Added in Ansible 2.9
|
|
1427
810
|
"""
|
|
1428
811
|
|
|
1429
|
-
method = method.upper()
|
|
1430
|
-
|
|
1431
812
|
if headers is None:
|
|
1432
813
|
headers = {}
|
|
1433
814
|
elif not isinstance(headers, dict):
|
|
@@ -1452,106 +833,46 @@ class Request:
|
|
|
1452
833
|
decompress = self._fallback(decompress, self.decompress)
|
|
1453
834
|
ciphers = self._fallback(ciphers, self.ciphers)
|
|
1454
835
|
use_netrc = self._fallback(use_netrc, self.use_netrc)
|
|
836
|
+
context = self._fallback(context, self.context)
|
|
1455
837
|
|
|
1456
838
|
handlers = []
|
|
1457
839
|
|
|
1458
840
|
if unix_socket:
|
|
1459
841
|
handlers.append(UnixHTTPHandler(unix_socket))
|
|
1460
842
|
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
password = url_password
|
|
1465
|
-
|
|
1466
|
-
if username:
|
|
1467
|
-
netloc = parsed.netloc
|
|
1468
|
-
elif '@' in parsed.netloc:
|
|
1469
|
-
credentials, netloc = parsed.netloc.split('@', 1)
|
|
1470
|
-
if ':' in credentials:
|
|
1471
|
-
username, password = credentials.split(':', 1)
|
|
1472
|
-
else:
|
|
1473
|
-
username = credentials
|
|
1474
|
-
password = ''
|
|
1475
|
-
|
|
1476
|
-
parsed_list = parsed.as_list()
|
|
1477
|
-
parsed_list[1] = netloc
|
|
1478
|
-
|
|
1479
|
-
# reconstruct url without credentials
|
|
1480
|
-
url = urlunparse(parsed_list)
|
|
1481
|
-
|
|
1482
|
-
if use_gssapi:
|
|
1483
|
-
if HTTPGSSAPIAuthHandler: # type: ignore[truthy-function]
|
|
1484
|
-
handlers.append(HTTPGSSAPIAuthHandler(username, password))
|
|
1485
|
-
else:
|
|
1486
|
-
imp_err_msg = missing_required_lib('gssapi', reason='for use_gssapi=True',
|
|
1487
|
-
url='https://pypi.org/project/gssapi/')
|
|
1488
|
-
raise MissingModuleError(imp_err_msg, import_traceback=GSSAPI_IMP_ERR)
|
|
1489
|
-
|
|
1490
|
-
elif username and not force_basic_auth:
|
|
1491
|
-
passman = urllib_request.HTTPPasswordMgrWithDefaultRealm()
|
|
1492
|
-
|
|
1493
|
-
# this creates a password manager
|
|
1494
|
-
passman.add_password(None, netloc, username, password)
|
|
1495
|
-
|
|
1496
|
-
# because we have put None at the start it will always
|
|
1497
|
-
# use this username/password combination for urls
|
|
1498
|
-
# for which `theurl` is a super-url
|
|
1499
|
-
authhandler = urllib_request.HTTPBasicAuthHandler(passman)
|
|
1500
|
-
digest_authhandler = urllib_request.HTTPDigestAuthHandler(passman)
|
|
1501
|
-
|
|
1502
|
-
# create the AuthHandler
|
|
1503
|
-
handlers.append(authhandler)
|
|
1504
|
-
handlers.append(digest_authhandler)
|
|
1505
|
-
|
|
1506
|
-
elif username and force_basic_auth:
|
|
1507
|
-
headers["Authorization"] = basic_auth_header(username, password)
|
|
1508
|
-
|
|
1509
|
-
elif use_netrc:
|
|
1510
|
-
try:
|
|
1511
|
-
rc = netrc.netrc(os.environ.get('NETRC'))
|
|
1512
|
-
login = rc.authenticators(parsed.hostname)
|
|
1513
|
-
except IOError:
|
|
1514
|
-
login = None
|
|
1515
|
-
|
|
1516
|
-
if login:
|
|
1517
|
-
username, dummy, password = login
|
|
1518
|
-
if username and password:
|
|
1519
|
-
headers["Authorization"] = basic_auth_header(username, password)
|
|
843
|
+
url, auth_headers, auth_handlers = _configure_auth(url, url_username, url_password, use_gssapi, force_basic_auth, use_netrc)
|
|
844
|
+
headers.update(auth_headers)
|
|
845
|
+
handlers.extend(auth_handlers)
|
|
1520
846
|
|
|
1521
847
|
if not use_proxy:
|
|
1522
|
-
proxyhandler =
|
|
848
|
+
proxyhandler = urllib.request.ProxyHandler({})
|
|
1523
849
|
handlers.append(proxyhandler)
|
|
1524
850
|
|
|
1525
|
-
if not
|
|
1526
|
-
ssl_handler = maybe_add_ssl_handler(url, validate_certs, ca_path=ca_path, ciphers=ciphers)
|
|
1527
|
-
if ssl_handler:
|
|
1528
|
-
handlers.append(ssl_handler)
|
|
1529
|
-
else:
|
|
1530
|
-
tmp_ca_path, cadata, paths_checked = get_ca_certs(ca_path)
|
|
851
|
+
if not context:
|
|
1531
852
|
context = make_context(
|
|
1532
|
-
cafile=
|
|
1533
|
-
cadata=cadata,
|
|
853
|
+
cafile=ca_path,
|
|
1534
854
|
ciphers=ciphers,
|
|
1535
855
|
validate_certs=validate_certs,
|
|
1536
856
|
client_cert=client_cert,
|
|
1537
857
|
client_key=client_key,
|
|
1538
858
|
)
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
859
|
+
if unix_socket:
|
|
860
|
+
ssl_handler = UnixHTTPSHandler(unix_socket=unix_socket, context=context)
|
|
861
|
+
else:
|
|
862
|
+
ssl_handler = urllib.request.HTTPSHandler(context=context)
|
|
863
|
+
handlers.append(ssl_handler)
|
|
1543
864
|
|
|
1544
|
-
handlers.append(
|
|
865
|
+
handlers.append(HTTPRedirectHandler(follow_redirects))
|
|
1545
866
|
|
|
1546
867
|
# add some nicer cookie handling
|
|
1547
868
|
if cookies is not None:
|
|
1548
|
-
handlers.append(
|
|
869
|
+
handlers.append(urllib.request.HTTPCookieProcessor(cookies))
|
|
1549
870
|
|
|
1550
|
-
opener =
|
|
1551
|
-
|
|
871
|
+
opener = urllib.request.build_opener(*handlers)
|
|
872
|
+
urllib.request.install_opener(opener)
|
|
1552
873
|
|
|
1553
874
|
data = to_bytes(data, nonstring='passthru')
|
|
1554
|
-
request =
|
|
875
|
+
request = urllib.request.Request(url, data=data, method=method.upper())
|
|
1555
876
|
|
|
1556
877
|
# add the custom agent header, to help prevent issues
|
|
1557
878
|
# with sites that block the default urllib agent string
|
|
@@ -1575,25 +896,13 @@ class Request:
|
|
|
1575
896
|
else:
|
|
1576
897
|
request.add_header(header, headers[header])
|
|
1577
898
|
|
|
1578
|
-
r =
|
|
899
|
+
r = urllib.request.urlopen(request, None, timeout)
|
|
1579
900
|
if decompress and r.headers.get('content-encoding', '').lower() == 'gzip':
|
|
1580
901
|
fp = GzipDecodedReader(r.fp)
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
r.length = None
|
|
1586
|
-
else:
|
|
1587
|
-
# Py2 maps ``r.read`` to ``fp.read``, create new ``addinfourl``
|
|
1588
|
-
# object to compensate
|
|
1589
|
-
msg = r.msg
|
|
1590
|
-
r = urllib_request.addinfourl(
|
|
1591
|
-
fp,
|
|
1592
|
-
r.info(),
|
|
1593
|
-
r.geturl(),
|
|
1594
|
-
r.getcode()
|
|
1595
|
-
)
|
|
1596
|
-
r.msg = msg
|
|
902
|
+
r.fp = fp
|
|
903
|
+
# Content-Length does not match gzip decoded length
|
|
904
|
+
# Prevent ``r.read`` from stopping at Content-Length
|
|
905
|
+
r.length = None
|
|
1597
906
|
return r
|
|
1598
907
|
|
|
1599
908
|
def get(self, url, **kwargs):
|
|
@@ -1678,7 +987,7 @@ def open_url(url, data=None, headers=None, method=None, use_proxy=True,
|
|
|
1678
987
|
use_gssapi=False, unix_socket=None, ca_path=None,
|
|
1679
988
|
unredirected_headers=None, decompress=True, ciphers=None, use_netrc=True):
|
|
1680
989
|
'''
|
|
1681
|
-
Sends a request via HTTP(S) or FTP using
|
|
990
|
+
Sends a request via HTTP(S) or FTP using urllib (Python3)
|
|
1682
991
|
|
|
1683
992
|
Does not require the module environment
|
|
1684
993
|
'''
|
|
@@ -1726,7 +1035,7 @@ def prepare_multipart(fields):
|
|
|
1726
1035
|
|
|
1727
1036
|
m = email.mime.multipart.MIMEMultipart('form-data')
|
|
1728
1037
|
for field, value in sorted(fields.items()):
|
|
1729
|
-
if isinstance(value,
|
|
1038
|
+
if isinstance(value, str):
|
|
1730
1039
|
main_type = 'text'
|
|
1731
1040
|
sub_type = 'plain'
|
|
1732
1041
|
content = value
|
|
@@ -1774,30 +1083,15 @@ def prepare_multipart(fields):
|
|
|
1774
1083
|
|
|
1775
1084
|
m.attach(part)
|
|
1776
1085
|
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
b_data = m.as_bytes(policy=email.policy.HTTP)
|
|
1781
|
-
else:
|
|
1782
|
-
# Py2
|
|
1783
|
-
# We cannot just call ``as_string`` since it provides no way
|
|
1784
|
-
# to specify ``maxheaderlen``
|
|
1785
|
-
fp = cStringIO() # cStringIO seems to be required here
|
|
1786
|
-
# Ensure headers are not split over multiple lines
|
|
1787
|
-
g = email.generator.Generator(fp, maxheaderlen=0)
|
|
1788
|
-
g.flatten(m)
|
|
1789
|
-
# ``fix_eols`` switches from ``\n`` to ``\r\n``
|
|
1790
|
-
b_data = email.utils.fix_eols(fp.getvalue())
|
|
1086
|
+
# Ensure headers are not split over multiple lines
|
|
1087
|
+
# The HTTP policy also uses CRLF by default
|
|
1088
|
+
b_data = m.as_bytes(policy=email.policy.HTTP)
|
|
1791
1089
|
del m
|
|
1792
1090
|
|
|
1793
1091
|
headers, sep, b_content = b_data.partition(b'\r\n\r\n')
|
|
1794
1092
|
del b_data
|
|
1795
1093
|
|
|
1796
|
-
|
|
1797
|
-
parser = email.parser.BytesHeaderParser().parsebytes
|
|
1798
|
-
else:
|
|
1799
|
-
# Py2
|
|
1800
|
-
parser = email.parser.HeaderParser().parsestr
|
|
1094
|
+
parser = email.parser.BytesHeaderParser().parsebytes
|
|
1801
1095
|
|
|
1802
1096
|
return (
|
|
1803
1097
|
parser(headers)['content-type'], # Message converts to native strings
|
|
@@ -1883,9 +1177,6 @@ def fetch_url(module, url, data=None, headers=None, method=None,
|
|
|
1883
1177
|
body = info['body']
|
|
1884
1178
|
"""
|
|
1885
1179
|
|
|
1886
|
-
if not HAS_URLPARSE:
|
|
1887
|
-
module.fail_json(msg='urlparse is not installed')
|
|
1888
|
-
|
|
1889
1180
|
if not HAS_GZIP:
|
|
1890
1181
|
module.fail_json(msg=GzipDecodedReader.missing_gzip_error())
|
|
1891
1182
|
|
|
@@ -1911,7 +1202,7 @@ def fetch_url(module, url, data=None, headers=None, method=None,
|
|
|
1911
1202
|
use_gssapi = module.params.get('use_gssapi', use_gssapi)
|
|
1912
1203
|
|
|
1913
1204
|
if not isinstance(cookies, cookiejar.CookieJar):
|
|
1914
|
-
cookies = cookiejar.
|
|
1205
|
+
cookies = cookiejar.CookieJar()
|
|
1915
1206
|
|
|
1916
1207
|
r = None
|
|
1917
1208
|
info = dict(url=url, status=-1)
|
|
@@ -1924,25 +1215,23 @@ def fetch_url(module, url, data=None, headers=None, method=None,
|
|
|
1924
1215
|
client_key=client_key, cookies=cookies, use_gssapi=use_gssapi,
|
|
1925
1216
|
unix_socket=unix_socket, ca_path=ca_path, unredirected_headers=unredirected_headers,
|
|
1926
1217
|
decompress=decompress, ciphers=ciphers, use_netrc=use_netrc)
|
|
1927
|
-
# Lowercase keys, to conform to py2 behavior
|
|
1928
|
-
info.update(
|
|
1218
|
+
# Lowercase keys, to conform to py2 behavior
|
|
1219
|
+
info.update({k.lower(): v for k, v in r.info().items()})
|
|
1929
1220
|
|
|
1930
1221
|
# Don't be lossy, append header values for duplicate headers
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
name =
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
temp_headers[name] = value
|
|
1941
|
-
info.update(temp_headers)
|
|
1222
|
+
temp_headers = {}
|
|
1223
|
+
for name, value in r.headers.items():
|
|
1224
|
+
# The same as above, lower case keys to match py2 behavior, and create more consistent results
|
|
1225
|
+
name = name.lower()
|
|
1226
|
+
if name in temp_headers:
|
|
1227
|
+
temp_headers[name] = ', '.join((temp_headers[name], value))
|
|
1228
|
+
else:
|
|
1229
|
+
temp_headers[name] = value
|
|
1230
|
+
info.update(temp_headers)
|
|
1942
1231
|
|
|
1943
1232
|
# parse the cookies into a nice dictionary
|
|
1944
1233
|
cookie_list = []
|
|
1945
|
-
cookie_dict =
|
|
1234
|
+
cookie_dict = {}
|
|
1946
1235
|
# Python sorts cookies in order of most specific (ie. longest) path first. See ``CookieJar._cookie_attrs``
|
|
1947
1236
|
# Cookies with the same path are reversed from response order.
|
|
1948
1237
|
# This code makes no assumptions about that, and accepts the order given by python
|
|
@@ -1954,17 +1243,11 @@ def fetch_url(module, url, data=None, headers=None, method=None,
|
|
|
1954
1243
|
info['cookies'] = cookie_dict
|
|
1955
1244
|
# finally update the result with a message about the fetch
|
|
1956
1245
|
info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), url=r.geturl(), status=r.code))
|
|
1957
|
-
except NoSSLError as e:
|
|
1958
|
-
distribution = get_distribution()
|
|
1959
|
-
if distribution is not None and distribution.lower() == 'redhat':
|
|
1960
|
-
module.fail_json(msg='%s. You can also install python-ssl from EPEL' % to_native(e), **info)
|
|
1961
|
-
else:
|
|
1962
|
-
module.fail_json(msg='%s' % to_native(e), **info)
|
|
1963
1246
|
except (ConnectionError, ValueError) as e:
|
|
1964
1247
|
module.fail_json(msg=to_native(e), **info)
|
|
1965
1248
|
except MissingModuleError as e:
|
|
1966
1249
|
module.fail_json(msg=to_text(e), exception=e.import_traceback)
|
|
1967
|
-
except
|
|
1250
|
+
except urllib.error.HTTPError as e:
|
|
1968
1251
|
r = e
|
|
1969
1252
|
try:
|
|
1970
1253
|
if e.fp is None:
|
|
@@ -1981,18 +1264,18 @@ def fetch_url(module, url, data=None, headers=None, method=None,
|
|
|
1981
1264
|
# Try to add exception info to the output but don't fail if we can't
|
|
1982
1265
|
try:
|
|
1983
1266
|
# Lowercase keys, to conform to py2 behavior, so that py3 and py2 are predictable
|
|
1984
|
-
info.update(
|
|
1267
|
+
info.update({k.lower(): v for k, v in e.info().items()})
|
|
1985
1268
|
except Exception:
|
|
1986
1269
|
pass
|
|
1987
1270
|
|
|
1988
1271
|
info.update({'msg': to_native(e), 'body': body, 'status': e.code})
|
|
1989
1272
|
|
|
1990
|
-
except
|
|
1273
|
+
except urllib.error.URLError as e:
|
|
1991
1274
|
code = int(getattr(e, 'code', -1))
|
|
1992
1275
|
info.update(dict(msg="Request failed: %s" % to_native(e), status=code))
|
|
1993
1276
|
except socket.error as e:
|
|
1994
1277
|
info.update(dict(msg="Connection failure: %s" % to_native(e), status=-1))
|
|
1995
|
-
except
|
|
1278
|
+
except http.client.BadStatusLine as e:
|
|
1996
1279
|
info.update(dict(msg="Connection failure: connection was closed before a valid response was received: %s" % to_native(e.line), status=-1))
|
|
1997
1280
|
except Exception as e:
|
|
1998
1281
|
info.update(dict(msg="An unknown error occurred: %s" % to_native(e), status=-1),
|
|
@@ -2075,7 +1358,7 @@ def fetch_file(module, url, data=None, headers=None, method=None,
|
|
|
2075
1358
|
try:
|
|
2076
1359
|
rsp, info = fetch_url(module, url, data, headers, method, use_proxy, force, last_mod_time, timeout,
|
|
2077
1360
|
unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers)
|
|
2078
|
-
if not rsp:
|
|
1361
|
+
if not rsp or (rsp.code and rsp.code >= 400):
|
|
2079
1362
|
module.fail_json(msg="Failure downloading %s, %s" % (url, info['msg']))
|
|
2080
1363
|
data = rsp.read(bufsize)
|
|
2081
1364
|
while data:
|