pax_global_header00006660000000000000000000000064142257171710014521gustar00rootroot0000000000000052 comment=de7ac17ef03fc3f25a1e809470e8f9986b79647a pywinrm-0.4.3/000077500000000000000000000000001422571717100132325ustar00rootroot00000000000000pywinrm-0.4.3/.coveragerc000066400000000000000000000000471422571717100153540ustar00rootroot00000000000000[run] omit = */tests* */vendor*pywinrm-0.4.3/.github/000077500000000000000000000000001422571717100145725ustar00rootroot00000000000000pywinrm-0.4.3/.github/workflows/000077500000000000000000000000001422571717100166275ustar00rootroot00000000000000pywinrm-0.4.3/.github/workflows/ci.yml000066400000000000000000000101351422571717100177450ustar00rootroot00000000000000name: Test pywinrm on: push: branches: - master pull_request: branches: - master # env: # WINRM_USERNAME: pywinrm-test # WINRM_PASSWORD: Password123! jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - macos-latest - ubuntu-latest - windows-latest python-version: - 2.7 - 3.6 - 3.7 - 3.8 - 3.9 - '3.10' - pypy-2.7 - pypy-3.7 arch: - x86 - x64 exclude: - os: macos-latest arch: x86 - os: macos-latest python-version: pypy-2.7 - os: macos-latest python-version: pypy-3.7 - os: ubuntu-latest arch: x86 - os: windows-latest python-version: pypy-2.7 - os: windows-latest python-version: pypy-3.7 steps: - uses: actions/checkout@v2 - name: set up python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.arch }} # - name: Remove extra modules to speed up PowerShell startup module due to slow WinRM issue # if: startsWith(matrix.os, 'windows') # shell: bash # run: | # rm -rf "/c/Program Files/WindowsPowerShell/Modules/AWSPowerShell" # rm -rf "/c/Program Files/WindowsPowerShell/Modules/Microsoft.Graph*" # - name: set up Windows integration tests # if: startsWith(matrix.os, 'windows') # shell: pwsh # run: | # Write-Host 'Create local admin user' # $userParams = @{ # Name = $env:WINRM_USERNAME # Password = (ConvertTo-SecureString -AsPlainText -Force -String $env:WINRM_PASSWORD) # AccountNeverExpires = $true # PasswordNeverExpires = $true # } # $null = New-LocalUser @userParams # Add-LocalGroupMember -Group Administrators -Member $userParams.Name # Write-Host 'Setting up WinRM settings' # Enable-PSRemoting -Force -SkipNetworkProfileCheck # Enable-WSManCredSSP -Role Server -Force # Set-Item WSMan:\localhost\Service\Auth\Basic $true # Set-Item WSMan:\localhost\Service\Auth\CredSSP $true # Set-Item WSMan:\localhost\Service\AllowUnencrypted $true - name: set up Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: >- sudo apt-get install -y gcc python-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 - non Windows if: "!startsWith(matrix.os, 'windows')" run: | pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ - name: run test - Windows if: startsWith(matrix.os, 'windows') run: | pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ # env: # WINRM_TRANSPORT: basic # WINRM_ENDPOINT: http://localhost:5985/wsman - name: upload coverage data if: "!endsWith(matrix.python-version, '2.7')" # Uses an older coverals version that doesn't support GHA run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # - name: run integration with NTLM # if: startsWith(matrix.os, 'windows') # run: | # Set-Item WSMan:\localhost\Service\AllowUnencrypted $false # py.test -v winrm/tests/test_integration_protocol.py winrm/tests/test_integration_session.py # env: # WINRM_TRANSPORT: ntlm # WINRM_ENDPOINT: http://localhost:5985/wsman # - name: run integration with CredSSP # if: startsWith(matrix.os, 'windows') # run: | # Set-Item WSMan:\localhost\Service\AllowUnencrypted $false # py.test -v winrm/tests/test_integration_protocol.py winrm/tests/test_integration_session.py # env: # WINRM_TRANSPORT: credssp # WINRM_ENDPOINT: http://localhost:5985/wsman pywinrm-0.4.3/.gitignore000066400000000000000000000007171422571717100152270ustar00rootroot00000000000000# 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 pywinrm-0.4.3/CHANGELOG.md000066400000000000000000000064241422571717100150510ustar00rootroot00000000000000# Changelog ### 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.4.3/LICENSE000066400000000000000000000020371422571717100142410ustar00rootroot00000000000000Copyright (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.4.3/MANIFEST.in000066400000000000000000000000211422571717100147610ustar00rootroot00000000000000include LICENSE pywinrm-0.4.3/README.md000066400000000000000000000221741422571717100145170ustar00rootroot00000000000000# 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/workflows/Test%20pywinrm/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 2.6-2.7, 3.3-3.5 or PyPy2 * [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 python-dev libkrb5-dev $ pip install pywinrm[kerberos] # for RHEL/CentOS/etc: $ sudo yum install gcc python-devel krb5-devel krb5-workstation python-devel $ pip install pywinrm[kerberos] ``` ### To use CredSSP authentication you need these optional dependencies ```bash # for Debian/Ubuntu/etc: $ sudo apt-get install gcc python-dev libssl-dev $ pip install pywinrm[credssp] # for RHEL/CentOS/etc: $ sudo yum install gcc python-devel openssl-devel $ 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). You can use [this script](https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1) to easily set up a HTTPS endpoint on WinRM with a self-signed certificate, but the use of a verifiable certificate authority is recommended in production environments. 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 and HTTPS with self-signed certificate (includes firewall rules): ``` # from powershell: Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1')) ``` 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.4.3/requirements-test.txt000066400000000000000000000001621422571717100174720ustar00rootroot00000000000000# this assumes the base requirements have been satisfied via setup.py pytest pytest-cov pytest-flake8==1.0.7 mock pywinrm-0.4.3/setup.cfg000066400000000000000000000002611422571717100150520ustar00rootroot00000000000000[bdist_rpm] requires = python-xmltodict [bdist_wheel] universal = 1 [tool:pytest] norecursedirs = .git .idea env flake8-ignore = tests/*.py E501 flake8-max-line-length = 160 pywinrm-0.4.3/setup.py000066400000000000000000000040141422571717100147430ustar00rootroot00000000000000from setuptools import setup, find_packages __version__ = '0.4.3' project_name = 'pywinrm' # PyPi supports only reStructuredText, so pandoc should be installed # before uploading package try: import pypandoc long_description = pypandoc.convert('README.md', 'rst') except ImportError: long_description = '' setup( name=project_name, version=__version__, description='Python library for Windows Remote Management', long_description=long_description, keywords='winrm ws-man devops ws-management'.split(' '), author='Alexey Diyan', author_email='alexey.diyan@gmail.com', url='http://github.com/diyan/pywinrm/', license='MIT license', packages=find_packages(), package_data={'winrm.tests': ['*.ps1']}, install_requires=['xmltodict', 'requests>=2.9.1', 'requests_ntlm>=1.1.0', 'six'], extras_require={ 'credssp': ['requests-credssp>=1.0.0'], 'kerberos:sys_platform=="win32"': ['winkerberos>=0.5.0'], 'kerberos:sys_platform!="win32"': ['pykerberos>=1.2.1,<2.0.0'] }, 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 :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Clustering', 'Topic :: System :: Distributed Computing', 'Topic :: System :: Systems Administration' ], ) pywinrm-0.4.3/winrm/000077500000000000000000000000001422571717100143665ustar00rootroot00000000000000pywinrm-0.4.3/winrm/__init__.py000066400000000000000000000120101422571717100164710ustar00rootroot00000000000000from __future__ import unicode_literals import re from base64 import b64encode import xml.etree.ElementTree as ET import warnings from winrm.protocol import Protocol # 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): self.std_out, self.std_err, self.status_code = args def __repr__(self): # 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, auth, **kwargs): 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, args=()): # 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): """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): """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 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): """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, transport): match = re.match( r'(?i)^((?Phttp[s]?)://)?(?P[0-9a-z-_.]+)(:(?P\d+))?(?P(/)?(wsman)?)?', target) # NOQA 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.4.3/winrm/encryption.py000066400000000000000000000243131422571717100171350ustar00rootroot00000000000000import requests import re import struct import sys from winrm.exceptions import WinRMError is_py2 = sys.version[0] == '2' if is_py2: from urlparse import urlsplit else: from urllib.parse import urlsplit class Encryption(object): SIXTEN_KB = 16384 MIME_BOUNDARY = b'--Encrypted Boundary' def __init__(self, session, protocol): """ [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, endpoint, message): """ 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)) 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): """ 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.text return msg def _encrypt_message(self, message, host): 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, host): 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, host): signature_length = struct.unpack("= 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='stdin', o_stream='stdout stderr', working_directory=None, env_vars=None, noprofile=False, codepage=437, lifetime=None, idle_timeout=None): """ 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._get_soap_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 next( node for node in root.findall('.//*') if node.get('Name') == 'ShellId').text # Helper method for building SOAP Header def _get_soap_header( self, action=None, resource_uri=None, shell_id=None, message_id=None): if not message_id: message_id = uuid.uuid4() header = { '@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 def send_message(self, message): # 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 not None: fault_data = dict( transport_message=ex.message, http_status_code=ex.code ) wsmanfault_code = fault.find('soapenv:Detail/wsmanfault:WSManFault[@Code]', xmlns) if wsmanfault_code is not None: fault_data['wsmanfault_code'] = wsmanfault_code.get('Code') # convert receive timeout code to WinRMOperationTimeoutError if fault_data['wsmanfault_code'] == '2150858793': # TODO: this fault code is specific to the Receive operation; convert all op timeouts? raise WinRMOperationTimeoutError() fault_code = fault.find('soapenv:Code/soapenv:Value', xmlns) if fault_code is not None: fault_data['fault_code'] = fault_code.text fault_subcode = fault.find('soapenv:Code/soapenv:Subcode/soapenv:Value', xmlns) if fault_subcode is not None: fault_data['fault_subcode'] = fault_subcode.text error_message = fault.find('soapenv:Reason/soapenv:Text', xmlns) if error_message is not None: error_message = error_message.text else: error_message = "(no error message in fault)" raise WinRMError('{0} (extended fault data: {1})'.format(error_message, fault_data)) def close_shell(self, shell_id, close_session=True): """ 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._get_soap_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 = 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, command, arguments=(), console_mode_stdin=True, skip_cmd_shell=False): """ 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._get_soap_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, text_type) else a.decode('utf-8') for a in arguments] cmd_line['rsp:Arguments'] = u' '.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 command_id def cleanup_command(self, shell_id, command_id): """ 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._get_soap_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 = 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, command_id, stdin_input, end=False): """ 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, text_type): stdin_input = stdin_input.encode("437") req = {'env:Envelope': self._get_soap_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, command_id): """ Get the Output of 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 #@return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the corresponding key # is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it occurs on # the console. """ stdout_buffer, stderr_buffer = [], [] command_done = False while not command_done: try: stdout, stderr, return_code, command_done = \ self._raw_get_command_output(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 _raw_get_command_output(self, shell_id, command_id): req = {'env:Envelope': self._get_soap_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 = b'' return_code = -1 for stream_node in stream_nodes: if not stream_node.text: continue if stream_node.attrib['Name'] == 'stdout': stdout += base64.b64decode(stream_node.text.encode('ascii')) elif stream_node.attrib['Name'] == 'stderr': stderr += 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) return stdout, stderr, return_code, command_done pywinrm-0.4.3/winrm/tests/000077500000000000000000000000001422571717100155305ustar00rootroot00000000000000pywinrm-0.4.3/winrm/tests/__init__.py000066400000000000000000000000001422571717100176270ustar00rootroot00000000000000pywinrm-0.4.3/winrm/tests/conftest.py000066400000000000000000001164421422571717100177370ustar00rootroot00000000000000# flake8: noqa import os import uuid import xmltodict from pytest import skip, fixture from mock import patch 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.4.3/winrm/tests/sample_script.ps1000066400000000000000000000000001422571717100210100ustar00rootroot00000000000000pywinrm-0.4.3/winrm/tests/test_cmd.py000066400000000000000000000000001422571717100176720ustar00rootroot00000000000000pywinrm-0.4.3/winrm/tests/test_encryption.py000066400000000000000000000302311422571717100213320ustar00rootroot00000000000000import base64 import pytest import struct 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("System.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.4.3/winrm/tests/test_transport.py000066400000000000000000000344321422571717100212030ustar00rootroot00000000000000# coding=utf-8 import os import mock import unittest from winrm import transport from winrm.exceptions import WinRMError, InvalidCredentialsError 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.4.3/winrm/tests/test_wql.py000066400000000000000000000000001422571717100177320ustar00rootroot00000000000000pywinrm-0.4.3/winrm/transport.py000066400000000000000000000351471422571717100170060ustar00rootroot00000000000000from __future__ import unicode_literals import sys import os import requests import requests.auth import warnings from winrm.exceptions import InvalidCredentialsError, WinRMError, WinRMTransportError from winrm.encryption import Encryption is_py2 = sys.version[0] == '2' DISPLAYED_PROXY_WARNING = False DISPLAYED_CA_TRUST_WARNING = False if is_py2: # use six for this instead? unicode_type = type(u'') else: # use six for this instead? unicode_type = type(u'') HAVE_KERBEROS = False try: from winrm.vendor.requests_kerberos import HTTPKerberosAuth, REQUIRED 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): 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, username=None, password=None, realm=None, service=None, keytab=None, ca_trust_path='legacy_requests', cert_pem=None, cert_key_pem=None, read_timeout_sec=None, server_cert_validation='validate', kerberos_delegation=False, kerberos_hostname_override=None, auth_method='auto', message_encryption='auto', credssp_disable_tlsv1_2=False, credssp_auth_mechanism='auto', credssp_minimum_version=2, send_cbt=True, proxy='legacy_requests'): 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 = None # Used for encrypting messages self.encryption = 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): 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") 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 session.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, password=self.password) else: session.cert = (self.cert_pem, self.cert_key_pem) 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, password=self.password) 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() elif self.message_encryption == 'auto' and not self.endpoint.lower().startswith('https'): self.setup_encryption() def setup_encryption(self): # Security context doesn't exist, sending blank message to initialise context request = requests.Request('POST', self.endpoint, data=None) prepared_request = self.session.prepare_request(request) self._send_message_request(prepared_request, '') self.encryption = Encryption(self.session, self.auth_method) def close_session(self): if not self.session: return self.session.close() self.session = None def send_message(self, message): if not self.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, unicode_type): message = message.encode('utf-8') if self.encryption: prepared_request = self.encryption.prepare_encrypted_request(self.session, self.endpoint, message) else: request = requests.Request('POST', self.endpoint, data=message) prepared_request = self.session.prepare_request(request) response = self._send_message_request(prepared_request, message) return self._get_message_response_text(response) def _send_message_request(self, prepared_request, message): try: response = self.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 = '' raise WinRMTransportError('http', ex.response.status_code, response_text) def _get_message_response_text(self, response): if self.encryption: response_text = self.encryption.parse_encrypted_response(response) else: response_text = response.content return response_text pywinrm-0.4.3/winrm/vendor/000077500000000000000000000000001422571717100156635ustar00rootroot00000000000000pywinrm-0.4.3/winrm/vendor/__init__.py000066400000000000000000000000001422571717100177620ustar00rootroot00000000000000pywinrm-0.4.3/winrm/vendor/requests_kerberos/000077500000000000000000000000001422571717100214325ustar00rootroot00000000000000pywinrm-0.4.3/winrm/vendor/requests_kerberos/__init__.py000066400000000000000000000031171422571717100235450ustar00rootroot00000000000000# 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 .kerberos_ import HTTPKerberosAuth, REQUIRED, OPTIONAL, DISABLED from .exceptions import MutualAuthenticationError from .compat import NullHandler logging.getLogger(__name__).addHandler(NullHandler()) __all__ = ('HTTPKerberosAuth', 'MutualAuthenticationError', 'REQUIRED', 'OPTIONAL', 'DISABLED') __version__ = '0.12.0' pywinrm-0.4.3/winrm/vendor/requests_kerberos/compat.py000066400000000000000000000022041422571717100232650ustar00rootroot00000000000000# 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.4.3/winrm/vendor/requests_kerberos/exceptions.py000066400000000000000000000021171422571717100241660ustar00rootroot00000000000000# 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.4.3/winrm/vendor/requests_kerberos/kerberos_.py000066400000000000000000000457531422571717100237750ustar00rootroot00000000000000# 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.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.exceptions import UnsupportedAlgorithm from requests.auth import AuthBase from requests.models import Response from requests.compat import urlparse, StringIO from requests.structures import CaseInsensitiveDict from requests.cookies import cookiejar_from_dict from requests.packages.urllib3 import HTTPResponse from .exceptions import MutualAuthenticationError, KerberosExchangeError 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