from __future__ import unicode_literals
import logging
logger = logging.getLogger('awinrm')
handler = logging.StreamHandler()
formatter = logging.Formatter(
        '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False


import re
from base64 import b64encode
import xml.etree.ElementTree as ET
import warnings
import asyncio


from asysocks.unicomm.protocol.client.http.commons.factory import HTTPConnectionFactory
from awinrm.protocol import Protocol


class Session:
	def __init__(self, url:str, ssl_ctx = None, authtype='auto', factory:HTTPConnectionFactory = None, **kwargs):
		if factory is None:
			if url is None:
				raise Exception('Either url or factory parameter is required')
			factory = HTTPConnectionFactory.from_url(url)
		cred = factory.get_credential()
		target = factory.get_target()
		self.url = self._build_url(target.get_url(), kwargs.get('transport', 'plaintext'))
		self.protocol = Protocol(self.url, cred, ssl_ctx = ssl_ctx, authtype=authtype, proxies=target.proxies, **kwargs)
		self.__shells = []

	async def __aenter__(self):
		return self
	
	async def __aexit__(self, exc_type, exc, tb):
		for shell in self.__shells:
			await shell.close()
		await self.protocol.close()

	def create_shell(self, working_directory=None, env_vars=None, noprofile=False,
					codepage=437, lifetime=None, idle_timeout=None):
		shell = WinRMShell(self, working_directory=working_directory, env_vars=env_vars, noprofile=noprofile,
					codepage=codepage, lifetime=lifetime, idle_timeout=idle_timeout)
		self.__shells.append(shell)
		return shell

	async def run_cmd(self, command, args=()):
		shell_id = await self.protocol.open_shell()
		command_id = await self.protocol.run_command(shell_id, command, args)
		stdout_buff = b''
		stderr_buff = b''
		return_code = -1
		async for stdout, stderr, return_code in self.protocol.get_command_output(shell_id, command_id):
			stdout_buff += stdout
			stderr_buff += stderr
			return_code = return_code
		await self.protocol.cleanup_command(shell_id, command_id)
		await self.protocol.close_shell(shell_id)
		return stdout_buff, stderr_buff, return_code

	async 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)^((?P<scheme>http[s]?)://)?(?P<host>[0-9a-z-_.]+)(:(?P<port>\d+))?(?P<path>(/)?(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('/'))



class WinRMShell:
	def __init__(self, session:Session, working_directory:str=None, env_vars=None, noprofile:bool=False,
				codepage:int=437, lifetime:int=None, idle_timeout:int=None):
		self.session = session
		self.working_directory = working_directory
		self.env_vars = env_vars
		self.noprofile = noprofile
		self.codepage = codepage
		self.lifetime = lifetime
		self.idle_timeout = idle_timeout
		self.shell_cmd = 'cmd.exe'
		self.command_id = None
		self.closed_evt = asyncio.Event()

		self.shell_id = None
		self.stdin = asyncio.Queue()
		self.stdout = asyncio.Queue()
		self.stderr = asyncio.Queue()
		self.return_code = None

	async def __aenter__(self):
		try:
			self.shell_id = await self.session.protocol.open_shell(
				working_directory = self.working_directory,
				env_vars = self.env_vars,
				noprofile = self.noprofile,
				codepage = self.codepage,
				lifetime = self.lifetime,
				idle_timeout = self.idle_timeout
			)
			self.command_id = await self.session.protocol.run_command(self.shell_id, self.shell_cmd)
			stdout, stderr, return_code, command_done = await self.session.protocol._raw_get_command_output(self.shell_id, self.command_id)
			await self.stdout.put(stdout)
			await self.stderr.put(stderr)
			self.return_code = return_code

			return self
		except Exception as e:
			await self.__aexit__(None, None, None)
			raise e
	
	async def __aexit__(self, exc_type, exc, tb):
		await self.close()

	async def close(self):
		if self.closed_evt.is_set():
			return
		await self.session.protocol.cleanup_command(self.shell_id, self.command_id)
		await self.session.protocol.close_shell(self.shell_id)
		self.closed_evt.set()

	async def send_input(self, data):
		await self.session.protocol.send_command_input(self.shell_id, self.command_id, data)
		await self.read_output()

	async def read_output(self):
		stdout, stderr, return_code, command_done = await self.session.protocol._raw_get_command_output(self.shell_id, self.command_id)
		await self.stdout.put(stdout)
		await self.stderr.put(stderr)
		self.return_code = return_code


def decode_bytes(data:bytes, hint:str='cp437'):
	encodings = ['utf-8', 'cp1252', 'cp1251', 'cp932', 'cp936']
	if hint is not None and len(hint) > 0:
		encodings.insert(0, hint)
	for encoding in encodings:
		try:
			return data.decode(encoding)
		except UnicodeDecodeError:
			continue
	raise UnicodeDecodeError("Unable to decode the input data with the provided encodings.")

awinrm/__init__.py
skelsec/awinrm

__version__ = "0.0.1"
__banner__ = \
"""
# awinrm %s 
# Author: Tamas Jos @skelsec (info@skelsecprojects.com)
""" % __version__
awinrm/_version.py
skelsec/awinrm
import re
import struct
from awinrm import logger
from awinrm.exceptions import WinRMError
from urllib.parse import urlsplit

from asyauth.common.credentials.ntlm import NTLMCredential
from asyauth.common.credentials.kerberos import KerberosCredential
from asyauth.common.credentials.spnego import SPNEGOCredential
from asyauth.common.credentials.credssp import CREDSSPCredential

class Encryption(object):

    SIXTEN_KB = 16384
    MIME_BOUNDARY = b'--Encrypted Boundary'

    def __init__(self, session, protocol:str):
        """
        [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.lower()
        self.session = session
        self.sequence_number = 0
        logger.debug('Using encryption protocol: %s' % self.protocol)
        cred = self.session.authmanager.authobj.get_active_credential()
        if self.protocol == 'spnego':  # Details under Negotiate [2.2.9.1.1] in MS-WSMV
            self.protocol_string = b"application/HTTP-SPNEGO-session-encrypted"
            if isinstance(cred, NTLMCredential):
                self._build_message = self._build_ntlm_message
                self._decrypt_message = self._decrypt_ntlm_message
            elif isinstance(cred, KerberosCredential):
                self._build_message = self._build_kerberos_message
                self._decrypt_message = self._decrypt_kerberos_message

        elif self.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
            
        else:
            raise WinRMError("Encryption for protocol '%s' not supported in awinrm" % self.protocol)

    async def prepare_encrypted_request(self, endpoint, message):
        """
        Creates a prepared request to send to the server with an encrypted message
        and correct headers

        :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 = await self._encrypt_message(message_chunk, host)
                encrypted_message += encrypted_chunk
        else:
            content_type = 'multipart/encrypted'
            encrypted_message = await self._encrypt_message(message, host)
        encrypted_message += self.MIME_BOUNDARY + b"--\r\n"
        
        headers = []
        headers.append(('Content-Type', '{0};protocol="{1}";boundary="Encrypted Boundary"'\
            .format(content_type, self.protocol_string.decode())))

        return encrypted_message, headers

    async def parse_encrypted_response(self, response, data):
        """
        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.getheaders('content-type')
        if content_type is None:
            return data
        content_type = content_type[0]
        if 'protocol="{0}"'.format(self.protocol_string.decode()) in content_type:
            host = urlsplit(response.url).hostname
            msg = await self._decrypt_response(host, data)
        else:
            msg = data

        return msg

    async def _encrypt_message(self, message, host):
        message_length = str(len(message)).encode()
        encrypted_stream = await 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

    async def _decrypt_response(self, host, data):
        parts = data.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 = await 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

    async def _decrypt_ntlm_message(self, encrypted_data, host):
        message = await self.session.authmanager.authobj.decrypt(encrypted_data[4:], None)
        return message[0]

    async def _decrypt_credssp_message(self, encrypted_data, host):
        message, signature = await self.session.authmanager.authobj.decrypt(encrypted_data[4:], None)
        return message

    async def _decrypt_kerberos_message(self, encrypted_data, host):
        signature_length = struct.unpack("<i", encrypted_data[:4])[0]
        signature = b'\x00'*8 + encrypted_data[4:signature_length + 4]
        encrypted_message = encrypted_data[signature_length + 4:]

        message, _ = await self.session.authmanager.authobj.decrypt(encrypted_message, None, auth_data=signature)
        return message

    async def _build_ntlm_message(self, message, host):
        sealed_message, signature = await self.session.authmanager.authobj.encrypt(message, self.sequence_number)
        signature_length = struct.pack("<i", len(signature))
        self.sequence_number += 1
        return signature_length + signature + sealed_message

    async def _build_credssp_message(self, message, host):
        sealed_message, _ = await self.session.authmanager.authobj.encrypt(message, self.sequence_number) ##TODO: Check if this is correct
        cipher_negotiated = self.session.authmanager.authobj.get_cipher_name()
        trailer_length = self._get_credssp_trailer_length(len(message), cipher_negotiated)
        return struct.pack("<i", trailer_length) + sealed_message

    async def _build_kerberos_message(self, message, host):
        self.sequence_number = 0
        sealed_message, signature = await self.session.authmanager.authobj.encrypt(message, self.sequence_number) ##TODO: Check if this is correct
        self.sequence_number += 1
        signature_length = struct.pack("<i", len(signature))
        return signature_length + signature + sealed_message

    def _get_credssp_trailer_length(self, message_length, cipher_suite):
        # I really don't like the way this works but can't find a better way, MS
        # allows you to get this info through the struct SecPkgContext_StreamSizes
        # but there is no GSSAPI/OpenSSL equivalent so we need to calculate it
        # ourselves

        if re.match(r'^.*-GCM-[\w\d]*$', cipher_suite):
            # We are using GCM for the cipher suite, GCM has a fixed length of 16
            # bytes for the TLS trailer making it easy for us
            trailer_length = 16
        else:
            # We are not using GCM so need to calculate the trailer size. The
            # trailer length is equal to the length of the hmac + the length of the
            # padding required by the block cipher
            hash_algorithm = cipher_suite.split('-')[-1]

            # while there are other algorithms, SChannel doesn't support them
            # as of yet https://msdn.microsoft.com/en-us/library/windows/desktop/aa374757(v=vs.85).aspx
            if hash_algorithm == 'MD5':
                hash_length = 16
            elif hash_algorithm == 'SHA':
                hash_length = 20
            elif hash_algorithm == 'SHA256':
                hash_length = 32
            elif hash_algorithm == 'SHA384':
                hash_length = 48
            else:
                hash_length = 0

            pre_pad_length = message_length + hash_length

            if "RC4" in cipher_suite:
                # RC4 is a stream cipher so no padding would be added
                padding_length = 0
            elif "DES" in cipher_suite or "3DES" in cipher_suite:
                # 3DES is a 64 bit block cipher
                padding_length = 8 - (pre_pad_length % 8)
            else:
                # AES is a 128 bit block cipher
                padding_length = 16 - (pre_pad_length % 16)

            trailer_length = (pre_pad_length + padding_length) - message_length

        return trailer_length
awinrm/encryption.py
skelsec/awinrm
import asyncio
from awinrm import Session
from asysocks.unicomm.protocol.client.http.client import ClientSession

async def check_auth(url):
	async with ClientSession() as session:
		async with session.post(url) as resp:
			if resp.status == 401:
				return resp.get_all('www-authenticate', [])
			return ['NOAUTH']

async def amain(url):
	transport = 'ssl'
	if url.startswith('http://'):
		transport = 'plaintext'
	url = Session._build_url(url, transport)
	print('[+] Testing URL: ' + url)
	headers = await check_auth(url)
	for entry in headers:
		print('[+] Authenthod: %s' % entry)


def main():
	import argparse

	parser = argparse.ArgumentParser(description='WinRM - Enumerate authentication types supported by the remote server')
	parser.add_argument('url', type=str, help = 'URL to connect to. Must start with http:// or https://')
	args = parser.parse_args()
	
	asyncio.run(amain(args.url))

if __name__ == '__main__':
	main()
awinrm/examples/authcheck.py
skelsec/awinrm
import sys
import asyncio
import traceback
from awinrm import Session, decode_bytes
from awinrm import logger

async def amain(url, command, authtype):
	async with Session(url, authtype=authtype) as session:
		try:
			stdout, stderr, return_code = await session.run_cmd(command)
			if len(stdout) > 0:
				print(decode_bytes(stdout))
			if len(stderr) > 0:
				print(decode_bytes(stderr), file=sys.stderr)
			sys.exit(return_code)
		except Exception as e:
			traceback.print_exc()
			print('[-] Error: %s' % str(e))
			sys.exit(1)

def main():
	import argparse
	import logging
	from asyauth import logger as authlogger
	from asysocks import logger as sockslogger

	parser = argparse.ArgumentParser(description='WinRM - Execute a single shell command remotely')
	parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity')
	parser.add_argument('-c', '--command', type=str, default = 'ipconfig /all', help = 'Shell command to execute')
	parser.add_argument('-a', '--authproto', choices=['spnego', 'credssp'], default = 'spnego', help = 'Authentication protocol to use')
	parser.add_argument('url', type=str, help = 'URL to connect to')
	args = parser.parse_args()

	if args.verbose == 0:
		logging.basicConfig(level=logging.INFO)
		logger.setLevel(logging.ERROR)
		authlogger.setLevel(logging.ERROR)
		sockslogger.setLevel(logging.ERROR)
	else:
		logging.basicConfig(level=logging.DEBUG)
		logger.setLevel(logging.DEBUG)
		authlogger.setLevel(logging.DEBUG)
		sockslogger.setLevel(logging.DEBUG)
	
	asyncio.run(amain(args.url, args.command, args.authproto))

if __name__ == '__main__':
	main()
awinrm/examples/runcmd.py
skelsec/awinrm
import sys
import asyncio
import traceback
from awinrm import Session, decode_bytes
from awinrm import logger

import aioconsole

async def print_output(shell):
	while True:
		try:
			stdout = await shell.stdout.get()
			if len(stdout) > 0:
				print(decode_bytes(stdout), end='')
			stderr = await shell.stderr.get()
			if len(stderr) > 0:
				print(decode_bytes(stderr), end='', file=sys.stderr)
		except asyncio.CancelledError:
			break
		except Exception as e:
			traceback.print_exc()
			print('[-] Error: %s' % str(e))
			sys.exit(1)

async def amain(url, authtype):
	async with Session(url, authtype=authtype) as session:
		async with session.create_shell() as shell:
			try:
				x = asyncio.create_task(print_output(shell))
				while True:
					user_input = await aioconsole.ainput("")
					await shell.send_input(user_input + '\r\n')
			except Exception as e:
				traceback.print_exc()
				print('[-] Error: %s' % str(e))
				sys.exit(1)
	

def main():
	import argparse
	import logging
	from asyauth import logger as authlogger

	parser = argparse.ArgumentParser(description='WinRM - Execute a single shell command remotely')
	parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity')
	parser.add_argument('-a', '--authproto', choices=['spnego', 'credssp'], default = 'spnego', help = 'Authentication protocol to use')
	parser.add_argument('url', type=str, help = 'URL to connect to')
	args = parser.parse_args()

	if args.verbose == 0:
		logging.basicConfig(level=logging.INFO)
		logger.setLevel(logging.ERROR)
		authlogger.setLevel(logging.ERROR)
	else:
		logging.basicConfig(level=logging.DEBUG)
		logger.setLevel(logging.DEBUG)
		authlogger.setLevel(logging.DEBUG)
	
	asyncio.run(amain(args.url, args.authproto))

if __name__ == '__main__':
	main()
awinrm/examples/shell.py
skelsec/awinrm
"""Contains client side logic of WinRM SOAP protocol implementation"""
from __future__ import unicode_literals
import base64
import uuid
import asyncio

import xml.etree.ElementTree as ET
import xmltodict
import traceback

from six import text_type

from awinrm.transport import Transport
from awinrm.exceptions import WinRMError, WinRMTransportError, WinRMOperationTimeoutError

xmlns = {
    'soapenv': 'http://www.w3.org/2003/05/soap-envelope',
    'soapaddr': 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
    'wsmanfault': "http://schemas.microsoft.com/wbem/wsman/1/wsmanfault"
}


class Protocol(object):
    """This is the main class that does the SOAP request/response logic. There
    are a few helper classes, but pretty much everything comes through here
    first.
    """
    DEFAULT_READ_TIMEOUT_SEC = 30
    DEFAULT_OPERATION_TIMEOUT_SEC = 20
    DEFAULT_MAX_ENV_SIZE = 153600
    DEFAULT_LOCALE = 'en-US'

    def __init__(
            self, endpoint, credential, proxies, ssl_ctx=None, service="HTTP",
            read_timeout_sec=DEFAULT_READ_TIMEOUT_SEC,
            operation_timeout_sec=DEFAULT_OPERATION_TIMEOUT_SEC,
            message_encryption='auto', authtype='auto'
        ):
        """
        @param string endpoint: the WinRM webservice endpoint
        @param string transport: transport type, one of 'plaintext' (default), 'kerberos', 'ssl', 'ntlm', 'credssp'  # NOQA
        @param string username: username
        @param string password: password
        @param string realm: unused
        @param string service: the service name, default is HTTP
        @param string keytab: the path to a keytab file if you are using one
        @param string ca_trust_path: Certification Authority trust path. If server_cert_validation is set to 'validate':
                                        'legacy_requests'(default) to use environment variables,
                                        None to explicitly disallow any additional CA trust path
                                        Any other value will be considered the CA trust path to use.
        @param string cert_pem: client authentication certificate file path in PEM format  # NOQA
        @param string cert_key_pem: client authentication certificate key file path in PEM format  # NOQA
        @param string server_cert_validation: whether server certificate should be validated on Python versions that support it; one of 'validate' (default), 'ignore' #NOQA
        @param bool kerberos_delegation: if True, TGT is sent to target server to allow multiple hops  # NOQA
        @param int read_timeout_sec: maximum seconds to wait before an HTTP connect/read times out (default 30). This value should be slightly higher than operation_timeout_sec, as the server can block *at least* that long. # NOQA
        @param int operation_timeout_sec: maximum allowed time in seconds for any single wsman HTTP operation (default 20). Note that operation timeouts while receiving output (the only wsman operation that should take any significant time, and where these timeouts are expected) will be silently retried indefinitely. # NOQA
        @param string kerberos_hostname_override: the hostname to use for the kerberos exchange (defaults to the hostname in the endpoint URL)
        @param bool message_encryption_enabled: Will encrypt the WinRM messages if set to True and the transport auth supports message encryption (Default True).
        @param string proxy: Specify a proxy for the WinRM connection to use. 'legacy_requests'(default) to use environment variables, None to disable proxies completely or the proxy URL itself.
        """

        try:
            read_timeout_sec = int(read_timeout_sec)
        except ValueError as ve:
            raise ValueError("failed to parse read_timeout_sec as int: %s" % str(ve))

        try:
            operation_timeout_sec = int(operation_timeout_sec)
        except ValueError as ve:
            raise ValueError("failed to parse operation_timeout_sec as int: %s" % str(ve))

        if operation_timeout_sec >= read_timeout_sec or operation_timeout_sec < 1:
            raise WinRMError("read_timeout_sec must exceed operation_timeout_sec, and both must be non-zero")

        self.read_timeout_sec = read_timeout_sec
        self.operation_timeout_sec = operation_timeout_sec
        self.max_env_sz = Protocol.DEFAULT_MAX_ENV_SIZE
        self.locale = Protocol.DEFAULT_LOCALE

        self.transport = Transport(
            endpoint=endpoint,
            credential=credential,
            ssl_ctx=ssl_ctx,
            read_timeout_sec=self.read_timeout_sec,
            authtype = authtype,
            proxies = proxies,
        )

        self.service = service
    
    async def close(self):
        await self.transport.close_session()

    async 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 = await 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

    async def send_message(self, message):
        # TODO add message_id vs relates_to checking
        # TODO port error handling code
        try:
            resp = await self.transport.send_message(message)
            return resp
        except WinRMTransportError as ex:
            traceback.print_exc()
            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))

    async 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 = await 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:
                await self.transport.close_session()

        # TODO change assert into user-friendly exception
        assert uuid.UUID(relates_to.replace('uuid:', '')) == message_id

    async 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 = await 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

    async 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 = await 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

    async 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
        """
        try:
            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)
            res = await self.send_message(xmltodict.unparse(req))
            print('Res: %s' % repr(res))
        except Exception as e:
            traceback.print_exc()

    async 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.
        """
        command_done = False
        while not command_done:
            try:
                stdout, stderr, return_code, command_done = \
                    await self._raw_get_command_output(shell_id, command_id)
                yield stdout, stderr, return_code
            except WinRMOperationTimeoutError:
                # this is an expected error when waiting for a long-running process, just silently retry
                pass

    async 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 = await 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...
        #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
        #   to...
        #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
        #     <rsp:ExitCode>0</rsp:ExitCode>
        #   </rsp:CommandState>
        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
awinrm/protocol.py
skelsec/awinrm
from __future__ import unicode_literals

from awinrm import logger
from awinrm.exceptions import InvalidCredentialsError, WinRMError, WinRMTransportError
from awinrm.encryption import Encryption
from asysocks.unicomm.protocol.client.http.client import ClientSession

DISPLAYED_PROXY_WARNING = False
DISPLAYED_CA_TRUST_WARNING = False

unicode_type = type(u'')

__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, 
            credential,
            authtype='auto',
            ssl_ctx=None,
            read_timeout_sec=None,
            proxies=None):
        self.credential = credential
        self.ssl_ctx = ssl_ctx
        self.endpoint = endpoint
        self.read_timeout_sec = read_timeout_sec
        self.authtype = authtype
        self.server_supported_authtypes = None
        self.default_headers = {
            'Content-Type': 'application/soap+xml;charset=UTF-8',
            'User-Agent': 'Python WinRM client',
        }
        self.proxies = proxies

        self.session = None
        self.encryption = None

    async def build_session(self):
        async with ClientSession(proxies = self.proxies) as session:
            session.static_headers.update(self.default_headers)
            async with session.post(self.endpoint, data=None) as response:
                respdata = await response.read()
                if response.status == 401:
                    self.server_supported_authtypes = response.get_all('www-authenticate', [])
                    logger.debug('Server supported auth types: %s' % self.server_supported_authtypes)
                    if self.authtype == 'auto':
                        if 'Negotiate' in self.server_supported_authtypes:
                            self.authtype = 'spnego'
                        elif 'CredSSP' in self.server_supported_authtypes:
                            self.authtype = 'credssp'
                        else:
                            raise WinRMError('No supported authentication types available on the server')
                    logger.debug('Using auth type: %s' % self.authtype)

                elif response.status == 200:
                    raise WinRMTransportError('http', response.status, 'Server doesn\'t require authentication. This is unexpected!')
                
        session = ClientSession(credential=self.credential, ssl_ctx=self.ssl_ctx, force_sinle_connection=True, auth_type=self.authtype,proxies = self.proxies)
        session.static_headers.update(self.default_headers)
        self.session = session
        await self.setup_encryption()

    async def setup_encryption(self):
        try:
            # Security context doesn't exist, sending blank message to initialise context
            async with self.session.post(self.endpoint, data=None) as response:
                if response.status != 200:
                    respdata = await response.read()
                    raise WinRMTransportError('http', response.status, respdata)
            
            self.encryption = Encryption(self.session, self.authtype)
            self.encryption.sequence_number = self.session.authmanager.authobj.get_seq_number()
        except Exception as e:
            raise e

    async def close_session(self):
        if not self.session:
            return
        await self.session.close()
        self.session = None

    async def send_message(self, message):
        if not self.session:
            await 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')

        headers = []
        if self.encryption:
            message, headers = await self.encryption.prepare_encrypted_request(self.endpoint, message)

        try:
            async with self.session.post(self.endpoint, data=message, headers=headers) as response:
                if response.status != 200:
                    errtext = await response.read()
                    if self.encryption:
                        errtext = await self.encryption.parse_encrypted_response(response, errtext)
                    raise WinRMTransportError('http', response.status, errtext)

                data = await response.read()
                return await self._get_message_response_text(response, data)
        except Exception as e:
            raise e

    async def _get_message_response_text(self, response, data):
        if self.encryption:
            response_text = await self.encryption.parse_encrypted_response(response, data)
        else:
            response_text = data
        return response_text
awinrm/transport.py
skelsec/awinrm
from setuptools import setup, find_packages
import re

VERSIONFILE="awinrm/_version.py"
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
    verstr = mo.group(1)
else:
    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))

setup(
	# Application name:
	name="awinrm",

	# Version number (initial):
	version=verstr,

	# Application author details:
	author="Tamas Jos",
	author_email="info@skelsecprojects.com",

	# Packages
	packages=find_packages(exclude=["tests*"]),

	# Include additional files into the package
	include_package_data=True,

	# Details
	url="https://github.com/skelsec/awinrm",

	zip_safe = False,
	#
	license='MIT license',
	description='Asynchronous Python library for Windows Remote Management',
	long_description='Asynchronous Python library for Windows Remote Management',

	python_requires='>=3.7',
	classifiers=[
		"Programming Language :: Python :: 3.7",
		"Programming Language :: Python :: 3.8",
		"License :: OSI Approved :: MIT License",
		"Operating System :: OS Independent",
	],
	install_requires=[
		'unicrypto==0.0.10',
		'asysocks>=0.2.10',
        'asyauth>=0.0.14',
		'six',
        'xmltodict',
	],

    entry_points={
		'console_scripts': [
			'awinrm-runcmd = awinrm.examples.runcmd:main',
            'awinrm-cmdshell = awinrm.examples.shell:main',
		],
	}
)
setup.py
skelsec/awinrm