diff --git a/.gitignore b/.gitignore index 7d6a3ec9..61a93d79 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ __pycache__ /winrm/tests/config.json .pytest_cache venv +.tox diff --git a/requirements-test.txt b/requirements-test.txt index aa82eab0..ff85389b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,3 +4,4 @@ pytest==4.4.2 pytest-cov==2.7.1 pytest-flake8==1.0.4 mock==3.0.5 +requests_mock==1.5.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c1a6340d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +xmltodict +requests>=2.9.1 +requests_ntlm>=0.3.0 +six>=1.7.0 \ No newline at end of file diff --git a/requirements/extras/requirements-credssp.txt b/requirements/extras/requirements-credssp.txt new file mode 100644 index 00000000..f6b73ec7 --- /dev/null +++ b/requirements/extras/requirements-credssp.txt @@ -0,0 +1 @@ +requests-credssp>=1.0.0 diff --git a/requirements/extras/requirements-kerberos.txt b/requirements/extras/requirements-kerberos.txt new file mode 100644 index 00000000..ede9cd09 --- /dev/null +++ b/requirements/extras/requirements-kerberos.txt @@ -0,0 +1,2 @@ +winkerberos>=0.5.0 ; sys_platform=="win32" +pykerberos>=1.2.1,<2.0.0 ; sys_platform!="win32" \ No newline at end of file diff --git a/requirements/requirements-testmin.txt b/requirements/requirements-testmin.txt new file mode 100644 index 00000000..c8d5f9e0 --- /dev/null +++ b/requirements/requirements-testmin.txt @@ -0,0 +1,13 @@ +xmltodict==0.3.0 +requests==2.9.1 +requests_ntlm==0.3.0 +six==1.7.0 + +# credssp +requests-credssp==1.0.0 + +# kerberos +winkerberos==0.5.0 ; sys_platform=="win32" +pykerberos==1.2.1 ; sys_platform!="win32" + +-r ../requirements-test.txt \ No newline at end of file diff --git a/setup.py b/setup.py index b71ec2f2..05b59d05 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +import os + from setuptools import setup __version__ = '0.3.1.dev0' @@ -11,6 +13,29 @@ except ImportError: long_description = '' + +def install_deps(): + default = open('requirements.txt', 'r').readlines() + pkg_list = [] + for resource in default: + pkg_list.append(resource.strip()) + return pkg_list + + +def install_extras(): + extras = dict() + for filename in os.listdir('requirements/extras'): + extra_key = filename.replace('requirements-', '').replace('.txt', '') + extras[extra_key] = list() + with open("requirements/extras/" + filename, 'r') as req_file: + for pkg_name in req_file.readlines(): + extras[extra_key].append(pkg_name) + return extras + + +req_deps_list = install_deps() +extras_deps = install_extras() + setup( name=project_name, version=__version__, @@ -23,12 +48,8 @@ license='MIT license', packages=('winrm', 'winrm.tests', 'winrm.vendor.requests_kerberos'), package_data={'winrm.tests': ['*.ps1']}, - install_requires=['xmltodict', 'requests>=2.9.1', 'requests_ntlm>=0.3.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'] - }, + install_requires=req_deps_list, + extras_require=extras_deps, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..2bc61218 --- /dev/null +++ b/tox.ini @@ -0,0 +1,67 @@ +[tox] +envlist = py27,py35,py36,py37,py38,py27min,py37min,py27base,py37base +skip_missing_interpreters = True +skipsdist = True +sitepackages = False + +[testenv] +ignore_errors = False +deps = + -r{toxinidir}/requirements-test.txt + -r{toxinidir}/requirements/extras/requirements-credssp.txt + -r{toxinidir}/requirements/extras/requirements-kerberos.txt + -r{toxinidir}/requirements.txt +setenv = + PYWINRM_TEST_CREDSSP=1 + PYWINRM_TEST_KERBEROS=1 +commands = + pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ +whitelist_externals = bash + +# Test the base install, without extras. +[testenv:py27base] +ignore_errors = False +deps = + -r{toxinidir}/requirements-test.txt + -r{toxinidir}/requirements.txt +setenv = + PYWINRM_TEST_CREDSSP=0 + PYWINRM_TEST_KERBEROS=0 +commands = + pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ +whitelist_externals = bash + +[testenv:py37base] +ignore_errors = False +deps = + -r{toxinidir}/requirements-test.txt + -r{toxinidir}/requirements.txt +setenv = + PYWINRM_TEST_CREDSSP=0 + PYWINRM_TEST_KERBEROS=0 +commands = + pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ +whitelist_externals = bash + +# Test the minimum requirements for the packages. +[testenv:py27min] +ignore_errors = False +deps = + -r{toxinidir}/requirements/requirements-testmin.txt +setenv = + PYWINRM_TEST_CREDSSP=1 + PYWINRM_TEST_KERBEROS=1 +commands = + pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ +whitelist_externals = bash + +[testenv:py37min] +ignore_errors = False +deps = + -r{toxinidir}/requirements/requirements-testmin.txt +setenv = + PYWINRM_TEST_CREDSSP=1 + PYWINRM_TEST_KERBEROS=1 +commands = + pytest -v --flake8 --cov=winrm --cov-report=term-missing winrm/tests/ +whitelist_externals = bash diff --git a/winrm/tests/base.py b/winrm/tests/base.py new file mode 100644 index 00000000..0eafc277 --- /dev/null +++ b/winrm/tests/base.py @@ -0,0 +1,45 @@ +import os +import unittest +import requests_mock +from winrm import transport + +if os.environ.get('PYWINRM_TEST_CREDSSP') == '1': + EXPECT_CREDSSP = True +elif os.environ.get('PYWINRM_TEST_CREDSSP') == '0': + EXPECT_CREDSSP = False +else: + EXPECT_CREDSSP = transport.HAVE_CREDSSP + +if os.environ.get('PYWINRM_TEST_KERBEROS') == '1': + EXPECT_KERBEROS = True +elif os.environ.get('PYWINRM_TEST_KERBEROS') == '0': + EXPECT_KERBEROS = False +else: + EXPECT_KERBEROS = transport.HAVE_KERBEROS + + +class BaseTest(unittest.TestCase): + maxDiff = 2048 + _old_env = None + + def setUp(self): + super(BaseTest, self).setUp() + self.mocked_request = requests_mock.Mocker() + self.mocked_request.start() + 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) + + def tearDown(self): + super(BaseTest, 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) + self.mocked_request.stop() diff --git a/winrm/tests/test_protocol.py b/winrm/tests/test_protocol.py index bcea47ea..98084ce6 100644 --- a/winrm/tests/test_protocol.py +++ b/winrm/tests/test_protocol.py @@ -1,8 +1,22 @@ import pytest +import copy +import xmltodict +from winrm.tests import base as base_test +from winrm.tests.winrm_responses import shells as shell_responses from winrm.protocol import Protocol +def convert_to_dict(ordered_dict): + new_dict = dict() + for name, value in ordered_dict.items(): + if isinstance(value, dict): + new_dict[name] = convert_to_dict(value) + else: + new_dict[name] = value + return new_dict + + def test_open_shell_and_close_shell(protocol_fake): shell_id = protocol_fake.open_shell() assert shell_id == '11111111-1111-1111-1111-111111111113' @@ -71,3 +85,23 @@ def test_fail_set_operation_timeout_as_sec(): operation_timeout_sec='29a') assert str(exc.value) == "failed to parse operation_timeout_sec as int: " \ "invalid literal for int() with base 10: '29a'" + + +class TestTransportShells(base_test.BaseTest): + + def test_open_shell(self): + self.mocked_request.post('https://example.com', text=shell_responses.OPEN_SHELL_RESPONSE) + server_conn = Protocol(endpoint="https://example.com", + username='test', + password='test', + ) + response = server_conn.open_shell() + self.assertEqual('5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE', response) + + # MessageID will be dynamic, no need to compare + expected_shell_request = xmltodict.parse(copy.deepcopy(shell_responses.OPEN_SHELL_REQUEST)) + actual_shell_request = xmltodict.parse(copy.deepcopy(self.mocked_request.request_history[0].body)) + del expected_shell_request['env:Envelope']['env:Header']['a:MessageID'] + del actual_shell_request['env:Envelope']['env:Header']['a:MessageID'] + + self.assertEqual(convert_to_dict(expected_shell_request), convert_to_dict(actual_shell_request)) diff --git a/winrm/tests/test_transport.py b/winrm/tests/test_transport.py index cc451f3f..6ab15f55 100644 --- a/winrm/tests/test_transport.py +++ b/winrm/tests/test_transport.py @@ -2,36 +2,23 @@ import os import mock import unittest +import requests +from . import base as base_test +from distutils.version import StrictVersion from winrm import transport from winrm.exceptions import WinRMError, InvalidCredentialsError +REQUEST_VERSION = requests.__version__.split('.') -class TestTransport(unittest.TestCase): - maxDiff = 2048 - _old_env = None + +class TestTransport(base_test.BaseTest): 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', @@ -144,6 +131,24 @@ def test_build_session_cert_ignore_2(self): t_default.build_session() self.assertIs(False, t_default.session.verify) + # TODO: I am not sure in which version changed specifically, but this can be updated if we need to find out. + @unittest.skipIf(StrictVersion(requests.__version__) > StrictVersion('2.9.1'), reason="Skipping for versions 2.9.1 or older") + def test_build_session_proxy_none_old_request(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': '*', 'http': 'random_proxy', 'https': 'random_proxy_2'}, t_default.session.proxies) + + @unittest.skipIf(StrictVersion(requests.__version__) <= StrictVersion('2.9.1'), reason="Skipping for versions newer than 2.9.1") def test_build_session_proxy_none(self): os.environ['HTTP_PROXY'] = 'random_proxy' os.environ['HTTPS_PROXY'] = 'random_proxy_2' @@ -308,3 +313,49 @@ def test_close_session_not_built(self, mock_session): t_default.close_session() self.assertFalse(mock_session.return_value.close.called) self.assertIsNone(t_default.session) + + +class TestTransportCredSSP(base_test.BaseTest): + + @unittest.skipIf(base_test.EXPECT_CREDSSP is False, reason="Only testing when CredSSP is available") + def test_with_credssp(self): + t_default = transport.Transport(endpoint="https://example.com", + username='test', + password='test', + auth_method='credssp', + ) + t_default.build_session() + + @unittest.skipIf(base_test.EXPECT_CREDSSP is True, reason="Only testing when CredSSP is unavailable") + def test_without_credssp(self): + t_default = transport.Transport(endpoint="https://example.com", + username='test', + password='test', + auth_method='credssp', + ) + with self.assertRaises(WinRMError) as exc: + t_default.build_session() + self.assertEqual(str(exc.exception), 'requests auth method is credssp, but requests-credssp is not installed') + + +class TestTransportKerberos(base_test.BaseTest): + + @unittest.skipIf(base_test.EXPECT_KERBEROS is False, reason="Only testing when kerberos is available") + def test_with_kerberos(self): + t_default = transport.Transport(endpoint="https://example.com", + username='test', + password='test', + auth_method='kerberos', + ) + t_default.build_session() + + @unittest.skipIf(base_test.EXPECT_KERBEROS is True, reason="Only testing when kerberos is unavailable") + def test_without_kerberos(self): + t_default = transport.Transport(endpoint="https://example.com", + username='test', + password='test', + auth_method='kerberos', + ) + with self.assertRaises(WinRMError) as exc: + t_default.build_session() + self.assertEqual(str(exc.exception), 'requested auth method is kerberos, but requests_kerberos is not installed') diff --git a/winrm/tests/winrm_responses/__init__.py b/winrm/tests/winrm_responses/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/winrm/tests/winrm_responses/shells.py b/winrm/tests/winrm_responses/shells.py new file mode 100644 index 00000000..bf24353d --- /dev/null +++ b/winrm/tests/winrm_responses/shells.py @@ -0,0 +1,147 @@ +OPEN_SHELL_REQUEST = """ + + + PT20S + http://windows-host:5985/wsman + + FALSE + 437 + + 153600 + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Create + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + uuid:63acc10b-c16f-44e6-a1f8-45798c58f63d + + + + + stdin + stdout stderr + + + +""" + +OPEN_SHELL_RESPONSE = """ + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse + uuid:BF99E0E5-4604-4FBE-A3F9-3C2251E2655E + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:63acc10b-c16f-44e6-a1f8-45798c58f63d + + + + http://windows-host:5985/wsman + + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + + 5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE + + + + + 5207F2DF-E6CA-4D10-8C7F-5380F01D6FDE + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + SERVER-01\\Administrator + 192.168.0.1 + PT7200.000S + stdin + stdout stderr + P0DT0H0M0S + P0DT0H0M0S + + + +""" + +CLOSE_SHELL_REQUEST = """ + + + + PT20S + http://windows-host:5985/wsman + + B5235B70-E451-4378-BFB4-53C1ABACCAD2 + + 153600 + http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd + http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete + + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + uuid:5fc69899-bbc7-4179-a7a2-f6bb62fe2860 + + + + +""" + +CLOSE_SHELL_RESPONSE = """ + + + http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse + uuid:C20C16C3-D163-45B4-9821-8A11C4ED1E42 + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:5fc69899-bbc7-4179-a7a2-f6bb62fe2860 + + + +""" + +# Status code: 500 +CLOSE_SHELL_RESPONSE_ALREADY_CLOSED = """ + + + http://schemas.dmtf.org/wbem/wsman/1/wsman/fault + uuid:C0005351-7407-4B75-BE76-2A80BA45B7BF + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + uuid:fe945306-d7ce-4126-b168-c704bad7588c + + + + + s:Sender + + w:InvalidSelectors + + + + The WS-Management service cannot process the request because the request contained invalid selectors for the + resource. + + + + http://schemas.dmtf.org/wbem/wsman/1/wsman/faultDetail/UnexpectedSelectors + + The request for the Windows Remote Shell with ShellId B387C9DF-58ED-4E97-AE80-4117D8D12116 failed because the shell was not found + on the server. Possible causes are: the specified ShellId is incorrect or the shell no longer exists on the server. Provide the correct + ShellId or create a new shell and retry the operation. + + + + + + +"""