pax_global_header00006660000000000000000000000064146455441770014533gustar00rootroot0000000000000052 comment=bf01aac15512caa00f07858ce09168f2a4e11847 pywinrm-0.5.0/000077500000000000000000000000001464554417700132425ustar00rootroot00000000000000pywinrm-0.5.0/.coveragerc000066400000000000000000000000471464554417700153640ustar00rootroot00000000000000[run] omit = */tests* */vendor*pywinrm-0.5.0/.git-blame-ignore-revs000066400000000000000000000001331464554417700173370ustar00rootroot00000000000000# .git-blame-ignore-revs # Bulk black reformatting fca26dd0c1860738760c0827deb0a5221117636dpywinrm-0.5.0/.github/000077500000000000000000000000001464554417700146025ustar00rootroot00000000000000pywinrm-0.5.0/.github/workflows/000077500000000000000000000000001464554417700166375ustar00rootroot00000000000000pywinrm-0.5.0/.github/workflows/ci.yml000066400000000000000000000036331464554417700177620ustar00rootroot00000000000000name: Test pywinrm on: push: branches: - master pull_request: branches: - master jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - macos-latest - ubuntu-latest - windows-latest python-version: - 3.8 - 3.9 - '3.10' - '3.11' - '3.12' - pypy-3.9 - pypy-3.10 arch: - x86 - x64 exclude: - os: macos-latest arch: x86 - os: macos-latest python-version: 3.8 - os: macos-latest python-version: 3.9 - os: macos-latest python-version: '3.10' - os: macos-latest python-version: pypy-3.9 - os: macos-latest python-version: pypy-3.10 - os: ubuntu-latest arch: x86 - os: windows-latest python-version: pypy-3.9 - os: windows-latest python-version: pypy-3.10 steps: - uses: actions/checkout@v4 - name: set up python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.arch }} - name: set up Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: >- sudo apt-get install -y gcc python3-dev libkrb5-dev env: DEBIAN_FRONTEND: noninteractive - name: install dependencies run: | pip install coveralls pip install .[credssp,kerberos] pip install -r requirements-test.txt - name: run test run: | python -m black . --check python -m isort . --check-only python -m mypy . pytest -v --cov=winrm --cov-report=term-missing winrm/tests/ - name: upload coverage data run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pywinrm-0.5.0/.gitignore000066400000000000000000000007321464554417700152340ustar00rootroot00000000000000# File artifacts which may by produced regular software on Windows, Mac OS X and Linux Thumbs.db .DS_Store *.bak # Unit test / coverage reports .coverage .cache # JetBrains PyCharm configuration and working files .idea # Folder which should be used for temporary working files /sandbox/ # Python virtual environment /env/ # build outputs /build/ /dist/ /pywinrm.egg-info/ /MANIFEST *.py[co] __pycache__ ~temp/ *~ /winrm/tests/config.json .pytest_cache venv .mypy_cachepywinrm-0.5.0/CHANGELOG.md000066400000000000000000000103421464554417700150530ustar00rootroot00000000000000# Changelog ### Version 0.5.0 - Dropped Python 2.7, 3.6, and 3.7 support, minimum supported version is 3.8 - Migrate to PEP 517 compliant build with a `pyproject.toml` file - Added type annotation - Added `WSManFaultError` which contains WSManFault specific information when receiving a 500 WSMan fault response - This contains pre-parsed values like the code, subcode, wsman fault code, wmi error code, and raw response - It can be used by the caller to implement fallback behaviour based on specific error codes - Added public API `protocol.build_wsman_header` that can create the standard WSMan header used by the protocol - This can be used to craft custom WSMan messages that are not supported in the existing actions - Added public API `protocol.get_command_output_raw` - This can be used to send a single WSMan receive request and get the output - Unlike `protocol.get_command_output`, it will not loop until the command is done and will not catch a timeout exception ### Version 0.4.3 - Fix invalid regex escape sequences. - Decoding CLIXML failures for `run_ps` will create a `UserWarning` rather than printing the warning. - Remove usage of deprecated Python API to support Python 3.11 ### Version 0.4.2 - Dropped Python 3.5 from support matrix as it is EOL. - Remove dependency on `distutils` that is deprecated in Python 3.10. ### Version 0.4.1 - HOT FIX: Fixing an issue with `requests_kerberos` not imported correctly from the changes in `0.4.0`. ### Version 0.4.0 - Ensure `server_cert_validation=ignore` supersedes ca_trust_path/env overrides - Added deprecated warnings if CA trusts defined by environment variables are used. - Set minimum version of requests-credssp to support Kerberos auth over CredSSP and other changes - Added `proxy` support where it can be defined within the application, with the ability to specify the proxy within the application - Fix for shell not setting all environment variables. - Fix session clixml encoding on Python 3 - `Protocol.close_shell(shell_id)` will now close the session(and TCP connections) to the Windows machine. `close_session` option has been added in case of leaving the session alone. - Add a function to send input to a running process. ### Version 0.3.0 - Added support for message encryption over HTTP when using NTLM/Kerberos/CredSSP - Added parameter to disable TLSv1.2 when using CredSSP for Server 2008 support - Error detail from SOAP fault (if present) is now included with HTTP 500 errors - Fixed CA path override (incl envvar) - Fixed Kerberos service override - Try harder to suppress urllib3 InsecureRequestWarnings on various OSs - Fixed timeout values to parse correctly if passed as strings - Various updates to CI/tests ### Version 0.2.2 - Added support for CredSSP authenication (via requests-credssp) - Improved README, see 'Valid transport options' section - Run unit tests on Linux / Travis CI on Python 2.6-2.7, 3.3-3.6, PyPy2 - Run integration tests on Windows / AppVeyor on Python 2.7, 3.3-3.5 - Drop support for Python 3.0-3.2 due to lack of explicit unicode literal, see pep-0414 - Drop support for Python 2.6 on Windows - Add support for Python 3.6-dev on Linux ### Version 0.2.1 - Minor import bugfix for error "'module' object has no attribute 'util'" when using Kerberos delegation on older Python builds ### Version 0.2.0 - Switched core HTTP transport from urllib2 to requests - Added support for NTLM (via requests_ntlm) - Added support for kerberos delegation (via requests_kerberos) - Added support for explicit kerberos principals (in conjuction w/ pykerberos bugfix) - Timeouts are more configurable ### Version 0.1.1 - Force basic auth header to avoid additional HTTP request and reduce latency - Python 2.7.9+. Allow server cert validation to be ignored using SSLContext.verify_mode - Tests. Enable Python 3.4 on Travis CI ### Version 0.0.3 - Use xmltodict instead of not supported xmlwitch - Add certificate authentication support - Setup PyPI classifiers - Fix. Include UUID when sending request - Fix. Python 2.6.6/CentOS. Use tuples instead of lists in setup.py - Fix. Python 2.6. String formatting - Handle unauthorized response and raise UnauthorizeError - Convert different forms of short urls into full well-formed endpoint - Add Session.run_ps() helper to execute PowerShell scripts pywinrm-0.5.0/LICENSE000066400000000000000000000020371464554417700142510ustar00rootroot00000000000000Copyright (c) 2013 Alexey Diyan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.pywinrm-0.5.0/MANIFEST.in000066400000000000000000000000211464554417700147710ustar00rootroot00000000000000include LICENSE pywinrm-0.5.0/README.md000066400000000000000000000205421464554417700145240ustar00rootroot00000000000000# pywinrm pywinrm is a Python client for the Windows Remote Management (WinRM) service. It allows you to invoke commands on target Windows machines from any machine that can run Python. [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/diyan/pywinrm/blob/master/LICENSE) [![Test workflow](https://github.com/diyan/pywinrm/actions/workflows/ci.yml/badge.svg)](https://github.com/diyan/pywinrm/actions/workflows/ci.yml) [![Coverage](https://coveralls.io/repos/diyan/pywinrm/badge.svg)](https://coveralls.io/r/diyan/pywinrm) [![PyPI](https://img.shields.io/pypi/dm/pywinrm.svg)](https://pypi.python.org/pypi/pywinrm) WinRM allows you to perform various management tasks remotely. These include, but are not limited to: running batch scripts, powershell scripts, and fetching WMI variables. Used by [Ansible](https://www.ansible.com/) for Windows support. For more information on WinRM, please visit [Microsoft's WinRM site](http://msdn.microsoft.com/en-us/library/aa384426.aspx). ## Requirements * Linux, Mac OS X or Windows * CPython 3.8+ or PyPy3 * [requests-kerberos](http://pypi.python.org/pypi/requests-kerberos) and [requests-credssp](https://github.com/jborean93/requests-credssp) is optional ## Installation ### To install pywinrm with support for basic, certificate, and NTLM auth, simply ```bash $ pip install pywinrm ``` ### To use Kerberos authentication you need these optional dependencies ```bash # for Debian/Ubuntu/etc: $ sudo apt-get install gcc python3-dev libkrb5-dev $ pip install pywinrm[kerberos] # for RHEL/CentOS/etc: $ sudo dnf install gcc krb5-devel krb5-workstation python3-devel $ pip install pywinrm[kerberos] ``` ### To use CredSSP authentication you need these optional dependencies ```bash $ pip install pywinrm[credssp] ``` ## Example Usage ### Run a process on a remote host ```python import winrm s = winrm.Session('windows-host.example.com', auth=('john.smith', 'secret')) r = s.run_cmd('ipconfig', ['/all']) >>> r.status_code 0 >>> r.std_out Windows IP Configuration Host Name . . . . . . . . . . . . : WINDOWS-HOST Primary Dns Suffix . . . . . . . : Node Type . . . . . . . . . . . . : Hybrid IP Routing Enabled. . . . . . . . : No WINS Proxy Enabled. . . . . . . . : No ... >>> r.std_err ``` NOTE: pywinrm will try and guess the correct endpoint url from the following formats: - windows-host -> http://windows-host:5985/wsman - windows-host:1111 -> http://windows-host:1111/wsman - http://windows-host -> http://windows-host:5985/wsman - http://windows-host:1111 -> http://windows-host:1111/wsman - http://windows-host:1111/wsman -> http://windows-host:1111/wsman ### Run Powershell script on remote host ```python import winrm ps_script = """$strComputer = $Host Clear $RAM = WmiObject Win32_ComputerSystem $MB = 1048576 "Installed Memory: " + [int]($RAM.TotalPhysicalMemory /$MB) + " MB" """ s = winrm.Session('windows-host.example.com', auth=('john.smith', 'secret')) r = s.run_ps(ps_script) >>> r.status_code 0 >>> r.std_out Installed Memory: 3840 MB >>> r.std_err ``` Powershell scripts will be base64 UTF16 little-endian encoded prior to sending to the Windows host. Error messages are converted from the Powershell CLIXML format to a human readable format as a convenience. ### Run process with low-level API with domain user, disabling HTTPS cert validation ```python from winrm.protocol import Protocol p = Protocol( endpoint='https://windows-host:5986/wsman', transport='ntlm', username=r'somedomain\someuser', password='secret', server_cert_validation='ignore') shell_id = p.open_shell() command_id = p.run_command(shell_id, 'ipconfig', ['/all']) std_out, std_err, status_code = p.get_command_output(shell_id, command_id) p.cleanup_command(shell_id, command_id) p.close_shell(shell_id) ``` ### Valid transport options pywinrm supports various transport methods in order to authenticate with the WinRM server. The options that are supported in the `transport` parameter are; * `basic`: Basic auth only works for local Windows accounts not domain accounts. Credentials are base64 encoded when sending to the server. * `plaintext`: Same as basic auth. * `certificate`: Authentication is done through a certificate that is mapped to a local Windows account on the server. * `ssl`: When used in conjunction with `cert_pem` and `cert_key_pem` it will use a certificate as above. If not will revert to basic auth over HTTPS. * `kerberos`: Will use Kerberos authentication for domain accounts which only works when the client is in the same domain as the server and the required dependencies are installed. Currently a Kerberos ticket needs to be initialized outside of pywinrm using the `kinit` command. * `ntlm`: Will use NTLM authentication for both domain and local accounts. * `credssp`: Will use CredSSP authentication for both domain and local accounts. Allows double hop authentication. This only works over a HTTPS endpoint and not HTTP. ### Encryption By default, WinRM will not accept unencrypted communication with a client. There are two ways to enable encrypted communication with pywinrm: 1. Use an HTTPS endpoint instead of HTTP (Recommended) 2. Use NTLM, Kerberos, or CredSSP as the transport auth Using an HTTPS endpoint is recommended, as it will encrypt all the data sent to the server (including all headers), works securely with all auth types, and can properly verify remote host identity (when used with certificates signed by a verifiable certificate authority). The second option is to use NTLM, Kerberos, or CredSSP, and set the `message_encryption` arg to protocol to `auto` (the default value) or `always`. This will use the authentication GSS-API Wrap and Unwrap methods to encrypt the message contents sent to the server. This form of encryption is independent of the transport layer, and the strength of the encryption used varies with the underlying authentication type selected (NTLM generally being the weakest and CredSSP the strongest). To configure message encryption you can use the `message_encryption` argument when initialising protocol. This option has 3 values that can be set as shown below. * `auto`: Default, Will only use message encryption if it is available for the auth method and HTTPS isn't used. * `never`: Will never use message encryption even when not over HTTPS. * `always`: Will always use message encryption even when running over HTTPS (fails if encryption support is unavailable on the selected auth method). If you set the value to `always` and the transport opt doesn't support message encryption (e.g., `basic` auth or an old version of `pykerberos` without message encryption support is installed), pywinrm will throw an exception. If you do not use an HTTPS endpoint or message encryption, a default-configured WinRM server will automatically reject requests from pywinrm. Server settings can be modified allow unencrypted messages and credentials, but this is highly insecure and should only be used for diagnostic purposes. To allow unencrypted communications, run the following on the WinRM server (cmd and powershell versions provided): ``` # from cmd winrm set winrm/config/service @{AllowUnencrypted="true"} # or from powershell Set-Item -Path "WSMan:\localhost\Service\AllowUnencrypted" -Value $true ``` Again, this should *not* be used in production environments, as your credentials and WinRM messages can be trivially recovered. ### Enabling WinRM on remote host Enable WinRM over HTTP for test usage (includes firewall rules): ``` winrm quickconfig ``` Enable WinRM basic authentication. For domain users, it is necessary to use NTLM, Kerberos, or CredSSP authentication (Kerberos and NTLM authentication are enabled by default, CredSSP is not). ``` # from cmd: winrm set winrm/config/service/auth @{Basic="true"} ``` Enable WinRM CredSSP authentication. This allows double hop support so you can authenticate with a network service when running command son the remote host. This command is run in Powershell. ```powershell Enable-WSManCredSSP -Role Server -Force Set-Item -Path "WSMan:\localhost\Service\Auth\CredSSP" -Value $true ``` ### Contributors (alphabetically) - Alessandro Pilotti - Alexey Diyan - Chris Church - David Cournapeau - Gema Gomez - Jijo Varghese - Jordan Borean - Juan J. Martinez - Lukas Bednar - Manuel Sabban - Matt Clark - Matt Davis - Maxim Kovgan - Nir Cohen - Patrick Dunnigan - Reina Abolofia Want to help - send a pull request. I will accept good pull requests for sure. pywinrm-0.5.0/pyproject.toml000066400000000000000000000054571464554417700161710ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 61.0.0", # Support for setuptools config in pyproject.toml ] build-backend = "setuptools.build_meta" [project] name = "pywinrm" description = "Python library for Windows Remote Management" readme = "README.md" requires-python = ">=3.8" license = { file = "LICENSE" } authors = [ { name = "Alexey Diyan", email = "alexey.diyan@gmail.com" } ] keywords = ["winrm", "ws-man", "devops", "ws-management"] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Distributed Computing", "Topic :: System :: Systems Administration" ] dependencies = [ "requests >= 2.9.1", "requests_ntlm >= 1.1.0", "xmltodict" ] dynamic = ["version"] [project.urls] homepage = "http://github.com/diyan/pywinrm/" [project.optional-dependencies] credssp = [ "requests-credssp >= 1.0.0" ] kerberos = [ "pykerberos >= 1.2.1, < 2.0.0; sys_platform != 'win32'", "winkerberos >= 0.5.0; sys_platform == 'win32'" ] [tool.setuptools] include-package-data = true [tool.setupstools.packages] find = {} [tool.setuptools.package-data] "winrm" = ["py.typed"] "winrm.tests" = ["*.ps1"] [tool.setuptools.dynamic] version = { attr = "winrm.__version__" } [tool.black] line-length = 160 exclude = ''' /( \.git | \.venv | build | dist | winrm/vendor )/ ''' [tool.isort] profile = "black" [tool.mypy] exclude = "build/|winrm/tests/|winrm/vendor/" mypy_path = "$MYPY_CONFIG_FILE_DIR" python_version = "3.8" show_error_codes = true show_column_numbers = true disallow_any_unimported = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = true no_implicit_reexport = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true warn_unreachable = true [[tool.mypy.overrides]] module = "winrm.vendor.*" follow_imports = "skip" [[tool.mypy.overrides]] module = "requests.packages.urllib3.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "requests_credssp" ignore_missing_imports = true [[tool.mypy.overrides]] module = "requests_ntlm" ignore_missing_imports = truepywinrm-0.5.0/requirements-test.txt000066400000000000000000000002521464554417700175020ustar00rootroot00000000000000# this assumes the base requirements have been satisfied via setup.py black == 24.4.2 isort == 5.13.2 mypy == 1.10.0 pytest pytest-cov mock types-requests types-xmltodictpywinrm-0.5.0/winrm/000077500000000000000000000000001464554417700143765ustar00rootroot00000000000000pywinrm-0.5.0/winrm/__init__.py000066400000000000000000000124421464554417700165120ustar00rootroot00000000000000from __future__ import annotations import collections.abc import re import typing as t import warnings import xml.etree.ElementTree as ET from base64 import b64encode from winrm.protocol import Protocol __version__ = "0.5.0" # Feature support attributes for multi-version clients. # These values can be easily checked for with hasattr(winrm, "FEATURE_X"), # "'auth_type' in winrm.FEATURE_SUPPORTED_AUTHTYPES", etc for clients to sniff features # supported by a particular version of pywinrm FEATURE_SUPPORTED_AUTHTYPES = ["basic", "certificate", "ntlm", "kerberos", "plaintext", "ssl", "credssp"] FEATURE_READ_TIMEOUT = True FEATURE_OPERATION_TIMEOUT = True FEATURE_PROXY_SUPPORT = True class Response(object): """Response from a remote command execution""" def __init__(self, args: tuple[bytes, bytes, int]) -> None: self.std_out, self.std_err, self.status_code = args def __repr__(self) -> str: # TODO put tree dots at the end if out/err was truncated return ''.format(self.status_code, self.std_out[:20], self.std_err[:20]) class Session(object): # TODO implement context manager methods def __init__(self, target: str, auth: tuple[str, str], **kwargs: t.Any) -> None: username, password = auth self.url = self._build_url(target, kwargs.get("transport", "plaintext")) self.protocol = Protocol(self.url, username=username, password=password, **kwargs) def run_cmd(self, command: str, args: collections.abc.Iterable[str | bytes] = ()) -> Response: # TODO optimize perf. Do not call open/close shell every time shell_id = self.protocol.open_shell() command_id = self.protocol.run_command(shell_id, command, args) rs = Response(self.protocol.get_command_output(shell_id, command_id)) self.protocol.cleanup_command(shell_id, command_id) self.protocol.close_shell(shell_id) return rs def run_ps(self, script: str) -> Response: """base64 encodes a Powershell script and executes the powershell encoded script command """ # must use utf16 little endian on windows encoded_ps = b64encode(script.encode("utf_16_le")).decode("ascii") rs = self.run_cmd("powershell -encodedcommand {0}".format(encoded_ps)) if len(rs.std_err): # if there was an error message, clean it it up and make it human # readable rs.std_err = self._clean_error_msg(rs.std_err) return rs def _clean_error_msg(self, msg: bytes) -> bytes: """converts a Powershell CLIXML message to a more human readable string""" # TODO prepare unit test, beautify code # if the msg does not start with this, return it as is if msg.startswith(b"#< CLIXML\r\n"): # for proper xml, we need to remove the CLIXML part # (the first line) msg_xml = msg[11:] try: # remove the namespaces from the xml for easier processing msg_xml = self._strip_namespace(msg_xml) root = ET.fromstring(msg_xml) # the S node is the error message, find all S nodes nodes = root.findall("./S") new_msg = "" for s in nodes: # append error msg string to result, also # the hex chars represent CRLF so we replace with newline if s.text: new_msg += s.text.replace("_x000D__x000A_", "\n") except Exception as e: # if any of the above fails, the msg was not true xml # print a warning and return the original string warnings.warn("There was a problem converting the Powershell error " "message: %s" % (e)) else: # if new_msg was populated, that's our error message # otherwise the original error message will be used if len(new_msg): # remove leading and trailing whitespace while we are here return new_msg.strip().encode("utf-8") # either failed to decode CLIXML or there was nothing to decode # just return the original message return msg def _strip_namespace(self, xml: bytes) -> bytes: """strips any namespaces from an xml string""" p = re.compile(b'xmlns=*[""][^""]*[""]') allmatches = p.finditer(xml) for match in allmatches: xml = xml.replace(match.group(), b"") return xml @staticmethod def _build_url(target: str, transport: str) -> str: match = re.match(r"(?i)^((?Phttp[s]?)://)?(?P[0-9a-z-_.]+)(:(?P\d+))?(?P(/)?(wsman)?)?", target) # NOQA if not match: raise ValueError("Invalid target URL: {0}".format(target)) scheme = match.group("scheme") if not scheme: # TODO do we have anything other than HTTP/HTTPS scheme = "https" if transport == "ssl" else "http" host = match.group("host") port = match.group("port") if not port: port = 5986 if transport == "ssl" else 5985 path = match.group("path") if not path: path = "wsman" return "{0}://{1}:{2}/{3}".format(scheme, host, port, path.lstrip("/")) pywinrm-0.5.0/winrm/encryption.py000066400000000000000000000250711464554417700171470ustar00rootroot00000000000000from __future__ import annotations import re import struct from urllib.parse import urlsplit import requests from winrm.exceptions import WinRMError class Encryption(object): SIXTEN_KB = 16384 MIME_BOUNDARY = b"--Encrypted Boundary" def __init__(self, session: requests.Session, protocol: str) -> None: """ [MS-WSMV] v30.0 2016-07-14 2.2.9.1 Encrypted Message Types When using Encryption, there are three options available 1. Negotiate/SPNEGO 2. Kerberos 3. CredSSP Details for each implementation can be found in this document under this section This init sets the following values to use to encrypt and decrypt. This is to help generify the methods used in the body of the class. wrap: A method that will return the encrypted message and a signature unwrap: A method that will return an unencrypted message and verify the signature protocol_string: The protocol string used for the particular auth protocol :param session: The handle of the session to get GSS-API wrap and unwrap methods :param protocol: The auth protocol used, will determine the wrapping and unwrapping method plus the protocol string to use. Currently only NTLM and CredSSP is supported """ self.protocol = protocol self.session = session if protocol == "ntlm": # Details under Negotiate [2.2.9.1.1] in MS-WSMV self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted" self._build_message = self._build_ntlm_message self._decrypt_message = self._decrypt_ntlm_message elif protocol == "credssp": # Details under CredSSP [2.2.9.1.3] in MS-WSMV self.protocol_string = b"application/HTTP-CredSSP-session-encrypted" self._build_message = self._build_credssp_message self._decrypt_message = self._decrypt_credssp_message elif protocol == "kerberos": self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted" self._build_message = self._build_kerberos_message self._decrypt_message = self._decrypt_kerberos_message else: raise WinRMError("Encryption for protocol '%s' not supported in pywinrm" % protocol) def prepare_encrypted_request(self, session: requests.Session, endpoint: str | bytes, message: bytes) -> requests.PreparedRequest: """ Creates a prepared request to send to the server with an encrypted message and correct headers :param session: The handle of the session to prepare requests with :param endpoint: The endpoint/server to prepare requests to :param message: The unencrypted message to send to the server :return: A prepared request that has an encrypted message """ host = urlsplit(endpoint).hostname if self.protocol == "credssp" and len(message) > self.SIXTEN_KB: content_type = "multipart/x-multi-encrypted" encrypted_message = b"" message_chunks = [message[i : i + self.SIXTEN_KB] for i in range(0, len(message), self.SIXTEN_KB)] for message_chunk in message_chunks: encrypted_chunk = self._encrypt_message(message_chunk, host) encrypted_message += encrypted_chunk else: content_type = "multipart/encrypted" encrypted_message = self._encrypt_message(message, host) encrypted_message += self.MIME_BOUNDARY + b"--\r\n" request = requests.Request("POST", endpoint, data=encrypted_message) prepared_request = session.prepare_request(request) prepared_request.headers["Content-Length"] = str(len(prepared_request.body)) if prepared_request.body else "0" prepared_request.headers["Content-Type"] = '{0};protocol="{1}";boundary="Encrypted Boundary"'.format(content_type, self.protocol_string.decode()) return prepared_request def parse_encrypted_response(self, response: requests.Response) -> bytes: """ Takes in the encrypted response from the server and decrypts it :param response: The response that needs to be decrypted :return: The unencrypted message from the server """ content_type = response.headers["Content-Type"] if 'protocol="{0}"'.format(self.protocol_string.decode()) in content_type: host = urlsplit(response.request.url).hostname msg = self._decrypt_response(response, host) else: msg = response.content return msg def _encrypt_message(self, message: bytes, host: str | bytes | None) -> bytes: message_length = str(len(message)).encode() encrypted_stream = self._build_message(message, host) message_payload = ( self.MIME_BOUNDARY + b"\r\n" b"\tContent-Type: " + self.protocol_string + b"\r\n" b"\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=" + message_length + b"\r\n" + self.MIME_BOUNDARY + b"\r\n" b"\tContent-Type: application/octet-stream\r\n" + encrypted_stream ) return message_payload def _decrypt_response(self, response: requests.Response, host: str | bytes | None) -> bytes: parts = response.content.split(self.MIME_BOUNDARY + b"\r\n") parts = list(filter(None, parts)) # filter out empty parts of the split message = b"" for i in range(0, len(parts)): if i % 2 == 1: continue header = parts[i].strip() payload = parts[i + 1] expected_length = int(header.split(b"Length=")[1]) # remove the end MIME block if it exists if payload.endswith(self.MIME_BOUNDARY + b"--\r\n"): payload = payload[: len(payload) - 24] encrypted_data = payload.replace(b"\tContent-Type: application/octet-stream\r\n", b"") decrypted_message = self._decrypt_message(encrypted_data, host) actual_length = len(decrypted_message) if actual_length != expected_length: raise WinRMError("Encrypted length from server does not match the " "expected size, message has been tampered with") message += decrypted_message return message def _decrypt_ntlm_message(self, encrypted_data: bytes, host: str | bytes | None) -> bytes: signature_length = struct.unpack(" bytes: # trailer_length = struct.unpack(" bytes: signature_length = struct.unpack(" bytes: sealed_message, signature = self.session.auth.session_security.wrap(message) # type: ignore[union-attr] signature_length = struct.pack(" bytes: credssp_context = self.session.auth.contexts[host] # type: ignore[union-attr] sealed_message = credssp_context.wrap(message) cipher_negotiated = credssp_context.tls_connection.get_cipher_name() trailer_length = self._get_credssp_trailer_length(len(message), cipher_negotiated) return struct.pack(" bytes: sealed_message, signature = self.session.auth.wrap_winrm(host, message) # type: ignore[union-attr] signature_length = struct.pack(" int: # I really don't like the way this works but can't find a better way, MS # allows you to get this info through the struct SecPkgContext_StreamSizes # but there is no GSSAPI/OpenSSL equivalent so we need to calculate it # ourselves if re.match(r"^.*-GCM-[\w\d]*$", cipher_suite): # We are using GCM for the cipher suite, GCM has a fixed length of 16 # bytes for the TLS trailer making it easy for us trailer_length = 16 else: # We are not using GCM so need to calculate the trailer size. The # trailer length is equal to the length of the hmac + the length of the # padding required by the block cipher hash_algorithm = cipher_suite.split("-")[-1] # while there are other algorithms, SChannel doesn't support them # as of yet https://msdn.microsoft.com/en-us/library/windows/desktop/aa374757(v=vs.85).aspx if hash_algorithm == "MD5": hash_length = 16 elif hash_algorithm == "SHA": hash_length = 20 elif hash_algorithm == "SHA256": hash_length = 32 elif hash_algorithm == "SHA384": hash_length = 48 else: hash_length = 0 pre_pad_length = message_length + hash_length if "RC4" in cipher_suite: # RC4 is a stream cipher so no padding would be added padding_length = 0 elif "DES" in cipher_suite or "3DES" in cipher_suite: # 3DES is a 64 bit block cipher padding_length = 8 - (pre_pad_length % 8) else: # AES is a 128 bit block cipher padding_length = 16 - (pre_pad_length % 16) trailer_length = (pre_pad_length + padding_length) - message_length return trailer_length pywinrm-0.5.0/winrm/exceptions.py000066400000000000000000000063311464554417700171340ustar00rootroot00000000000000from __future__ import annotations class WinRMError(Exception): """ "Generic WinRM error""" code = 500 class WSManFaultError(WinRMError): """WSMan Fault Error. Exception that is raised when receiving a WSMan fault message. It contains the raw response as well as the fault details parsed from the response. The wsman_fault_code is returned by the Microsoft WSMan server rather than the WSMan protocol error code strings. The wmierror_code can contain more fatal service error codes returned as a MSFT_WmiError object, for example quota violations. @param int code: The HTTP status code of the response. @param str message: The error message. @param str response: The raw WSMan response text. @param str reason: The WSMan fault reason. @param string fault_code: The WSMan fault code. @param string fault_subcode: The WSMan fault subcode. @param int wsman_fault_code: The MS WSManFault specific code. @param int wmierror_code: The MS WMI error code. """ def __init__( self, code: int, message: str, response: str, reason: str, fault_code: str | None = None, fault_subcode: str | None = None, wsman_fault_code: int | None = None, wmierror_code: int | None = None, ) -> None: self.code = code self.response = response self.fault_code = fault_code self.fault_subcode = fault_subcode self.reason = reason self.wsman_fault_code = wsman_fault_code self.wmierror_code = wmierror_code # Using the dict repr is for backwards compatibility. fault_data = { "transport_message": message, "http_status_code": code, } if wsman_fault_code is not None: fault_data["wsmanfault_code"] = wsman_fault_code if fault_code is not None: fault_data["fault_code"] = fault_code if fault_subcode is not None: fault_data["fault_subcode"] = fault_subcode super().__init__("{0} (extended fault data: {1})".format(reason, fault_data)) class WinRMTransportError(Exception): """WinRM errors specific to transport-level problems (unexpected HTTP error codes, etc)""" @property def protocol(self) -> str: return self.args[0] @property def code(self) -> int: return self.args[1] @property def message(self) -> str: return "Bad HTTP response returned from server. Code {0}".format(self.code) @property def response_text(self) -> str: return self.args[2] def __str__(self) -> str: return self.message class WinRMOperationTimeoutError(Exception): """ Raised when a WinRM-level operation timeout (not a connection-level timeout) has occurred. This is considered a normal error that should be retried transparently by the client when waiting for output from a long-running process. """ code = 500 class AuthenticationError(WinRMError): """Authorization Error""" code = 401 class BasicAuthDisabledError(AuthenticationError): message = "WinRM/HTTP Basic authentication is not enabled on remote host" class InvalidCredentialsError(AuthenticationError): pass pywinrm-0.5.0/winrm/protocol.py000066400000000000000000000654071464554417700166250ustar00rootroot00000000000000"""Contains client side logic of WinRM SOAP protocol implementation""" from __future__ import annotations import base64 import collections.abc import typing as t import uuid import xml.etree.ElementTree as ET import xmltodict from winrm.exceptions import ( WinRMError, WinRMOperationTimeoutError, WinRMTransportError, WSManFaultError, ) from winrm.transport import Transport xmlns = { "soapenv": "http://www.w3.org/2003/05/soap-envelope", "soapaddr": "http://schemas.xmlsoap.org/ws/2004/08/addressing", "wsmanfault": "http://schemas.microsoft.com/wbem/wsman/1/wsmanfault", "wmierror": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/MSFT_WmiError", } class Protocol(object): """This is the main class that does the SOAP request/response logic. There are a few helper classes, but pretty much everything comes through here first. """ DEFAULT_READ_TIMEOUT_SEC = 30 DEFAULT_OPERATION_TIMEOUT_SEC = 20 DEFAULT_MAX_ENV_SIZE = 153600 DEFAULT_LOCALE = "en-US" def __init__( self, endpoint: str, transport: t.Literal["auto", "basic", "certificate", "ntlm", "kerberos", "credssp", "plaintext", "ssl"] = "plaintext", username: str | None = None, password: str | None = None, realm: None = None, service: str = "HTTP", keytab: None = None, ca_trust_path: t.Literal["legacy_requests"] | str = "legacy_requests", cert_pem: str | None = None, cert_key_pem: str | None = None, server_cert_validation: t.Literal["validate", "ignore"] | None = "validate", kerberos_delegation: bool = False, read_timeout_sec: str | int = DEFAULT_READ_TIMEOUT_SEC, operation_timeout_sec: str | int = DEFAULT_OPERATION_TIMEOUT_SEC, kerberos_hostname_override: str | None = None, message_encryption: t.Literal["auto", "always", "never"] = "auto", credssp_disable_tlsv1_2: bool = False, send_cbt: bool = True, proxy: t.Literal["legacy_requests"] | str | None = "legacy_requests", ): """ @param string endpoint: the WinRM webservice endpoint @param string transport: transport type, one of 'plaintext' (default), 'kerberos', 'ssl', 'ntlm', 'credssp' # NOQA @param string username: username @param string password: password @param string realm: unused @param string service: the service name, default is HTTP @param string keytab: unused @param string ca_trust_path: Certification Authority trust path. If server_cert_validation is set to 'validate': 'legacy_requests'(default) to use environment variables, None to explicitly disallow any additional CA trust path Any other value will be considered the CA trust path to use. @param string cert_pem: client authentication certificate file path in PEM format # NOQA @param string cert_key_pem: client authentication certificate key file path in PEM format # NOQA @param string server_cert_validation: whether server certificate should be validated on Python versions that support it; one of 'validate' (default), 'ignore' #NOQA @param bool kerberos_delegation: if True, TGT is sent to target server to allow multiple hops # NOQA @param int read_timeout_sec: maximum seconds to wait before an HTTP connect/read times out (default 30). This value should be slightly higher than operation_timeout_sec, as the server can block *at least* that long. # NOQA @param int operation_timeout_sec: maximum allowed time in seconds for any single wsman HTTP operation (default 20). Note that operation timeouts while receiving output (the only wsman operation that should take any significant time, and where these timeouts are expected) will be silently retried indefinitely. # NOQA @param string kerberos_hostname_override: the hostname to use for the kerberos exchange (defaults to the hostname in the endpoint URL) @param bool message_encryption_enabled: Will encrypt the WinRM messages if set to True and the transport auth supports message encryption (Default True). @param string proxy: Specify a proxy for the WinRM connection to use. 'legacy_requests'(default) to use environment variables, None to disable proxies completely or the proxy URL itself. """ try: read_timeout_sec = int(read_timeout_sec) except ValueError as ve: raise ValueError("failed to parse read_timeout_sec as int: %s" % str(ve)) try: operation_timeout_sec = int(operation_timeout_sec) except ValueError as ve: raise ValueError("failed to parse operation_timeout_sec as int: %s" % str(ve)) if operation_timeout_sec >= read_timeout_sec or operation_timeout_sec < 1: raise WinRMError("read_timeout_sec must exceed operation_timeout_sec, and both must be non-zero") self.read_timeout_sec = read_timeout_sec self.operation_timeout_sec = operation_timeout_sec self.max_env_sz = Protocol.DEFAULT_MAX_ENV_SIZE self.locale = Protocol.DEFAULT_LOCALE self.transport = Transport( endpoint=endpoint, username=username, password=password, realm=realm, service=service, keytab=keytab, ca_trust_path=ca_trust_path, cert_pem=cert_pem, cert_key_pem=cert_key_pem, read_timeout_sec=self.read_timeout_sec, server_cert_validation=server_cert_validation, kerberos_delegation=kerberos_delegation, kerberos_hostname_override=kerberos_hostname_override, auth_method=transport, message_encryption=message_encryption, credssp_disable_tlsv1_2=credssp_disable_tlsv1_2, send_cbt=send_cbt, proxy=proxy, ) self.username = username self.password = password self.service = service self.keytab = keytab self.ca_trust_path = ca_trust_path self.server_cert_validation = server_cert_validation self.kerberos_delegation = kerberos_delegation self.kerberos_hostname_override = kerberos_hostname_override self.credssp_disable_tlsv1_2 = credssp_disable_tlsv1_2 def open_shell( self, i_stream: str = "stdin", o_stream: str = "stdout stderr", working_directory: str | None = None, env_vars: dict[str, str] | None = None, noprofile: bool = False, codepage: int = 437, lifetime: None = None, idle_timeout: str | int | None = None, ) -> str: """ Create a Shell on the destination host @param string i_stream: Which input stream to open. Leave this alone unless you know what you're doing (default: stdin) @param string o_stream: Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr) @param string working_directory: the directory to create the shell in @param dict env_vars: environment variables to set for the shell. For instance: {'PATH': '%PATH%;c:/Program Files (x86)/Git/bin/', 'CYGWIN': 'nontsec codepage:utf8'} @returns The ShellId from the SOAP response. This is our open shell instance on the remote machine. @rtype string """ req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.xmlsoap.org/ws/2004/09/transfer/Create", ) } header = req["env:Envelope"]["env:Header"] header["w:OptionSet"] = { "w:Option": [ {"@Name": "WINRS_NOPROFILE", "#text": str(noprofile).upper()}, # TODO remove str call {"@Name": "WINRS_CODEPAGE", "#text": str(codepage)}, # TODO remove str call ] } shell = req["env:Envelope"].setdefault("env:Body", {}).setdefault("rsp:Shell", {}) shell["rsp:InputStreams"] = i_stream shell["rsp:OutputStreams"] = o_stream if working_directory: # TODO ensure that rsp:WorkingDirectory should be nested within rsp:Shell # NOQA shell["rsp:WorkingDirectory"] = working_directory # TODO check Lifetime param: http://msdn.microsoft.com/en-us/library/cc251546(v=PROT.13).aspx # NOQA # if lifetime: # shell['rsp:Lifetime'] = iso8601_duration.sec_to_dur(lifetime) # TODO make it so the input is given in milliseconds and converted to xs:duration # NOQA if idle_timeout: shell["rsp:IdleTimeOut"] = idle_timeout if env_vars: # the rsp:Variable tag needs to be list of variables so that all # environment variables in the env_vars dict are set on the shell env = shell.setdefault("rsp:Environment", {}).setdefault("rsp:Variable", []) for key, value in env_vars.items(): env.append({"@Name": key, "#text": value}) res = self.send_message(xmltodict.unparse(req)) # res = xmltodict.parse(res) # return res['s:Envelope']['s:Body']['x:ResourceCreated']['a:ReferenceParameters']['w:SelectorSet']['w:Selector']['#text'] root = ET.fromstring(res) return t.cast(str, next(node for node in root.findall(".//*") if node.get("Name") == "ShellId").text) # Helper method for building SOAP Header def build_wsman_header( self, action: str, resource_uri: str, shell_id: str | None = None, message_id: str | uuid.UUID | None = None, ) -> dict[str, t.Any]: """ Builds the standard header needed for WSMan operations. The return value is a dictionary that can be used by xmltodict to generate the WSMan envelope when sending custom requests. @param string action: The WSMan action to perform. @param string resource_uri: The WSMan resource URI the request is for. @param string shell_id: The optional shell UUID the request is for. @param string message_id: A unique message UUID, if unset a random UUID is used. @returns The WSMan header as a dictionary. @rtype dict[str, t.Any] """ if not message_id: message_id = uuid.uuid4() header: dict[str, t.Any] = { "@xmlns:xsd": "http://www.w3.org/2001/XMLSchema", "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "@xmlns:env": xmlns["soapenv"], "@xmlns:a": xmlns["soapaddr"], "@xmlns:b": "http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd", "@xmlns:n": "http://schemas.xmlsoap.org/ws/2004/09/enumeration", "@xmlns:x": "http://schemas.xmlsoap.org/ws/2004/09/transfer", "@xmlns:w": "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd", "@xmlns:p": "http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd", "@xmlns:rsp": "http://schemas.microsoft.com/wbem/wsman/1/windows/shell", # NOQA "@xmlns:cfg": "http://schemas.microsoft.com/wbem/wsman/1/config", "env:Header": { "a:To": "http://windows-host:5985/wsman", "a:ReplyTo": {"a:Address": {"@mustUnderstand": "true", "#text": "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"}}, # NOQA "w:MaxEnvelopeSize": {"@mustUnderstand": "true", "#text": "153600"}, "a:MessageID": "uuid:{0}".format(message_id), "w:Locale": {"@mustUnderstand": "false", "@xml:lang": "en-US"}, "p:DataLocale": {"@mustUnderstand": "false", "@xml:lang": "en-US"}, # TODO: research this a bit http://msdn.microsoft.com/en-us/library/cc251561(v=PROT.13).aspx # NOQA # 'cfg:MaxTimeoutms': 600 # Operation timeout in ISO8601 format, see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx # NOQA "w:OperationTimeout": "PT{0}S".format(int(self.operation_timeout_sec)), "w:ResourceURI": {"@mustUnderstand": "true", "#text": resource_uri}, "a:Action": {"@mustUnderstand": "true", "#text": action}, }, } if shell_id: header["env:Header"]["w:SelectorSet"] = {"w:Selector": {"@Name": "ShellId", "#text": shell_id}} return header # For backwards compatibility with Ansible. This should not be removed # until all supported releases of Ansible has been updated to use the new # method. _get_soap_header = build_wsman_header def send_message(self, message: str) -> bytes: # TODO add message_id vs relates_to checking # TODO port error handling code try: resp = self.transport.send_message(message) return resp except WinRMTransportError as ex: try: # if response is XML-parseable, it's probably a SOAP fault; extract the details root = ET.fromstring(ex.response_text) except Exception: # assume some other transport error; raise the original exception raise ex fault = root.find("soapenv:Body/soapenv:Fault", xmlns) if fault is None: raise wsmanfault_code_raw = fault.find("soapenv:Detail/wsmanfault:WSManFault[@Code]", xmlns) wsmanfault_code: int | None = None if wsmanfault_code_raw is not None: wsmanfault_code = int(wsmanfault_code_raw.attrib["Code"]) # convert receive timeout code to WinRMOperationTimeoutError if wsmanfault_code == 2150858793: # TODO: this fault code is specific to the Receive operation; convert all op timeouts? raise WinRMOperationTimeoutError() fault_code_raw = fault.find("soapenv:Code/soapenv:Value", xmlns) fault_code: str | None = None if fault_code_raw is not None and fault_code_raw.text: fault_code = fault_code_raw.text fault_subcode_raw = fault.find("soapenv:Code/soapenv:Subcode/soapenv:Value", xmlns) fault_subcode: str | None = None if fault_subcode_raw is not None and fault_subcode_raw.text: fault_subcode = fault_subcode_raw.text error_message_node = fault.find("soapenv:Reason/soapenv:Text", xmlns) reason: str | None = None if error_message_node is not None: reason = error_message_node.text wmi_error_code_raw = fault.find("soapenv:Detail/wmierror:MSFT_WmiError/wmierror:error_Code", xmlns) wmi_error_code: int | None = None if wmi_error_code_raw is not None and wmi_error_code_raw.text: wmi_error_code = int(wmi_error_code_raw.text) raise WSManFaultError( code=ex.code, message=ex.message, response=ex.response_text, reason=reason or "(no error message in fault)", fault_code=fault_code, fault_subcode=fault_subcode, wsman_fault_code=wsmanfault_code, wmierror_code=wmi_error_code, ) def close_shell(self, shell_id: str, close_session: bool = True) -> None: """ Close the shell @param string shell_id: The shell id on the remote machine. See #open_shell @param bool close_session: If we want to close the requests's session. Allows to completely close all TCP connections to the server. @returns This should have more error checking but it just returns true for now. @rtype bool """ try: message_id = uuid.uuid4() req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete", shell_id=shell_id, message_id=message_id, ) } # SOAP message requires empty env:Body req["env:Envelope"].setdefault("env:Body", {}) res = self.send_message(xmltodict.unparse(req)) root = ET.fromstring(res) relates_to = t.cast(str, next(node for node in root.findall(".//*") if node.tag.endswith("RelatesTo")).text) finally: # Close the transport if we are done with the shell. # This will ensure no lingering TCP connections are thrown back into a requests' connection pool. if close_session: self.transport.close_session() # TODO change assert into user-friendly exception assert uuid.UUID(relates_to.replace("uuid:", "")) == message_id def run_command( self, shell_id: str, command: str, arguments: collections.abc.Iterable[str | bytes] = (), console_mode_stdin: bool = True, skip_cmd_shell: bool = False, ) -> str: """ Run a command on a machine with an open shell @param string shell_id: The shell id on the remote machine. See #open_shell @param string command: The command to run on the remote machine @param iterable of string arguments: An array of arguments for this command @param bool console_mode_stdin: (default: True) @param bool skip_cmd_shell: (default: False) @return: The CommandId from the SOAP response. This is the ID we need to query in order to get output. @rtype string """ req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command", # NOQA shell_id=shell_id, ) } header = req["env:Envelope"]["env:Header"] header["w:OptionSet"] = { "w:Option": [ {"@Name": "WINRS_CONSOLEMODE_STDIN", "#text": str(console_mode_stdin).upper()}, {"@Name": "WINRS_SKIP_CMD_SHELL", "#text": str(skip_cmd_shell).upper()}, ] } cmd_line = req["env:Envelope"].setdefault("env:Body", {}).setdefault("rsp:CommandLine", {}) cmd_line["rsp:Command"] = {"#text": command} if arguments: unicode_args = [a if isinstance(a, str) else a.decode("utf-8") for a in arguments] cmd_line["rsp:Arguments"] = " ".join(unicode_args) res = self.send_message(xmltodict.unparse(req)) root = ET.fromstring(res) command_id = next(node for node in root.findall(".//*") if node.tag.endswith("CommandId")).text return t.cast(str, command_id) def cleanup_command(self, shell_id: str, command_id: str) -> None: """ Clean-up after a command. @see #run_command @param string shell_id: The shell id on the remote machine. See #open_shell @param string command_id: The command id on the remote machine. See #run_command @returns: This should have more error checking but it just returns true for now. @rtype bool """ message_id = uuid.uuid4() req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal", # NOQA shell_id=shell_id, message_id=message_id, ) } # Signal the Command references to terminate (close stdout/stderr) signal = req["env:Envelope"].setdefault("env:Body", {}).setdefault("rsp:Signal", {}) signal["@CommandId"] = command_id signal["rsp:Code"] = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate" # NOQA res = self.send_message(xmltodict.unparse(req)) root = ET.fromstring(res) relates_to = t.cast(str, next(node for node in root.findall(".//*") if node.tag.endswith("RelatesTo")).text) # TODO change assert into user-friendly exception assert uuid.UUID(relates_to.replace("uuid:", "")) == message_id def send_command_input(self, shell_id: str, command_id: str, stdin_input: str | bytes, end: bool = False) -> None: """ Send input to the given shell and command. @param string shell_id: The shell id on the remote machine. See #open_shell @param string command_id: The command id on the remote machine. See #run_command @param string stdin_input: The input unicode string or byte string to be sent. @param bool end: Boolean value which will close the stdin stream. If end=True then the stdin pipe to the remotely running process will be closed causing the next read by the remote process to stdin to return a EndOfFile error; the behavior of each process when this error is encountered is defined by the process, but most processes ( like CMD and powershell for instance) will just exit. Setting this value to 'True' means that no more input will be able to be sent to the process and attempting to do so should result in an error. @return: None """ if isinstance(stdin_input, str): stdin_input = stdin_input.encode("437") req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send", # NOQA shell_id=shell_id, ) } stdin_envelope = req["env:Envelope"].setdefault("env:Body", {}).setdefault("rsp:Send", {}).setdefault("rsp:Stream", {}) stdin_envelope["@CommandId"] = command_id stdin_envelope["@Name"] = "stdin" if end: stdin_envelope["@End"] = "true" else: stdin_envelope["@End"] = "false" stdin_envelope["@xmlns:rsp"] = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell" stdin_envelope["#text"] = base64.b64encode(stdin_input) self.send_message(xmltodict.unparse(req)) def get_command_output(self, shell_id: str, command_id: str) -> tuple[bytes, bytes, int]: """ Get the Output of the given shell and command. This will wait until the command is finished before returning the output. @param string shell_id: The shell id on the remote machine. See #open_shell @param string command_id: The command id on the remote machine. See #run_command @return tuple[bytes, bytes, int]: Returns a tuple with the stdout, stderr, and the return code of the command. The stdout and stderr value is a byte string and not a normal string. """ stdout_buffer, stderr_buffer = [], [] command_done = False while not command_done: try: stdout, stderr, return_code, command_done = self.get_command_output_raw(shell_id, command_id) stdout_buffer.append(stdout) stderr_buffer.append(stderr) except WinRMOperationTimeoutError: # this is an expected error when waiting for a long-running process, just silently retry pass return b"".join(stdout_buffer), b"".join(stderr_buffer), return_code def get_command_output_raw(self, shell_id: str, command_id: str) -> tuple[bytes, bytes, int, bool]: """ Get the next available output of the given shell and command. This will wait until the issued WSMan Receive action returns data or times out with WinRMOperationTimeoutError. @param string shell_id: The shell id on the remote machine. See #open_shell @param string command_id: The command id on the remote machine. See #run_command @return tuple[bytes, bytes, int, bool]: Returns a tuple with the stdout, stderr, the return code of the command, and whether it has finished or not. The stdout and stderr value is a byte string and not a normal string. @raises WinRMOperationTimeoutError: Raised when there has been no output from the command """ req = { "env:Envelope": self.build_wsman_header( resource_uri="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", # NOQA action="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive", # NOQA shell_id=shell_id, ) } stream = req["env:Envelope"].setdefault("env:Body", {}).setdefault("rsp:Receive", {}).setdefault("rsp:DesiredStream", {}) stream["@CommandId"] = command_id stream["#text"] = "stdout stderr" res = self.send_message(xmltodict.unparse(req)) root = ET.fromstring(res) stream_nodes = [node for node in root.findall(".//*") if node.tag.endswith("Stream")] stdout = [] stderr = [] return_code = -1 for stream_node in stream_nodes: if not stream_node.text: continue if stream_node.attrib["Name"] == "stdout": stdout.append(base64.b64decode(stream_node.text.encode("ascii"))) elif stream_node.attrib["Name"] == "stderr": stderr.append(base64.b64decode(stream_node.text.encode("ascii"))) # We may need to get additional output if the stream has not finished. # The CommandState will change from Running to Done like so: # @example # from... # # to... # # 0 # command_done = len([node for node in root.findall(".//*") if node.get("State", "").endswith("CommandState/Done")]) == 1 if command_done: return_code = int(next(node for node in root.findall(".//*") if node.tag.endswith("ExitCode")).text or -1) return b"".join(stdout), b"".join(stderr), return_code, command_done # While it was meant to be private it has been treated as a public API. # This might be removed in a future version but for now keep it as an # alias for the now public API method 'get_command_output_raw'. # https://github.com/search?q=_raw_get_command_output+language%3APython&type=code&l=Python _raw_get_command_output = get_command_output_raw pywinrm-0.5.0/winrm/py.typed000066400000000000000000000000001464554417700160630ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/000077500000000000000000000000001464554417700155405ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/__init__.py000066400000000000000000000000001464554417700176370ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/conftest.py000066400000000000000000001162101464554417700177400ustar00rootroot00000000000000# flake8: noqa import os import uuid import xmltodict from mock import patch from pytest import fixture, skip open_shell_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.xmlsoap.org/ws/2004/09/transfer/Create FALSE 437 stdin stdout stderr """ open_shell_response = """\ http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 http://windows-host:5985/wsman http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd 11111111-1111-1111-1111-111111111113 """ close_shell_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete 11111111-1111-1111-1111-111111111113 """ close_shell_response = """\ http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 """ run_cmd_with_args_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command 11111111-1111-1111-1111-111111111113 TRUE FALSE ipconfig /all """ run_cmd_wo_args_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command 11111111-1111-1111-1111-111111111113 TRUE FALSE hostname """ run_cmd_ps_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 11111111-1111-1111-1111-1111111111%s4 """ # PS request is Write-Error "Error" run_ps_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command 11111111-1111-1111-1111-111111111113 TRUE FALSE powershell -encodedcommand VwByAGkAdABlAC0ARQByAHIAbwByACAAIgBFAHIAcgBvAHIAIgA= """ cleanup_cmd_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal 11111111-1111-1111-1111-111111111113 http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate """ cleanup_cmd_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/SignalResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 """ get_cmd_ps_output_request = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive 11111111-1111-1111-1111-111111111113 stdout stderr """ get_cmd_output_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 DQpXaW5kb3dzIElQIENvbmZpZ3VyYXRpb24NCg0K ICAgSG9zdCBOYW1lIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogV0lORE9XUy1IT1NUCiAgIFByaW1hcnkgRG5zIFN1ZmZpeCAgLiAuIC4gLiAuIC4gLiA6IAogICBOb2RlIFR5cGUgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBIeWJyaWQKICAgSVAgUm91dGluZyBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgV0lOUyBQcm94eSBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIDogTm8KCkV0aGVybmV0IGFkYXB0ZXIgTG9jYWwgQXJlYSBDb25uZWN0aW9uOgoKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEludGVsKFIpIDgyNTY3Vi0yIEdpZ2FiaXQgTmV0d29yayBDb25uZWN0aW9uCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IEY4LTBGLTQxLTE2LTg4LUU4CiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBMaW5rLWxvY2FsIElQdjYgQWRkcmVzcyAuIC4gLiAuIC4gOiBmZTgwOjphOTkwOjM1ZTM6YTZhYjpmYzE1JTEwKFByZWZlcnJlZCkgCiAgIElQdjQgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDE3My4xODUuMTUzLjkzKFByZWZlcnJlZCkgCiAgIFN1Ym5ldCBNYXNrIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDI1NS4yNTUuMjU1LjI0OAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAxNzMuMTg1LjE1My44OQogICBESENQdjYgSUFJRCAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyNTExMzc4NTcKICAgREhDUHY2IENsaWVudCBEVUlELiAuIC4gLiAuIC4gLiAuIDogMDAtMDEtMDAtMDEtMTYtM0ItM0YtQzItRjgtMEYtNDEtMTYtODgtRTgKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRW5hYmxlZAoKRXRoZXJuZXQgYWRhcHRlciBMb2NhbCBBcmVhIENvbm5lY3Rpb24qIDk6CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IEp1bmlwZXIgTmV0d29yayBDb25uZWN0IFZpcnR1YWwgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC1GRi1BMC04My00OC0wNAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBZZXMKICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCgpUdW5uZWwgYWRhcHRlciBpc2F0YXAue0FBNDI2QjM3LTM2OTUtNEVCOC05OTBGLTRDRkFDODQ1RkQxN306CgogICBNZWRpYSBTdGF0ZSAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNZWRpYSBkaXNjb25uZWN0ZWQKICAgQ29ubmVjdGlvbi1zcGVjaWZpYyBETlMgU3VmZml4ICAuIDogCiAgIERlc2NyaXB0aW9uIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1pY3Jvc29mdCBJU0FUQVAgQWRhcHRlcgogICBQaHlzaWNhbCBBZGRyZXNzLiAuIC4gLiAuIC4gLiAuIC4gOiAwMC0wMC0wMC0wMC0wMC0wMC0wMC1FMAogICBESENQIEVuYWJsZWQuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBObwogICBBdXRvY29uZmlndXJhdGlvbiBFbmFibGVkIC4gLiAuIC4gOiBZZXMKClR1bm5lbCBhZGFwdGVyIFRlcmVkbyBUdW5uZWxpbmcgUHNldWRvLUludGVyZmFjZToKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBUZXJlZG8gVHVubmVsaW5nIFBzZXVkby1JbnRlcmZhY2UKICAgUGh5c2ljYWwgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIDogMDAtMDAtMDAtMDAtMDAtMDAtMDAtRTAKICAgREhDUCBFbmFibGVkLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTm8KICAgQXV0b2NvbmZpZ3VyYXRpb24gRW5hYmxlZCAuIC4gLiAuIDogWWVzCiAgIElQdjYgQWRkcmVzcy4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDE6MDo5ZDM4Ojk1M2M6MmNlZjo3ZmM6NTI0Njo2NmEyKFByZWZlcnJlZCkgCiAgIExpbmstbG9jYWwgSVB2NiBBZGRyZXNzIC4gLiAuIC4gLiA6IGZlODA6OjJjZWY6N2ZjOjUyNDY6NjZhMiUxMyhQcmVmZXJyZWQpIAogICBEZWZhdWx0IEdhdGV3YXkgLiAuIC4gLiAuIC4gLiAuIC4gOiAKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIDZUTzQgQWRhcHRlcjoKCiAgIENvbm5lY3Rpb24tc3BlY2lmaWMgRE5TIFN1ZmZpeCAgLiA6IAogICBEZXNjcmlwdGlvbiAuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiBNaWNyb3NvZnQgNnRvNCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwogICBJUHY2IEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiAuIC4gOiAyMDAyOmFkYjk6OTk1ZDo6YWRiOTo5OTVkKFByZWZlcnJlZCkgCiAgIERlZmF1bHQgR2F0ZXdheSAuIC4gLiAuIC4gLiAuIC4gLiA6IDIwMDI6YzA1ODo2MzAxOjpjMDU4OjYzMDEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjAwMjpjMDU4OjYzMDE6OjEKICAgRE5TIFNlcnZlcnMgLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogMjA3LjkxLjUuMzIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMjA4LjY3LjIyMi4yMjIKICAgTmV0QklPUyBvdmVyIFRjcGlwLiAuIC4gLiAuIC4gLiAuIDogRGlzYWJsZWQKClR1bm5lbCBhZGFwdGVyIGlzYXRhcC57QkExNjBGQzUtNzAyOC00QjFGLUEwNEItMUFDODAyQjBGRjVBfToKCiAgIE1lZGlhIFN0YXRlIC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE1lZGlhIGRpc2Nvbm5lY3RlZAogICBDb25uZWN0aW9uLXNwZWNpZmljIEROUyBTdWZmaXggIC4gOiAKICAgRGVzY3JpcHRpb24gLiAuIC4gLiAuIC4gLiAuIC4gLiAuIDogTWljcm9zb2Z0IElTQVRBUCBBZGFwdGVyICMyCiAgIFBoeXNpY2FsIEFkZHJlc3MuIC4gLiAuIC4gLiAuIC4gLiA6IDAwLTAwLTAwLTAwLTAwLTAwLTAwLUUwCiAgIERIQ1AgRW5hYmxlZC4gLiAuIC4gLiAuIC4gLiAuIC4gLiA6IE5vCiAgIEF1dG9jb25maWd1cmF0aW9uIEVuYWJsZWQgLiAuIC4gLiA6IFllcwo= 0 """ get_ps_output_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse uuid:11111111-1111-1111-1111-111111111112 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 IzwgQ0xJWE1MDQo= PE9ianMgVmVyc2lvbj0iMS4xLjAuMSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vcG93ZXJzaGVsbC8yMDA0LzA0Ij48UyBTPSJFcnJvciI+V3JpdGUtRXJyb3IgIkVycm9yIiA6IEVycm9yX3gwMDBEX194MDAwQV88L1M+PFMgUz0iRXJyb3IiPiAgICArIENhdGVnb3J5SW5mbyAgICAgICAgICA6IE5vdFNwZWNpZmllZDogKDopIFtXcml0ZS1FcnJvcl0sIFdyaXRlRXJyb3JFeGNlcCBfeDAwMERfX3gwMDBBXzwvUz48UyBTPSJFcnJvciI+ICAgdGlvbl94MDAwRF9feDAwMEFfPC9TPjxTIFM9IkVycm9yIj4gICAgKyBGdWxseVF1YWxpZmllZEVycm9ySWQgOiBNaWNyb3NvZnQuUG93ZXJTaGVsbC5Db21tYW5kcy5Xcml0ZUVycm9yRXhjZXB0aW8gX3gwMDBEX194MDAwQV88L1M+PFMgUz0iRXJyb3IiPiAgIG5feDAwMERfX3gwMDBBXzwvUz48UyBTPSJFcnJvciI+IF94MDAwRF9feDAwMEFfPC9TPjwvT2Jqcz4= 1 """ run_cmd_req_input = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command 11111111-1111-1111-1111-111111111113 TRUE FALSE cmd """ run_cmd_req_input_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandResponse uuid:11111111-1111-1111-1111-111111111114 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111112 11111111-1111-1111-1111-111111111111 """ run_cmd_send_input = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send 11111111-1111-1111-1111-111111111113 ZWNobyAiaGVsbG8gd29ybGQiICYmIGV4aXQNCg== """ run_cmd_send_input_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/SendResponse uuid:72371E37-E073-474B-B4BA-6559D8D94632 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:9c3de121-c3a4-452b-8f82-36b84e25b7fe """ run_cmd_send_input_get_output = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive 11111111-1111-1111-1111-111111111113 stdout stderr """ run_cmd_send_input_get_output_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse uuid:6468086A-377E-4BE3-AC71-1155F0F1D4E1 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:02f258b6-186f-4ac0-adc3-51550a131e64 TWljcm9zb2Z0IFdpbmRvd3MgW1ZlcnNpb24gMTAuMC4xNzc2My4xMDdd DQooYykgMjAxOCBNaWNyb3NvZnQgQ29ycG9yYXRpb24uIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQoNCkM6XFVzZXJzXHJ3ZWJlcj5lY2hvIGhlbGxvIHdvcmxkICYmIGV4aXQNCmhlbGxvIHdvcmxkIA0K 0 """ stdin_cmd_cleanup = """\ http://windows-host:5985/wsman http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 153600 uuid:11111111-1111-1111-1111-111111111111 PT20S http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal 11111111-1111-1111-1111-111111111113 http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate """ stdin_cmd_cleanup_response = """\ http://schemas.microsoft.com/wbem/wsman/1/windows/shell/SignalResponse uuid:8A875405-3494-4400-A988-B47A563922E7 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:11111111-1111-1111-1111-111111111111 """ def sort_dict(ordered_dict): items = sorted(ordered_dict.items(), key=lambda x: x[0]) ordered_dict.clear() for key, value in items: if isinstance(value, dict): sort_dict(value) ordered_dict[key] = value def xml_str_compare(first, second): first_dict = xmltodict.parse(first) second_dict = xmltodict.parse(second) sort_dict(first_dict) sort_dict(second_dict) return first_dict == second_dict class TransportStub(object): def send_message(self, message): if xml_str_compare(message, open_shell_request): return open_shell_response elif xml_str_compare(message, close_shell_request): return close_shell_response elif xml_str_compare(message, run_cmd_with_args_request) or xml_str_compare(message, run_cmd_wo_args_request): return run_cmd_ps_response % "1" elif xml_str_compare(message, run_ps_request): return run_cmd_ps_response % "2" elif xml_str_compare(message, cleanup_cmd_request % "1") or xml_str_compare(message, cleanup_cmd_request % "2"): return cleanup_cmd_response elif xml_str_compare(message, get_cmd_ps_output_request % "1"): return get_cmd_output_response elif xml_str_compare(message, get_cmd_ps_output_request % "2"): return get_ps_output_response elif xml_str_compare(message, run_cmd_req_input): return run_cmd_req_input_response elif xml_str_compare(message, run_cmd_send_input): return run_cmd_send_input_response elif xml_str_compare(message, run_cmd_send_input_get_output): return run_cmd_send_input_get_output_response elif xml_str_compare(message, stdin_cmd_cleanup): return stdin_cmd_cleanup_response else: raise Exception("Message was not expected\n\n%s" % message) def close_session(self): pass @fixture(scope="module") def protocol_fake(request): uuid4_patcher = patch("uuid.uuid4") uuid4_mock = uuid4_patcher.start() uuid4_mock.return_value = uuid.UUID("11111111-1111-1111-1111-111111111111") from winrm.protocol import Protocol protocol_fake = Protocol(endpoint="http://windows-host:5985/wsman", transport="plaintext", username="john.smith", password="secret") protocol_fake.transport = TransportStub() def uuid4_patch_stop(): uuid4_patcher.stop() request.addfinalizer(uuid4_patch_stop) return protocol_fake @fixture(scope="module") def protocol_real(): endpoint = os.environ.get("WINRM_ENDPOINT", None) transport = os.environ.get("WINRM_TRANSPORT", None) username = os.environ.get("WINRM_USERNAME", None) password = os.environ.get("WINRM_PASSWORD", None) if endpoint: settings = dict(endpoint=endpoint, operation_timeout_sec=5, read_timeout_sec=7) if transport: settings["transport"] = transport if username: settings["username"] = username if password: settings["password"] = password from winrm.protocol import Protocol protocol = Protocol(**settings) return protocol else: skip("WINRM_ENDPOINT environment variable was not set. Integration tests will be skipped") pywinrm-0.5.0/winrm/tests/sample_script.ps1000066400000000000000000000000001464554417700210200ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/test_cmd.py000066400000000000000000000000001464554417700177020ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/test_encryption.py000066400000000000000000000262011464554417700213440ustar00rootroot00000000000000import base64 import struct import pytest from winrm.encryption import Encryption from winrm.exceptions import WinRMError def test_init_with_invalid_protocol(): with pytest.raises(WinRMError) as excinfo: Encryption(None, "invalid_protocol") assert "Encryption for protocol 'invalid_protocol' not supported in pywinrm" in str(excinfo.value) def test_encrypt_message(): test_session = SessionTest() test_message = b"unencrypted message" test_endpoint = b"endpoint" encryption = Encryption(test_session, "ntlm") actual = encryption.prepare_encrypted_request(test_session, test_endpoint, test_message) expected_encrypted_message = b"dW5lbmNyeXB0ZWQgbWVzc2FnZQ==" expected_signature = b"1234" signature_length = struct.pack(" None: raise exc def test_wsman_fault_must_understand(protocol_fake): xml_text = r""" http://schemas.xmlsoap.org/ws/2004/08/addressing/fault uuid:4DB571F9-F8DE-48FD-872C-2AF08D996249 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:eaa98952-3188-458f-b265-b03ace115f20 s:MustUnderstand Test reason. """ protocol_fake.transport.send_message = lambda m: raise_exc(WinRMTransportError("http", 500, xml_text)) with pytest.raises(WSManFaultError, match="Test reason\\.") as exc: protocol_fake.open_shell() assert isinstance(exc.value, WSManFaultError) assert exc.value.code == 500 assert exc.value.response == xml_text assert exc.value.fault_code == "s:MustUnderstand" assert exc.value.fault_subcode is None assert exc.value.wsman_fault_code is None assert exc.value.wmierror_code is None def test_wsman_fault_no_reason(protocol_fake): xml_text = r""" http://schemas.xmlsoap.org/ws/2004/08/addressing/fault uuid:4DB571F9-F8DE-48FD-872C-2AF08D996249 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:eaa98952-3188-458f-b265-b03ace115f20 s:Unknown """ protocol_fake.transport.send_message = lambda m: raise_exc(WinRMTransportError("http", 501, xml_text)) with pytest.raises(WSManFaultError, match="no error message in fault") as exc: protocol_fake.open_shell() assert isinstance(exc.value, WSManFaultError) assert exc.value.code == 501 assert exc.value.response == xml_text assert exc.value.fault_code == "s:Unknown" assert exc.value.fault_subcode is None assert exc.value.wsman_fault_code is None assert exc.value.wmierror_code is None def test_wsman_fault_known_fault(protocol_fake): xml_text = r""" http://schemas.dmtf.org/wbem/wsman/1/wsman/fault uuid:D7C4A9B1-9A18-4048-B346-248D62A6078D http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid:7340FE92-C302-42E5-A337-1918908654F8 s:Receiver w:TimedOut The WS-Management service cannot complete the operation within the time specified in OperationTimeout. The WS-Management service cannot complete the operation within the time specified in OperationTimeout. """ protocol_fake.transport.send_message = lambda m: raise_exc(WinRMTransportError("http", 500, xml_text)) with pytest.raises(WinRMOperationTimeoutError): protocol_fake.open_shell() def test_wsman_fault_with_wsmanfault(protocol_fake): xml_text = r""" http://schemas.dmtf.org/wbem/wsman/1/wsman/fault uuid:EE71C444-1658-4B3F-916D-54CE43B68BC9 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous uuid.761ca906-0bf0-41bb-a9d9-4cbbca986aeb s:Sender w:SchemaValidationError Reason text. Detail message. """ protocol_fake.transport.send_message = lambda m: raise_exc(WinRMTransportError("http", 500, xml_text)) with pytest.raises(WSManFaultError, match="Reason text\\.") as exc: protocol_fake.open_shell() assert isinstance(exc.value, WSManFaultError) assert exc.value.code == 500 assert exc.value.response == xml_text assert exc.value.fault_code == "s:Sender" assert exc.value.fault_subcode == "w:SchemaValidationError" assert exc.value.wsman_fault_code == 0x80338041 assert exc.value.wmierror_code is None def test_wsman_fault_wmi_error_detail(protocol_fake): xml_text = r""" http://schemas.dmtf.org/wbem/wsman/1/wsman/fault uuid:A832545B-9F5C-46AA-BB6A-5E4270D5E530 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous s:Receiver w:InternalError Reason text. 27 0 0 WMI Message. HRESULT 0x803381a6 0 0 30 2150859174 HRESULT Windows Error message. """ protocol_fake.transport.send_message = lambda m: raise_exc(WinRMTransportError("http", 500, xml_text)) with pytest.raises(WSManFaultError, match="Reason text\\.") as exc: protocol_fake.open_shell() assert isinstance(exc.value, WSManFaultError) assert exc.value.code == 500 assert exc.value.response == xml_text assert exc.value.fault_code == "s:Receiver" assert exc.value.fault_subcode == "w:InternalError" assert exc.value.wsman_fault_code is None assert exc.value.wmierror_code == 0x803381A6 pywinrm-0.5.0/winrm/tests/test_integration_protocol.py000066400000000000000000000065751464554417700234320ustar00rootroot00000000000000# coding=utf-8 import re import pytest xfail = pytest.mark.xfail def test_unicode_roundtrip(protocol_real): shell_id = protocol_real.open_shell(codepage=65001) command_id = protocol_real.run_command(shell_id, "PowerShell", arguments=["-Command", "Write-Host", "こんにちは"]) try: std_out, std_err, status_code = protocol_real.get_command_output(shell_id, command_id) assert status_code == 0 assert len(std_err) == 0 # std_out will be returned as UTF-8, but PEP8 won't let us store a # UTF-8 string literal, so we'll convert it on the fly assert std_out == ("こんにちは\n".encode("utf-8")) finally: protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) def test_open_shell_and_close_shell(protocol_real): shell_id = protocol_real.open_shell() assert re.match(r"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", shell_id) protocol_real.close_shell(shell_id) def test_run_command_with_arguments_and_cleanup_command(protocol_real): shell_id = protocol_real.open_shell() command_id = protocol_real.run_command(shell_id, "ipconfig", ["/all"]) assert re.match(r"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", command_id) protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) def test_run_command_without_arguments_and_cleanup_command(protocol_real): shell_id = protocol_real.open_shell() command_id = protocol_real.run_command(shell_id, "hostname") assert re.match(r"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", command_id) protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) def test_run_command_with_env(protocol_real): shell_id = protocol_real.open_shell(env_vars=dict(TESTENV1="hi mom", TESTENV2="another var")) command_id = protocol_real.run_command(shell_id, "echo", ["%TESTENV1%", "%TESTENV2%"]) std_out, std_err, status_code = protocol_real.get_command_output(shell_id, command_id) assert re.search(b"hi mom another var", std_out) protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) def test_get_command_output(protocol_real): shell_id = protocol_real.open_shell() command_id = protocol_real.run_command(shell_id, "ipconfig", ["/all"]) std_out, std_err, status_code = protocol_real.get_command_output(shell_id, command_id) assert status_code == 0 assert b"Windows IP Configuration" in std_out assert len(std_err) == 0 protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) def test_run_command_taking_more_than_operation_timeout_sec(protocol_real): shell_id = protocol_real.open_shell() command_id = protocol_real.run_command(shell_id, "PowerShell -Command Start-Sleep -s {0}".format(protocol_real.operation_timeout_sec * 2)) assert re.match(r"^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$", command_id) std_out, std_err, status_code = protocol_real.get_command_output(shell_id, command_id) assert status_code == 0 assert len(std_err) == 0 protocol_real.cleanup_command(shell_id, command_id) protocol_real.close_shell(shell_id) @xfail() def test_set_timeout(protocol_real): raise NotImplementedError() @xfail() def test_set_max_env_size(protocol_real): raise NotImplementedError() @xfail() def test_set_locale(protocol_real): raise NotImplementedError() pywinrm-0.5.0/winrm/tests/test_integration_session.py000066400000000000000000000001501464554417700232330ustar00rootroot00000000000000import pytest xfail = pytest.mark.xfail @xfail() def test_run_cmd(): raise NotImplementedError() pywinrm-0.5.0/winrm/tests/test_nori_type_casting.py000066400000000000000000000000001464554417700226570ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/test_powershell.py000066400000000000000000000000001464554417700213230ustar00rootroot00000000000000pywinrm-0.5.0/winrm/tests/test_protocol.py000066400000000000000000000077071464554417700210250ustar00rootroot00000000000000import pytest from winrm.protocol import Protocol @pytest.mark.parametrize("func_name", ["build_wsman_header", "_get_soap_header"]) def test_build_wsman_header(func_name, protocol_fake): func = getattr(protocol_fake, func_name) actual = func("my action", "resource uri", "shell id", "message id") assert actual["env:Header"]["a:Action"]["#text"] == "my action" assert actual["env:Header"]["w:ResourceURI"]["#text"] == "resource uri" assert actual["env:Header"]["a:MessageID"] == "uuid:message id" assert actual["env:Header"]["w:SelectorSet"]["w:Selector"]["#text"] == "shell id" def test_open_shell_and_close_shell(protocol_fake): shell_id = protocol_fake.open_shell() assert shell_id == "11111111-1111-1111-1111-111111111113" protocol_fake.close_shell(shell_id, close_session=True) def test_run_command_with_arguments_and_cleanup_command(protocol_fake): shell_id = protocol_fake.open_shell() command_id = protocol_fake.run_command(shell_id, "ipconfig", ["/all"]) assert command_id == "11111111-1111-1111-1111-111111111114" protocol_fake.cleanup_command(shell_id, command_id) protocol_fake.close_shell(shell_id) def test_run_command_without_arguments_and_cleanup_command(protocol_fake): shell_id = protocol_fake.open_shell() command_id = protocol_fake.run_command(shell_id, "hostname") assert command_id == "11111111-1111-1111-1111-111111111114" protocol_fake.cleanup_command(shell_id, command_id) protocol_fake.close_shell(shell_id) def test_get_command_output(protocol_fake): shell_id = protocol_fake.open_shell() command_id = protocol_fake.run_command(shell_id, "ipconfig", ["/all"]) std_out, std_err, status_code = protocol_fake.get_command_output(shell_id, command_id) assert status_code == 0 assert b"Windows IP Configuration" in std_out assert len(std_err) == 0 protocol_fake.cleanup_command(shell_id, command_id) protocol_fake.close_shell(shell_id) @pytest.mark.parametrize("func_name", ["get_command_output_raw", "_raw_get_command_output"]) def test_get_command_output_raw(func_name, protocol_fake): func = getattr(protocol_fake, func_name) shell_id = protocol_fake.open_shell() command_id = protocol_fake.run_command(shell_id, "ipconfig", ["/all"]) std_out, std_err, status_code, done = func(shell_id, command_id) assert status_code == 0 assert b"Windows IP Configuration" in std_out assert len(std_err) == 0 assert done is True protocol_fake.cleanup_command(shell_id, command_id) protocol_fake.close_shell(shell_id) def test_send_command_input(protocol_fake): shell_id = protocol_fake.open_shell() command_id = protocol_fake.run_command(shell_id, "cmd") protocol_fake.send_command_input(shell_id, command_id, 'echo "hello world" && exit\r\n') std_out, std_err, status_code = protocol_fake.get_command_output(shell_id, command_id) assert status_code == 0 assert b"hello world" in std_out assert len(std_err) == 0 protocol_fake.cleanup_command(shell_id, command_id) protocol_fake.close_shell(shell_id) def test_set_timeout_as_sec(): protocol = Protocol("endpoint", username="username", password="password", read_timeout_sec="30", operation_timeout_sec="29") assert protocol.read_timeout_sec == 30 assert protocol.operation_timeout_sec == 29 def test_fail_set_read_timeout_as_sec(): with pytest.raises(ValueError) as exc: Protocol("endpoint", username="username", password="password", read_timeout_sec="30a", operation_timeout_sec="29") assert str(exc.value) == "failed to parse read_timeout_sec as int: " "invalid literal for int() with base 10: '30a'" def test_fail_set_operation_timeout_as_sec(): with pytest.raises(ValueError) as exc: Protocol("endpoint", username="username", password="password", read_timeout_sec=30, operation_timeout_sec="29a") assert str(exc.value) == "failed to parse operation_timeout_sec as int: " "invalid literal for int() with base 10: '29a'" pywinrm-0.5.0/winrm/tests/test_session.py000066400000000000000000000115741464554417700206440ustar00rootroot00000000000000import pytest from winrm import Session def test_run_cmd(protocol_fake): # TODO this test should cover __init__ method s = Session("windows-host", auth=("john.smith", "secret")) s.protocol = protocol_fake r = s.run_cmd("ipconfig", ["/all"]) assert r.status_code == 0 assert b"Windows IP Configuration" in r.std_out assert len(r.std_err) == 0 def test_run_ps_with_error(protocol_fake): # TODO this test should cover __init__ method s = Session("windows-host", auth=("john.smith", "secret")) s.protocol = protocol_fake r = s.run_ps('Write-Error "Error"') assert r.status_code == 1 assert b'Write-Error "Error"' in r.std_err assert len(r.std_out) == 0 def test_target_as_hostname(): s = Session("windows-host", auth=("john.smith", "secret")) assert s.url == "http://windows-host:5985/wsman" def test_target_as_hostname_then_port(): s = Session("windows-host:1111", auth=("john.smith", "secret")) assert s.url == "http://windows-host:1111/wsman" def test_target_as_schema_then_hostname(): s = Session("http://windows-host", auth=("john.smith", "secret")) assert s.url == "http://windows-host:5985/wsman" def test_target_as_schema_then_hostname_then_port(): s = Session("http://windows-host:1111", auth=("john.smith", "secret")) assert s.url == "http://windows-host:1111/wsman" def test_target_as_full_url(): s = Session("http://windows-host:1111/wsman", auth=("john.smith", "secret")) assert s.url == "http://windows-host:1111/wsman" def test_target_with_dots(): s = Session("windows-host.example.com", auth=("john.smith", "secret")) assert s.url == "http://windows-host.example.com:5985/wsman" def test_decode_clixml_error(): s = Session("windows-host.example.com", auth=("john.smith", "secret")) msg = b'#< CLIXML\r\nSystem.Management.Automation.PSCustomObjectSystem.Object1Preparing modules for first use.0-1-1Completed-1 1Preparing modules for first use.0-1-1Completed-1 fake : The term \'fake\' is not recognized as the name of a cmdlet, function, script file, or operable program. Check _x000D__x000A_the spelling of the name, or if a path was included, verify that the path is correct and try again._x000D__x000A_At line:1 char:1_x000D__x000A_+ fake cmdlet_x000D__x000A_+ ~~~~_x000D__x000A_ + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_ + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_ _x000D__x000A_' expected = b"fake : The term 'fake' is not recognized as the name of a cmdlet, function, script file, or operable program. Check \nthe spelling of the name, or if a path was included, verify that the path is correct and try again.\nAt line:1 char:1\n+ fake cmdlet\n+ ~~~~\n + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException\n + FullyQualifiedErrorId : CommandNotFoundException" actual = s._clean_error_msg(msg) assert actual == expected def test_decode_clixml_no_clixml(): s = Session("windows-host.example.com", auth=("john.smith", "secret")) msg = b"stderr line" expected = b"stderr line" actual = s._clean_error_msg(msg) assert actual == expected def test_decode_clixml_no_errors(): s = Session("windows-host.example.com", auth=("john.smith", "secret")) msg = b'#< CLIXML\r\nSystem.Management.Automation.PSCustomObjectSystem.Object1Preparing modules for first use.0-1-1Completed-1 1Preparing modules for first use.0-1-1Completed-1 ' expected = msg actual = s._clean_error_msg(msg) assert actual == expected def test_decode_clixml_invalid_xml(): s = Session("windows-host.example.com", auth=("john.smith", "secret")) msg = b"#< CLIXML\r\ndasf" with pytest.warns(UserWarning, match="There was a problem converting the Powershell error message"): actual = s._clean_error_msg(msg) assert actual == msg pywinrm-0.5.0/winrm/tests/test_transport.py000066400000000000000000000262021464554417700212070ustar00rootroot00000000000000# coding=utf-8 import os import unittest import mock from winrm import transport from winrm.exceptions import InvalidCredentialsError, WinRMError class TestTransport(unittest.TestCase): maxDiff = 2048 _old_env = None def setUp(self): super(TestTransport, self).setUp() self._old_env = {} os.environ.pop("REQUESTS_CA_BUNDLE", None) os.environ.pop("TRAVIS_APT_PROXY", None) os.environ.pop("CURL_CA_BUNDLE", None) os.environ.pop("HTTPS_PROXY", None) os.environ.pop("HTTP_PROXY", None) os.environ.pop("NO_PROXY", None) transport.DISPLAYED_PROXY_WARNING = False transport.DISPLAYED_CA_TRUST_WARNING = False def tearDown(self): super(TestTransport, self).tearDown() os.environ.pop("REQUESTS_CA_BUNDLE", None) os.environ.pop("TRAVIS_APT_PROXY", None) os.environ.pop("CURL_CA_BUNDLE", None) os.environ.pop("HTTPS_PROXY", None) os.environ.pop("HTTP_PROXY", None) os.environ.pop("NO_PROXY", None) def test_build_session_cert_validate_default(self): t_default = transport.Transport( endpoint="https://example.com", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual(True, t_default.session.verify) def test_build_session_cert_validate_default_env(self): os.environ["REQUESTS_CA_BUNDLE"] = "path_to_REQUESTS_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual("path_to_REQUESTS_CA_CERT", t_default.session.verify) def test_build_session_cert_validate_1(self): os.environ["REQUESTS_CA_BUNDLE"] = "path_to_REQUESTS_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual("path_to_REQUESTS_CA_CERT", t_default.session.verify) def test_build_session_cert_validate_2(self): os.environ["CURL_CA_BUNDLE"] = "path_to_CURL_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual("path_to_CURL_CA_CERT", t_default.session.verify) def test_build_session_cert_override_1(self): os.environ["REQUESTS_CA_BUNDLE"] = "path_to_REQUESTS_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ca_trust_path="overridepath", ) t_default.build_session() self.assertEqual("overridepath", t_default.session.verify) def test_build_session_cert_override_2(self): os.environ["CURL_CA_BUNDLE"] = "path_to_CURL_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ca_trust_path="overridepath", ) t_default.build_session() self.assertEqual("overridepath", t_default.session.verify) def test_build_session_cert_override_3(self): os.environ["CURL_CA_BUNDLE"] = "path_to_CURL_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ca_trust_path=None, ) t_default.build_session() self.assertEqual(True, t_default.session.verify) def test_build_session_cert_ignore_1(self): os.environ["REQUESTS_CA_BUNDLE"] = "path_to_REQUESTS_CA_CERT" os.environ["CURL_CA_BUNDLE"] = "path_to_CURL_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="ignore", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertIs(False, t_default.session.verify) def test_build_session_cert_ignore_2(self): os.environ["REQUESTS_CA_BUNDLE"] = "path_to_REQUESTS_CA_CERT" os.environ["CURL_CA_BUNDLE"] = "path_to_CURL_CA_CERT" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="ignore", username="test", password="test", auth_method="basic", ca_trust_path="boguspath" ) t_default.build_session() self.assertIs(False, t_default.session.verify) def test_build_session_proxy_none(self): os.environ["HTTP_PROXY"] = "random_proxy" os.environ["HTTPS_PROXY"] = "random_proxy_2" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", proxy=None ) t_default.build_session() self.assertEqual({"no_proxy": "*"}, t_default.session.proxies) def test_build_session_proxy_defined(self): t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", proxy="test_proxy" ) t_default.build_session() self.assertEqual({"http": "test_proxy", "https": "test_proxy"}, t_default.session.proxies) def test_build_session_proxy_defined_and_env(self): os.environ["HTTPS_PROXY"] = "random_proxy" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", proxy="test_proxy" ) t_default.build_session() self.assertEqual({"http": "test_proxy", "https": "test_proxy"}, t_default.session.proxies) def test_build_session_proxy_with_env_https(self): os.environ["HTTPS_PROXY"] = "random_proxy" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual({"https": "random_proxy"}, t_default.session.proxies) def test_build_session_proxy_with_env_http(self): os.environ["HTTP_PROXY"] = "random_proxy" t_default = transport.Transport( endpoint="https://example.com", server_cert_validation="validate", username="test", password="test", auth_method="basic", ) t_default.build_session() self.assertEqual({"http": "random_proxy"}, t_default.session.proxies) def test_build_session_server_cert_validation_invalid(self): with self.assertRaises(WinRMError) as exc: transport.Transport( endpoint="Endpoint", server_cert_validation="invalid_value", username="test", password="test", auth_method="basic", ) self.assertEqual("invalid server_cert_validation mode: invalid_value", str(exc.exception)) def test_build_session_krb_delegation_as_str(self): winrm_transport = transport.Transport( endpoint="Endpoint", server_cert_validation="validate", username="test", password="test", auth_method="kerberos", kerberos_delegation="True" ) self.assertTrue(winrm_transport.kerberos_delegation) def test_build_session_krb_delegation_as_invalid_str(self): with self.assertRaises(ValueError) as exc: transport.Transport( endpoint="Endpoint", server_cert_validation="validate", username="test", password="test", auth_method="kerberos", kerberos_delegation="invalid_value", ) self.assertEqual("invalid truth value 'invalid_value'", str(exc.exception)) def test_build_session_no_username(self): with self.assertRaises(InvalidCredentialsError) as exc: transport.Transport( endpoint="Endpoint", server_cert_validation="validate", password="test", auth_method="basic", ) self.assertEqual("auth method basic requires a username", str(exc.exception)) def test_build_session_no_password(self): with self.assertRaises(InvalidCredentialsError) as exc: transport.Transport( endpoint="Endpoint", server_cert_validation="validate", username="test", auth_method="basic", ) self.assertEqual("auth method basic requires a password", str(exc.exception)) def test_build_session_invalid_auth(self): winrm_transport = transport.Transport( endpoint="Endpoint", server_cert_validation="validate", username="test", password="test", auth_method="invalid_value", ) with self.assertRaises(WinRMError) as exc: winrm_transport.build_session() self.assertEqual("unsupported auth method: invalid_value", str(exc.exception)) def test_build_session_invalid_encryption(self): with self.assertRaises(WinRMError) as exc: transport.Transport( endpoint="Endpoint", server_cert_validation="validate", username="test", password="test", auth_method="basic", message_encryption="invalid_value", ) self.assertEqual("invalid message_encryption arg: invalid_value. Should be 'auto', 'always', or 'never'", str(exc.exception)) @mock.patch("requests.Session") def test_close_session(self, mock_session): t_default = transport.Transport( endpoint="Endpoint", server_cert_validation="ignore", username="test", password="test", auth_method="basic", ) t_default.build_session() t_default.close_session() mock_session.return_value.close.assert_called_once_with() self.assertIsNone(t_default.session) @mock.patch("requests.Session") def test_close_session_not_built(self, mock_session): t_default = transport.Transport( endpoint="Endpoint", server_cert_validation="ignore", username="test", password="test", auth_method="basic", ) t_default.close_session() self.assertFalse(mock_session.return_value.close.called) self.assertIsNone(t_default.session) pywinrm-0.5.0/winrm/tests/test_wql.py000066400000000000000000000000001464554417700177420ustar00rootroot00000000000000pywinrm-0.5.0/winrm/transport.py000066400000000000000000000364041464554417700170130ustar00rootroot00000000000000from __future__ import annotations import os import typing as t import warnings import requests import requests.auth from winrm.encryption import Encryption from winrm.exceptions import InvalidCredentialsError, WinRMError, WinRMTransportError DISPLAYED_PROXY_WARNING = False DISPLAYED_CA_TRUST_WARNING = False HAVE_KERBEROS = False try: from winrm.vendor.requests_kerberos import REQUIRED, HTTPKerberosAuth HAVE_KERBEROS = True except ImportError: pass HAVE_NTLM = False try: from requests_ntlm import HttpNtlmAuth HAVE_NTLM = True except ImportError as ie: pass HAVE_CREDSSP = False try: from requests_credssp import HttpCredSSPAuth HAVE_CREDSSP = True except ImportError as ie: pass __all__ = ["Transport"] def strtobool(value: str) -> bool: value = value.lower() if value in ("true", "t", "yes", "y", "on", "1"): return True elif value in ("false", "f", "no", "n", "off", "0"): return False else: raise ValueError("invalid truth value '%s'" % value) class UnsupportedAuthArgument(Warning): pass class Transport(object): def __init__( self, endpoint: str, username: str | None = None, password: str | None = None, realm: None = None, service: str | None = None, keytab: None = None, ca_trust_path: t.Literal["legacy_requests"] | str = "legacy_requests", cert_pem: str | None = None, cert_key_pem: str | None = None, read_timeout_sec: int | None = None, server_cert_validation: t.Literal["validate", "ignore"] | None = "validate", kerberos_delegation: bool | str = False, kerberos_hostname_override: str | None = None, auth_method: t.Literal["auto", "basic", "certificate", "ntlm", "kerberos", "credssp", "plaintext", "ssl"] = "auto", message_encryption: t.Literal["auto", "always", "never"] = "auto", credssp_disable_tlsv1_2: bool = False, credssp_auth_mechanism: t.Literal["auto", "ntlm", "kerberos"] = "auto", credssp_minimum_version: int = 2, send_cbt: bool = True, proxy: t.Literal["legacy_requests"] | str | None = "legacy_requests", ) -> None: self.endpoint = endpoint self.username = username self.password = password self.realm = realm self.service = service self.keytab = keytab self.ca_trust_path = ca_trust_path self.cert_pem = cert_pem self.cert_key_pem = cert_key_pem self.read_timeout_sec = read_timeout_sec self.server_cert_validation = server_cert_validation self.kerberos_hostname_override = kerberos_hostname_override self.message_encryption = message_encryption self.credssp_disable_tlsv1_2 = credssp_disable_tlsv1_2 self.credssp_auth_mechanism = credssp_auth_mechanism self.credssp_minimum_version = credssp_minimum_version self.send_cbt = send_cbt self.proxy = proxy if self.server_cert_validation not in [None, "validate", "ignore"]: raise WinRMError("invalid server_cert_validation mode: %s" % self.server_cert_validation) # defensively parse this to a bool if isinstance(kerberos_delegation, bool): self.kerberos_delegation = kerberos_delegation else: self.kerberos_delegation = bool(strtobool(str(kerberos_delegation))) self.auth_method = auth_method self.default_headers = { "Content-Type": "application/soap+xml;charset=UTF-8", "User-Agent": "Python WinRM client", } # try to suppress user-unfriendly warnings from requests' vendored urllib3 try: from requests.packages.urllib3.exceptions import InsecurePlatformWarning warnings.simplefilter("ignore", category=InsecurePlatformWarning) except Exception: pass # oh well, we tried... try: from requests.packages.urllib3.exceptions import SNIMissingWarning warnings.simplefilter("ignore", category=SNIMissingWarning) except Exception: pass # oh well, we tried... # if we're explicitly ignoring validation, try to suppress InsecureRequestWarning, since the user opted-in if self.server_cert_validation == "ignore": try: from requests.packages.urllib3.exceptions import InsecureRequestWarning warnings.simplefilter("ignore", category=InsecureRequestWarning) except Exception: pass # oh well, we tried... try: from urllib3.exceptions import InsecureRequestWarning warnings.simplefilter("ignore", category=InsecureRequestWarning) except Exception: pass # oh well, we tried... # validate credential requirements for various auth types if self.auth_method != "kerberos": if self.auth_method == "certificate" or (self.auth_method == "ssl" and (self.cert_pem or self.cert_key_pem)): if not self.cert_pem or not self.cert_key_pem: raise InvalidCredentialsError("both cert_pem and cert_key_pem must be specified for cert auth") if not os.path.exists(self.cert_pem): raise InvalidCredentialsError("cert_pem file not found (%s)" % self.cert_pem) if not os.path.exists(self.cert_key_pem): raise InvalidCredentialsError("cert_key_pem file not found (%s)" % self.cert_key_pem) else: if not self.username: raise InvalidCredentialsError("auth method %s requires a username" % self.auth_method) if self.password is None: raise InvalidCredentialsError("auth method %s requires a password" % self.auth_method) self.session: requests.Session | None = None # Used for encrypting messages self.encryption: Encryption | None = None # The Pywinrm Encryption class used to encrypt/decrypt messages if self.message_encryption not in ["auto", "always", "never"]: raise WinRMError("invalid message_encryption arg: %s. Should be 'auto', 'always', or 'never'" % self.message_encryption) def build_session(self) -> requests.Session: if self.session: return self.session session = requests.Session() proxies = dict() if self.proxy is None: proxies["no_proxy"] = "*" elif self.proxy != "legacy_requests": # If there was a proxy specified then use it proxies["http"] = self.proxy proxies["https"] = self.proxy # Merge proxy environment variables settings = session.merge_environment_settings(url=self.endpoint, proxies=proxies, stream=None, verify=None, cert=None) global DISPLAYED_PROXY_WARNING # We want to eventually stop reading proxy information from the environment. # Also only display the warning once. This method can be called many times during an application's runtime. if not DISPLAYED_PROXY_WARNING and self.proxy == "legacy_requests" and ("http" in settings["proxies"] or "https" in settings["proxies"]): message = "'pywinrm' will use an environment defined proxy. This feature will be disabled in " "the future, please specify it explicitly." if "http" in settings["proxies"]: message += " HTTP proxy {proxy} discovered.".format(proxy=settings["proxies"]["http"]) if "https" in settings["proxies"]: message += " HTTPS proxy {proxy} discovered.".format(proxy=settings["proxies"]["https"]) DISPLAYED_PROXY_WARNING = True warnings.warn(message, DeprecationWarning) session.proxies = settings["proxies"] # specified validation mode takes precedence session.verify = self.server_cert_validation == "validate" # patch in CA path override if one was specified in init or env if session.verify: if self.ca_trust_path == "legacy_requests" and settings["verify"] is not None: # We will session.verify = settings["verify"] global DISPLAYED_CA_TRUST_WARNING # We want to eventually stop reading proxy information from the environment. # Also only display the warning once. This method can be called many times during an application's runtime. if not DISPLAYED_CA_TRUST_WARNING and session.verify is not True: message = ( "'pywinrm' will use an environment variable defined CA Trust. This feature will be disabled in " "the future, please specify it explicitly." ) if os.environ.get("REQUESTS_CA_BUNDLE") is not None: message += " REQUESTS_CA_BUNDLE contains {ca_path}".format(ca_path=os.environ.get("REQUESTS_CA_BUNDLE")) elif os.environ.get("CURL_CA_BUNDLE") is not None: message += " CURL_CA_BUNDLE contains {ca_path}".format(ca_path=os.environ.get("CURL_CA_BUNDLE")) DISPLAYED_CA_TRUST_WARNING = True warnings.warn(message, DeprecationWarning) elif session.verify and self.ca_trust_path is not None: # session.verify can be either a bool or path to a CA store; prefer passed-in value over env if both are present session.verify = self.ca_trust_path encryption_available = False if self.auth_method == "kerberos": if not HAVE_KERBEROS: raise WinRMError("requested auth method is kerberos, but pykerberos is not installed") kerb_auth = session.auth = HTTPKerberosAuth( mutual_authentication=REQUIRED, delegate=self.kerberos_delegation, force_preemptive=True, principal=self.username, hostname_override=self.kerberos_hostname_override, sanitize_mutual_error_response=False, service=self.service, send_cbt=self.send_cbt, ) encryption_available = hasattr(session.auth, "winrm_encryption_available") and kerb_auth.winrm_encryption_available elif self.auth_method in ["certificate", "ssl"]: if self.auth_method == "ssl" and not self.cert_pem and not self.cert_key_pem: # 'ssl' was overloaded for HTTPS with optional certificate auth, # fall back to basic auth if no cert specified session.auth = requests.auth.HTTPBasicAuth( username=self.username or "", password=self.password or "", ) else: session.cert = (self.cert_pem or "", self.cert_key_pem or "") session.headers["Authorization"] = "http://schemas.dmtf.org/wbem/wsman/1/wsman/secprofile/https/mutual" elif self.auth_method == "ntlm": if not HAVE_NTLM: raise WinRMError("requested auth method is ntlm, but requests_ntlm is not installed") session.auth = HttpNtlmAuth( username=self.username, password=self.password, send_cbt=self.send_cbt, ) # check if requests_ntlm has the session_security attribute available for encryption encryption_available = hasattr(session.auth, "session_security") # TODO: ssl is not exactly right here- should really be client_cert elif self.auth_method in ["basic", "plaintext"]: session.auth = requests.auth.HTTPBasicAuth( username=self.username or "", password=self.password or "", ) elif self.auth_method == "credssp": if not HAVE_CREDSSP: raise WinRMError("requests auth method is credssp, but requests-credssp is not installed") session.auth = HttpCredSSPAuth( username=self.username, password=self.password, disable_tlsv1_2=self.credssp_disable_tlsv1_2, auth_mechanism=self.credssp_auth_mechanism, minimum_version=self.credssp_minimum_version, ) encryption_available = True else: raise WinRMError("unsupported auth method: %s" % self.auth_method) session.headers.update(self.default_headers) self.session = session # Will check the current config and see if we need to setup message encryption if self.message_encryption == "always" and not encryption_available: raise WinRMError("message encryption is set to 'always' but the selected auth method %s does not support it" % self.auth_method) elif encryption_available: if self.message_encryption == "always": self.setup_encryption(session) elif self.message_encryption == "auto" and not self.endpoint.lower().startswith("https"): self.setup_encryption(session) return session def setup_encryption(self, session: requests.Session) -> None: # Security context doesn't exist, sending blank message to initialise context request = requests.Request("POST", self.endpoint, data=None) prepared_request = session.prepare_request(request) self._send_message_request(session, prepared_request) self.encryption = Encryption(session, self.auth_method) def close_session(self) -> None: if not self.session: return self.session.close() self.session = None def send_message(self, message: str | bytes) -> bytes: session = self.build_session() # urllib3 fails on SSL retries with unicode buffers- must send it a byte string # see https://github.com/shazow/urllib3/issues/717 if isinstance(message, str): message = message.encode("utf-8") if self.encryption: prepared_request = self.encryption.prepare_encrypted_request(session, self.endpoint, message) else: request = requests.Request("POST", self.endpoint, data=message) prepared_request = session.prepare_request(request) response = self._send_message_request(session, prepared_request) return self._get_message_response_text(response) def _send_message_request(self, session: requests.Session, prepared_request: requests.PreparedRequest) -> requests.Response: try: response = session.send(prepared_request, timeout=self.read_timeout_sec) response.raise_for_status() return response except requests.HTTPError as ex: if ex.response.status_code == 401: raise InvalidCredentialsError("the specified credentials were rejected by the server") if ex.response.content: response_text = self._get_message_response_text(ex.response) else: response_text = b"" raise WinRMTransportError("http", ex.response.status_code, response_text.decode()) def _get_message_response_text(self, response: requests.Response) -> bytes: if self.encryption: response_text = self.encryption.parse_encrypted_response(response) else: response_text = response.content return response_text pywinrm-0.5.0/winrm/vendor/000077500000000000000000000000001464554417700156735ustar00rootroot00000000000000pywinrm-0.5.0/winrm/vendor/__init__.py000066400000000000000000000000001464554417700177720ustar00rootroot00000000000000pywinrm-0.5.0/winrm/vendor/requests_kerberos/000077500000000000000000000000001464554417700214425ustar00rootroot00000000000000pywinrm-0.5.0/winrm/vendor/requests_kerberos/__init__.py000066400000000000000000000031171464554417700235550ustar00rootroot00000000000000# ISC License # # Copyright (c) 2012 Kenneth Reitz # # Permission to use, copy, modify and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ requests Kerberos/GSSAPI authentication library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Requests is an HTTP library, written in Python, for human beings. This library adds optional Kerberos/GSSAPI authentication support and supports mutual authentication. Basic GET usage: >>> import requests >>> from requests_kerberos import HTTPKerberosAuth >>> r = requests.get("http://example.org", auth=HTTPKerberosAuth()) The entire `requests.api` should be supported. """ import logging from .compat import NullHandler from .exceptions import MutualAuthenticationError from .kerberos_ import DISABLED, OPTIONAL, REQUIRED, HTTPKerberosAuth logging.getLogger(__name__).addHandler(NullHandler()) __all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 'OPTIONAL', 'DISABLED') __version__ = '0.12.0' pywinrm-0.5.0/winrm/vendor/requests_kerberos/compat.py000066400000000000000000000022041464554417700232750ustar00rootroot00000000000000# ISC License # # Copyright (c) 2012 Kenneth Reitz # # Permission to use, copy, modify and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ Compatibility library for older versions of python """ import sys # python 2.7 introduced a NullHandler which we want to use, but to support # older versions, we implement our own if needed. if sys.version_info[:2] > (2, 6): from logging import NullHandler else: from logging import Handler class NullHandler(Handler): def emit(self, record): pass pywinrm-0.5.0/winrm/vendor/requests_kerberos/exceptions.py000066400000000000000000000021171464554417700241760ustar00rootroot00000000000000# ISC License # # Copyright (c) 2012 Kenneth Reitz # # Permission to use, copy, modify and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ requests_kerberos.exceptions ~~~~~~~~~~~~~~~~~~~ This module contains the set of exceptions. """ from requests.exceptions import RequestException class MutualAuthenticationError(RequestException): """Mutual Authentication Error""" class KerberosExchangeError(RequestException): """Kerberos Exchange Failed Error""" pywinrm-0.5.0/winrm/vendor/requests_kerberos/kerberos_.py000066400000000000000000000457531464554417700240050ustar00rootroot00000000000000# ISC License # # Copyright (c) 2012 Kenneth Reitz # # Permission to use, copy, modify and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS-IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. try: import kerberos except ImportError: import winkerberos as kerberos import logging import re import sys import warnings from cryptography import x509 from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from requests.auth import AuthBase from requests.compat import StringIO, urlparse from requests.cookies import cookiejar_from_dict from requests.models import Response from requests.packages.urllib3 import HTTPResponse from requests.structures import CaseInsensitiveDict from .exceptions import KerberosExchangeError, MutualAuthenticationError log = logging.getLogger(__name__) # Different types of mutual authentication: # with mutual_authentication set to REQUIRED, all responses will be # authenticated with the exception of errors. Errors will have their contents # and headers stripped. If a non-error response cannot be authenticated, a # MutualAuthenticationError exception will be raised. # with mutual_authentication set to OPTIONAL, mutual authentication will be # attempted if supported, and if supported and failed, a # MutualAuthenticationError exception will be raised. Responses which do not # support mutual authentication will be returned directly to the user. # with mutual_authentication set to DISABLED, mutual authentication will not be # attempted, even if supported. REQUIRED = 1 OPTIONAL = 2 DISABLED = 3 class NoCertificateRetrievedWarning(Warning): pass class UnknownSignatureAlgorithmOID(Warning): pass class SanitizedResponse(Response): """The :class:`Response ` object, which contains a server's response to an HTTP request. This differs from `requests.models.Response` in that it's headers and content have been sanitized. This is only used for HTTP Error messages which do not support mutual authentication when mutual authentication is required.""" def __init__(self, response): super(SanitizedResponse, self).__init__() self.status_code = response.status_code self.encoding = response.encoding self.raw = response.raw self.reason = response.reason self.url = response.url self.request = response.request self.connection = response.connection self._content_consumed = True self._content = "" self.cookies = cookiejar_from_dict({}) self.headers = CaseInsensitiveDict() self.headers['content-length'] = '0' for header in ('date', 'server'): if header in response.headers: self.headers[header] = response.headers[header] def _negotiate_value(response): """Extracts the gssapi authentication token from the appropriate header""" if hasattr(_negotiate_value, 'regex'): regex = _negotiate_value.regex else: # There's no need to re-compile this EVERY time it is called. Compile # it once and you won't have the performance hit of the compilation. regex = re.compile(r'(?:.*,)*\s*Negotiate\s*([^,]*),?', re.I) _negotiate_value.regex = regex authreq = response.headers.get('www-authenticate', None) if authreq: match_obj = regex.search(authreq) if match_obj: return match_obj.group(1) return None def _get_certificate_hash(certificate_der): # https://tools.ietf.org/html/rfc5929#section-4.1 cert = x509.load_der_x509_certificate(certificate_der, default_backend()) try: hash_algorithm = cert.signature_hash_algorithm except UnsupportedAlgorithm as ex: warnings.warn("Failed to get signature algorithm from certificate, " "unable to pass channel bindings: %s" % str(ex), UnknownSignatureAlgorithmOID) return None # if the cert signature algorithm is either md5 or sha1 then use sha256 # otherwise use the signature algorithm if hash_algorithm.name in ['md5', 'sha1']: digest = hashes.Hash(hashes.SHA256(), default_backend()) else: digest = hashes.Hash(hash_algorithm, default_backend()) digest.update(certificate_der) certificate_hash = digest.finalize() return certificate_hash def _get_channel_bindings_application_data(response): """ https://tools.ietf.org/html/rfc5929 4. The 'tls-server-end-point' Channel Binding Type Gets the application_data value for the 'tls-server-end-point' CBT Type. This is ultimately the SHA256 hash of the certificate of the HTTPS endpoint appended onto tls-server-end-point. This value is then passed along to the kerberos library to bind to the auth response. If the socket is not an SSL socket or the raw HTTP object is not a urllib3 HTTPResponse then None will be returned and the Kerberos auth will use GSS_C_NO_CHANNEL_BINDINGS :param response: The original 401 response from the server :return: byte string used on the application_data.value field on the CBT struct """ application_data = None raw_response = response.raw if isinstance(raw_response, HTTPResponse): try: if sys.version_info > (3, 0): socket = raw_response._fp.fp.raw._sock else: socket = raw_response._fp.fp._sock except AttributeError: warnings.warn("Failed to get raw socket for CBT; has urllib3 impl changed", NoCertificateRetrievedWarning) else: try: server_certificate = socket.getpeercert(True) except AttributeError: pass else: certificate_hash = _get_certificate_hash(server_certificate) application_data = b'tls-server-end-point:' + certificate_hash else: warnings.warn( "Requests is running with a non urllib3 backend, cannot retrieve server certificate for CBT", NoCertificateRetrievedWarning) return application_data class HTTPKerberosAuth(AuthBase): """Attaches HTTP GSSAPI/Kerberos Authentication to the given Request object.""" def __init__( self, mutual_authentication=REQUIRED, service="HTTP", delegate=False, force_preemptive=False, principal=None, hostname_override=None, sanitize_mutual_error_response=True, send_cbt=True): self.context = {} self.mutual_authentication = mutual_authentication self.delegate = delegate self.pos = None self.service = service self.force_preemptive = force_preemptive self.principal = principal self.hostname_override = hostname_override self.sanitize_mutual_error_response = sanitize_mutual_error_response self.auth_done = False self.winrm_encryption_available = hasattr(kerberos, 'authGSSWinRMEncryptMessage') # Set the CBT values populated after the first response self.send_cbt = send_cbt self.cbt_binding_tried = False self.cbt_struct = None def generate_request_header(self, response, host, is_preemptive=False): """ Generates the GSSAPI authentication token with kerberos. If any GSSAPI step fails, raise KerberosExchangeError with failure detail. """ # Flags used by kerberos module. gssflags = kerberos.GSS_C_MUTUAL_FLAG | kerberos.GSS_C_SEQUENCE_FLAG if self.delegate: gssflags |= kerberos.GSS_C_DELEG_FLAG try: kerb_stage = "authGSSClientInit()" # contexts still need to be stored by host, but hostname_override # allows use of an arbitrary hostname for the kerberos exchange # (eg, in cases of aliased hosts, internal vs external, CNAMEs # w/ name-based HTTP hosting) kerb_host = self.hostname_override if self.hostname_override is not None else host kerb_spn = "{0}@{1}".format(self.service, kerb_host) result, self.context[host] = kerberos.authGSSClientInit(kerb_spn, gssflags=gssflags, principal=self.principal) if result < 1: raise EnvironmentError(result, kerb_stage) # if we have a previous response from the server, use it to continue # the auth process, otherwise use an empty value negotiate_resp_value = '' if is_preemptive else _negotiate_value(response) kerb_stage = "authGSSClientStep()" # If this is set pass along the struct to Kerberos if self.cbt_struct: result = kerberos.authGSSClientStep(self.context[host], negotiate_resp_value, channel_bindings=self.cbt_struct) else: result = kerberos.authGSSClientStep(self.context[host], negotiate_resp_value) if result < 0: raise EnvironmentError(result, kerb_stage) kerb_stage = "authGSSClientResponse()" gss_response = kerberos.authGSSClientResponse(self.context[host]) return "Negotiate {0}".format(gss_response) except kerberos.GSSError as error: log.exception( "generate_request_header(): {0} failed:".format(kerb_stage)) log.exception(error) raise KerberosExchangeError("%s failed: %s" % (kerb_stage, str(error.args))) except EnvironmentError as error: # ensure we raised this for translation to KerberosExchangeError # by comparing errno to result, re-raise if not if error.errno != result: raise message = "{0} failed, result: {1}".format(kerb_stage, result) log.error("generate_request_header(): {0}".format(message)) raise KerberosExchangeError(message) def authenticate_user(self, response, **kwargs): """Handles user authentication with gssapi/kerberos""" host = urlparse(response.url).hostname try: auth_header = self.generate_request_header(response, host) except KerberosExchangeError: # GSS Failure, return existing response return response log.debug("authenticate_user(): Authorization header: {0}".format( auth_header)) response.request.headers['Authorization'] = auth_header # Consume the content so we can reuse the connection for the next # request. response.content response.raw.release_conn() _r = response.connection.send(response.request, **kwargs) _r.history.append(response) log.debug("authenticate_user(): returning {0}".format(_r)) return _r def handle_401(self, response, **kwargs): """Handles 401's, attempts to use gssapi/kerberos authentication""" log.debug("handle_401(): Handling: 401") if _negotiate_value(response) is not None: _r = self.authenticate_user(response, **kwargs) log.debug("handle_401(): returning {0}".format(_r)) return _r else: log.debug("handle_401(): Kerberos is not supported") log.debug("handle_401(): returning {0}".format(response)) return response def handle_other(self, response): """Handles all responses with the exception of 401s. This is necessary so that we can authenticate responses if requested""" log.debug("handle_other(): Handling: %d" % response.status_code) if self.mutual_authentication in (REQUIRED, OPTIONAL) and not self.auth_done: is_http_error = response.status_code >= 400 if _negotiate_value(response) is not None: log.debug("handle_other(): Authenticating the server") if not self.authenticate_server(response): # Mutual authentication failure when mutual auth is wanted, # raise an exception so the user doesn't use an untrusted # response. log.error("handle_other(): Mutual authentication failed") raise MutualAuthenticationError("Unable to authenticate " "{0}".format(response)) # Authentication successful log.debug("handle_other(): returning {0}".format(response)) self.auth_done = True return response elif is_http_error or self.mutual_authentication == OPTIONAL: if not response.ok: log.error("handle_other(): Mutual authentication unavailable " "on {0} response".format(response.status_code)) if(self.mutual_authentication == REQUIRED and self.sanitize_mutual_error_response): return SanitizedResponse(response) else: return response else: # Unable to attempt mutual authentication when mutual auth is # required, raise an exception so the user doesn't use an # untrusted response. log.error("handle_other(): Mutual authentication failed") raise MutualAuthenticationError("Unable to authenticate " "{0}".format(response)) else: log.debug("handle_other(): returning {0}".format(response)) return response def authenticate_server(self, response): """ Uses GSSAPI to authenticate the server. Returns True on success, False on failure. """ log.debug("authenticate_server(): Authenticate header: {0}".format( _negotiate_value(response))) host = urlparse(response.url).hostname try: # If this is set pass along the struct to Kerberos if self.cbt_struct: result = kerberos.authGSSClientStep(self.context[host], _negotiate_value(response), channel_bindings=self.cbt_struct) else: result = kerberos.authGSSClientStep(self.context[host], _negotiate_value(response)) except kerberos.GSSError: log.exception("authenticate_server(): authGSSClientStep() failed:") return False if result < 1: log.error("authenticate_server(): authGSSClientStep() failed: " "{0}".format(result)) return False log.debug("authenticate_server(): returning {0}".format(response)) return True def handle_response(self, response, **kwargs): """Takes the given response and tries kerberos-auth, as needed.""" num_401s = kwargs.pop('num_401s', 0) # Check if we have already tried to get the CBT data value if not self.cbt_binding_tried and self.send_cbt: # If we haven't tried, try getting it now cbt_application_data = _get_channel_bindings_application_data(response) if cbt_application_data: # Only the latest version of pykerberos has this method available try: self.cbt_struct = kerberos.channelBindings(application_data=cbt_application_data) except AttributeError: # Using older version set to None self.cbt_struct = None # Regardless of the result, set tried to True so we don't waste time next time self.cbt_binding_tried = True if self.pos is not None: # Rewind the file position indicator of the body to where # it was to resend the request. response.request.body.seek(self.pos) if response.status_code == 401 and num_401s < 2: # 401 Unauthorized. Handle it, and if it still comes back as 401, # that means authentication failed. _r = self.handle_401(response, **kwargs) log.debug("handle_response(): returning %s", _r) log.debug("handle_response() has seen %d 401 responses", num_401s) num_401s += 1 return self.handle_response(_r, num_401s=num_401s, **kwargs) elif response.status_code == 401 and num_401s >= 2: # Still receiving 401 responses after attempting to handle them. # Authentication has failed. Return the 401 response. log.debug("handle_response(): returning 401 %s", response) return response else: _r = self.handle_other(response) log.debug("handle_response(): returning %s", _r) return _r def deregister(self, response): """Deregisters the response handler""" response.request.deregister_hook('response', self.handle_response) def wrap_winrm(self, host, message): if not self.winrm_encryption_available: raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos") return kerberos.authGSSWinRMEncryptMessage(self.context[host], message) def unwrap_winrm(self, host, message, header): if not self.winrm_encryption_available: raise NotImplementedError("WinRM encryption is not available on the installed version of pykerberos") return kerberos.authGSSWinRMDecryptMessage(self.context[host], message, header) def __call__(self, request): if self.force_preemptive and not self.auth_done: # add Authorization header before we receive a 401 # by the 401 handler host = urlparse(request.url).hostname auth_header = self.generate_request_header(None, host, is_preemptive=True) log.debug("HTTPKerberosAuth: Preemptive Authorization header: {0}".format(auth_header)) request.headers['Authorization'] = auth_header request.register_hook('response', self.handle_response) try: self.pos = request.body.tell() except AttributeError: # In the case of HTTPKerberosAuth being reused and the body # of the previous request was a file-like object, pos has # the file position of the previous body. Ensure it's set to # None. self.pos = None return request