Skip to content

Commit

Permalink
switch to requests, add NTLM and kerberos delegation support
Browse files Browse the repository at this point in the history
  • Loading branch information
nitzmahone committed May 3, 2016
1 parent 41aa058 commit 1213eaf
Show file tree
Hide file tree
Showing 15 changed files with 429 additions and 1,812 deletions.
8 changes: 5 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ language: python
python:
- 2.6
- 2.7
- 3.2
# py 3.2 removed since pytest itself is broken there
- 3.3
- 3.4
- 3.5
- pypy
install:
- sudo apt-get install libffi-dev
- pip install cffi coveralls
#- sudo apt-get install libffi-dev
#- pip install cffi coveralls
- pip install coveralls
- pip install -r requirements.txt
script:
# TODO enable pep8 checking
# py.test -v --pep8=winrm --cov=winrm --cov-report=term-missing winrm/tests/
- py.test -v --cov=winrm --cov-report=term-missing winrm/tests/

after_success:
- coveralls
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@ For more information on WinRM, please visit
* [python-kerberos](http://pypi.python.org/pypi/kerberos) is optional

## Installation
### To install pywinrm, simply
### 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 python-dev libkrb5-dev
$ pip install kerberos
$ pip install pywinrm[kerberos]

# for RHEL/CentOS/etc:
$ sudo yum install gcc krb5-devel krb5-workstation
$ pip install pywinrm[kerberos]
```

## Example Usage
Expand Down
60 changes: 60 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
environment:
global:
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
# /E:ON and /V:ON options are not enabled in the batch script intepreter
# See: http://stackoverflow.com/a/13751649/163740
WITH_COMPILER: "cmd /E:ON /V:ON /C .\\scripts\\run_with_compiler.cmd"
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.9"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python27-x64"
PYTHON_VERSION: "2.7.9"
PYTHON_ARCH: "64"

- PYTHON: "C:\\Python33"
PYTHON_VERSION: "3.3.5"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python33-x64"
PYTHON_VERSION: "3.3.5"
PYTHON_ARCH: "64"

- PYTHON: "C:\\Python34"
PYTHON_VERSION: "3.4.3"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python34-x64"
PYTHON_VERSION: "3.4.3"
PYTHON_ARCH: "64"
init:
# Override default Python version/architecture
- set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%
- python -c "import platform; print('Python', platform.python_version(), platform.architecture()[0])"
- if not exist %PYTHON%\\Scripts\\pip.exe curl https://bootstrap.pypa.io/get-pip.py | python

install:
- winrm set winrm/config/service/auth @{Basic="true"}
- winrm set winrm/config/service @{AllowUnencrypted="true"}
- "%WITH_COMPILER% pip install cffi coveralls"
- pip install -r requirements.txt

build: off # Do not run MSBuild, build stuff at install step

build_script:
- echo build_script

test_script:
# configure winrm envvars for tests
- ps: |
$ErrorActionPreference = "Stop"
$env:WINRM_USERNAME=$($env:USERNAME)
$env:WINRM_PASSWORD=[Microsoft.Win32.Registry]::GetValue("HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon", "DefaultPassword", '')
$env:WINRM_TRANSPORT="basic"
$env:WINRM_ENDPOINT="http://localhost:5985/wsman"
py.test -v --cov-report=term-missing --cov=.
after_test:
- echo after_test
47 changes: 47 additions & 0 deletions scripts/run_with_compiler.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
:: To build extensions for 64 bit Python 3, we need to configure environment
:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)
::
:: To build extensions for 64 bit Python 2, we need to configure environment
:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:
:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)
::
:: 32 bit builds do not require specific environment configurations.
::
:: Note: this script needs to be run with the /E:ON and /V:ON flags for the
:: cmd interpreter, at least for (SDK v7.0)
::
:: More details at:
:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
:: http://stackoverflow.com/a/13751649/163740
::
:: Author: Olivier Grisel
:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
@ECHO OFF

SET COMMAND_TO_RUN=%*
SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows

SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%"
IF %MAJOR_PYTHON_VERSION% == "2" (
SET WINDOWS_SDK_VERSION="v7.0"
) ELSE IF %MAJOR_PYTHON_VERSION% == "3" (
SET WINDOWS_SDK_VERSION="v7.1"
) ELSE (
ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%"
EXIT 1
)

IF "%PYTHON_ARCH%"=="64" (
ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture
SET DISTUTILS_USE_SDK=1
SET MSSdk=1
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
ECHO Executing: %COMMAND_TO_RUN%
call %COMMAND_TO_RUN% || EXIT 1
) ELSE (
ECHO Using default MSVC build environment for 32 bit architecture
ECHO Executing: %COMMAND_TO_RUN%
call %COMMAND_TO_RUN% || EXIT 1
)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[bdist_rpm]
requires = python-xmltodict python-isodate

[pytest]
norecursedirs = .git .idea env
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from distutils.core import setup

__version__ = '0.1.1'
__version__ = '0.2.0'
project_name = 'pywinrm'

# PyPi supports only reStructuredText, so pandoc should be installed
Expand All @@ -24,7 +24,8 @@
license='MIT license',
packages=('winrm', 'winrm.tests'),
package_data={'winrm.tests': ['*.ps1']},
install_requires=['xmltodict', 'isodate'],
install_requires=['xmltodict', 'isodate', 'requests>=2.9.1', 'requests_ntlm>0.2.0'],
extras_require = dict(kerberos=['requests_keberos>0.8.0']),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
Expand Down
23 changes: 14 additions & 9 deletions winrm/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import unicode_literals
import re
import base64
from base64 import b64encode
import xml.etree.ElementTree as ET

from winrm.protocol import Protocol

# feature support attributes for multi-version clients
FEATURE_SUPPORTED_AUTHTYPES=['basic', 'certificate', 'ntlm', 'kerberos', 'plaintext', 'ssl']
FEATURE_READ_TIMEOUT=True
FEATURE_OPERATION_TIMEOUT=True

class Response(object):
"""Response from a remote command execution"""
Expand Down Expand Up @@ -37,28 +42,27 @@ def run_ps(self, script):
"""base64 encodes a Powershell script and executes the powershell
encoded script command
"""

# must use utf16 little endian on windows
base64_script = base64.b64encode(script.encode("utf_16_le"))
rs = self.run_cmd("powershell -encodedcommand %s" % (base64_script))
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)
rs.std_err = self._clean_error_msg(rs.std_err)
return rs

def clean_error_msg(self, msg):
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("#< 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)
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")
Expand All @@ -70,6 +74,7 @@ def clean_error_msg(self, msg):
except Exception as e:
# if any of the above fails, the msg was not true xml
# print a warning and return the orignal string
# TODO do not print, raise user defined error instead
print("Warning: there was a problem converting the Powershell"
" error message: %s" % (e))
else:
Expand All @@ -80,7 +85,7 @@ def clean_error_msg(self, msg):
msg = new_msg.strip()
return msg

def strip_namespace(self, xml):
def _strip_namespace(self, xml):
"""strips any namespaces from an xml string"""
try:
p = re.compile("xmlns=*[\"\"][^\"\"]*[\"\"]")
Expand Down
50 changes: 19 additions & 31 deletions winrm/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
import re
from __future__ import unicode_literals


class WinRMWebServiceError(Exception):
"""Generic WinRM SOAP Error"""
pass


class WinRMAuthorizationError(Exception):
"""Authorization Error"""
pass


class WinRMWSManFault(Exception):
"""A Fault returned in the SOAP response. The XML node is a WSManFault"""
pass

class WinRMError(Exception):
""""Generic WinRM error"""
code = 500

class WinRMTransportError(Exception):
""""Transport-level error"""
"""Unused- only here for backcompat on things that import it"""
code = 500

def __init__(self, transport, message):
self.transport = transport
self.message = message

def __str__(self):
return '{0} {1}. {2}'.format(self.code, re.sub(
'Error$', '', self.__class__.__name__), self.message)
class WinRMOperationTimeoutError(Exception):
"""
Raised when a WinRM-level operation timeout (not a connection-level timeout) has occurred. This is
considered a normal error that should be retried transparently by the client when waiting for output from
a long-running process.
"""
code = 500

def __repr__(self):
return "{0}(code={1}, transport='{2}', message='{3}')".format(
self.__class__.__name__, self.code, self.transport, self.message)
class AuthenticationError(WinRMError):
"""Authorization Error"""
code = 401


class UnauthorizedError(WinRMTransportError):
"""Raise if the user is not authorized"""
code = 401
class BasicAuthDisabledError(AuthenticationError):
message = 'WinRM/HTTP Basic authentication is not enabled on remote host'


class TimeoutError(WinRMTransportError):
pass
class InvalidCredentialsError(AuthenticationError):
pass
Loading

0 comments on commit 1213eaf

Please sign in to comment.