pypsrp 0.8.1__py3-none-any.whl → 0.9.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypsrp/_utils.py CHANGED
@@ -107,7 +107,6 @@ def get_pwsh_script(name: str) -> str:
107
107
  block_comment = False
108
108
  new_lines = []
109
109
  for line in script.splitlines():
110
-
111
110
  line = line.strip()
112
111
  if block_comment:
113
112
  block_comment = not line.endswith("#>")
pypsrp/client.py CHANGED
@@ -15,7 +15,6 @@ import warnings
15
15
  import xml.etree.ElementTree as ET
16
16
 
17
17
  from pypsrp._utils import get_pwsh_script, to_bytes, to_unicode
18
- from pypsrp.complex_objects import ComplexObject
19
18
  from pypsrp.exceptions import WinRMError
20
19
  from pypsrp.powershell import (
21
20
  DEFAULT_CONFIGURATION_NAME,
pypsrp/complex_objects.py CHANGED
@@ -139,7 +139,7 @@ class Enum(ComplexObject):
139
139
  self.value = kwargs.get("value")
140
140
 
141
141
  @property # type: ignore[override]
142
- def _to_string(self) -> str: # type: ignore[override]
142
+ def _to_string(self) -> str:
143
143
  try:
144
144
  return self._string_map[self.value or 0]
145
145
  except KeyError as err:
@@ -378,7 +378,7 @@ class RemoteStreamOptions(Enum):
378
378
  )
379
379
 
380
380
  @property # type: ignore[override]
381
- def _to_string(self) -> str: # type: ignore[override]
381
+ def _to_string(self) -> str:
382
382
  if self.value == 15:
383
383
  return "AddInvocationInfo"
384
384
 
@@ -1149,7 +1149,7 @@ class CommandType(Enum):
1149
1149
  super(CommandType, self).__init__("System.Management.Automation.CommandTypes", {}, **kwargs)
1150
1150
 
1151
1151
  @property # type: ignore[override]
1152
- def _to_string(self) -> str: # type: ignore[override]
1152
+ def _to_string(self) -> str:
1153
1153
  if self.value == 0x01FF:
1154
1154
  return "All"
1155
1155
 
pypsrp/encryption.py CHANGED
@@ -10,7 +10,6 @@ log = logging.getLogger(__name__)
10
10
 
11
11
 
12
12
  class WinRMEncryption(object):
13
-
14
13
  SIXTEEN_KB = 16384
15
14
  MIME_BOUNDARY = "--Encrypted Boundary"
16
15
  CREDSSP = "application/HTTP-CredSSP-session-encrypted"
pypsrp/messages.py CHANGED
@@ -127,7 +127,8 @@ class Message(object):
127
127
  ):
128
128
  msg = ET.Element("S")
129
129
  else:
130
- msg = self._serializer.serialize(self.data) or b""
130
+ serialized_msg = self._serializer.serialize(self.data)
131
+ msg = serialized_msg if serialized_msg is not None else b""
131
132
 
132
133
  if not isinstance(msg, bytes):
133
134
  message_data = ET.tostring(msg, encoding="utf-8", method="xml")
@@ -155,7 +156,7 @@ class Message(object):
155
156
  rpid = str(uuid.UUID(bytes_le=data[8:24]))
156
157
  pid = str(uuid.UUID(bytes_le=data[24:40]))
157
158
 
158
- if data[40:43] == b"\xEF\xBB\xBF":
159
+ if data[40:43] == b"\xef\xbb\xbf":
159
160
  # 40-43 is the UTF-8 BOM which we don't care about
160
161
  message_data = to_string(data[43:])
161
162
  else:
pypsrp/negotiate.py CHANGED
@@ -11,7 +11,6 @@ import spnego
11
11
  import spnego.channel_bindings
12
12
  from cryptography import x509
13
13
  from cryptography.exceptions import UnsupportedAlgorithm
14
- from cryptography.hazmat.backends import default_backend
15
14
  from cryptography.hazmat.primitives import hashes
16
15
  from requests.auth import AuthBase
17
16
  from requests.packages.urllib3.response import HTTPResponse
@@ -257,9 +256,7 @@ class HTTPNegotiateAuth(AuthBase):
257
256
  :return: The byte string containing the hash of the server's
258
257
  certificate
259
258
  """
260
- backend = default_backend()
261
-
262
- cert = x509.load_der_x509_certificate(certificate_der, backend)
259
+ cert = x509.load_der_x509_certificate(certificate_der)
263
260
 
264
261
  hash_algorithm = None
265
262
  try:
@@ -273,9 +270,9 @@ class HTTPNegotiateAuth(AuthBase):
273
270
  # If the cert signature algorithm is unknown, md5, or sha1 then use sha256 otherwise use the signature
274
271
  # algorithm of the cert itself.
275
272
  if not hash_algorithm or hash_algorithm.name in ["md5", "sha1"]:
276
- digest = hashes.Hash(hashes.SHA256(), backend)
273
+ digest = hashes.Hash(hashes.SHA256())
277
274
  else:
278
- digest = hashes.Hash(hash_algorithm, backend)
275
+ digest = hashes.Hash(hash_algorithm)
279
276
 
280
277
  digest.update(certificate_der)
281
278
  certificate_hash = digest.finalize()
pypsrp/powershell.py CHANGED
@@ -11,7 +11,6 @@ import uuid
11
11
  import warnings
12
12
  import xml.etree.ElementTree as ET
13
13
 
14
- from cryptography.hazmat.backends import default_backend
15
14
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
16
15
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
17
16
 
@@ -103,6 +102,8 @@ class RunspacePool(object):
103
102
  min_runspaces: int = 1,
104
103
  max_runspaces: int = 1,
105
104
  session_key_timeout_ms: int = 60000,
105
+ *,
106
+ no_profile: bool = False,
106
107
  ) -> None:
107
108
  """
108
109
  Represents a Runspace pool on a remote host. This pool can contain
@@ -129,6 +130,8 @@ class RunspacePool(object):
129
130
  hold
130
131
  :param session_key_timeout_ms: The maximum time to wait for a session
131
132
  key transfer from the server
133
+ :param no_profile: If True, the user profile will not be loaded and
134
+ will use the machine defaults.
132
135
  """
133
136
  log.info("Initialising RunspacePool object for configuration %s" % configuration_name)
134
137
  # The below are defined in some way at
@@ -138,7 +141,12 @@ class RunspacePool(object):
138
141
  self.connection = connection
139
142
  resource_uri = "http://schemas.microsoft.com/powershell/%s" % configuration_name
140
143
  self.shell = WinRS(
141
- connection, resource_uri=resource_uri, id=self.id, input_streams="stdin pr", output_streams="stdout"
144
+ connection,
145
+ resource_uri=resource_uri,
146
+ id=self.id,
147
+ input_streams="stdin pr",
148
+ output_streams="stdout",
149
+ no_profile=no_profile,
142
150
  )
143
151
  self.ci_table: typing.Dict = {}
144
152
  self.pipelines: typing.Dict[str, "PowerShell"] = {}
@@ -569,7 +577,6 @@ class RunspacePool(object):
569
577
  self._exchange_key = rsa.generate_private_key(
570
578
  public_exponent=65537,
571
579
  key_size=2048,
572
- backend=default_backend(),
573
580
  )
574
581
  public_numbers = self._exchange_key.public_key().public_numbers()
575
582
  exponent = struct.pack("<I", public_numbers.e)
@@ -861,7 +868,7 @@ class RunspacePool(object):
861
868
  iv = b"\x00" * 16 # PSRP doesn't use an IV
862
869
  algorithm = algorithms.AES(decrypted_key)
863
870
  mode = modes.CBC(iv)
864
- cipher = Cipher(algorithm, mode, default_backend())
871
+ cipher = Cipher(algorithm, mode)
865
872
 
866
873
  self._serializer.cipher = cipher
867
874
  self._key_exchanged = True
@@ -907,7 +914,16 @@ class PowerShell(object):
907
914
  # CommandID that is created and we need to reference in the WSMan msgs
908
915
  self._command_id: typing.Optional[str] = None
909
916
 
910
- runspace_pool.pipelines[self.id] = self
917
+ def __enter__(self) -> "PowerShell":
918
+ return self
919
+
920
+ def __exit__(
921
+ self,
922
+ exc_type: typing.Optional[typing.Type[BaseException]],
923
+ value: typing.Optional[BaseException],
924
+ traceback: typing.Optional[types.TracebackType],
925
+ ) -> None:
926
+ self.close()
911
927
 
912
928
  def add_argument(
913
929
  self,
@@ -1042,6 +1058,33 @@ class PowerShell(object):
1042
1058
  self.commands = []
1043
1059
  return self
1044
1060
 
1061
+ def clear_streams(self) -> None:
1062
+ """
1063
+ Clears all the data streams of the current PowerShell object.
1064
+ """
1065
+ self.streams = PSDataStreams()
1066
+ self.output = []
1067
+
1068
+ def close(self) -> None:
1069
+ """
1070
+ Closes the PowerShell pipeline and cleans up server resources.
1071
+
1072
+ This method must be called to reuse a PowerShell object for multiple
1073
+ invocations. It sends a TERMINATE signal to the server and removes
1074
+ the pipeline from the runspace pool's pipeline registry.
1075
+
1076
+ :raises InvalidPipelineStateError: If the pipeline is not in a terminal
1077
+ state (COMPLETED, STOPPED, or FAILED)
1078
+ """
1079
+ valid_states = [PSInvocationState.COMPLETED, PSInvocationState.STOPPED, PSInvocationState.FAILED]
1080
+ if self.state == PSInvocationState.NOT_STARTED:
1081
+ return
1082
+ elif self.state not in valid_states:
1083
+ raise InvalidPipelineStateError(self.state, valid_states, "close a PowerShell pipeline")
1084
+
1085
+ self.runspace_pool.shell.signal(SignalCode.TERMINATE, command_id=self._command_id or self.id)
1086
+ self.runspace_pool.pipelines.pop(self.id, None)
1087
+
1045
1088
  def connect(self):
1046
1089
  """
1047
1090
  Connects to a running command on a remote server, waits until the
@@ -1124,8 +1167,14 @@ class PowerShell(object):
1124
1167
  values. Will default to returning the invocation info on all
1125
1168
  """
1126
1169
  log.info("Beginning remote Pipeline invocation")
1127
- if self.state != PSInvocationState.NOT_STARTED:
1128
- raise InvalidPipelineStateError(self.state, PSInvocationState.NOT_STARTED, "start a PowerShell pipeline")
1170
+ valid_states = [
1171
+ PSInvocationState.NOT_STARTED,
1172
+ PSInvocationState.STOPPED,
1173
+ PSInvocationState.COMPLETED,
1174
+ PSInvocationState.FAILED,
1175
+ ]
1176
+ if self.state not in valid_states:
1177
+ raise InvalidPipelineStateError(self.state, valid_states, "start a PowerShell pipeline")
1129
1178
 
1130
1179
  if len(self.commands) == 0:
1131
1180
  raise InvalidPSRPOperation("Cannot invoke PowerShell without any commands being set")
@@ -1414,7 +1463,7 @@ class PowerShell(object):
1414
1463
  self.state = PSInvocationState.STOPPING
1415
1464
  self.runspace_pool.shell.signal(SignalCode.PS_CTRL_C, str(self.id).upper())
1416
1465
  self.state = PSInvocationState.STOPPED
1417
- del self.runspace_pool.pipelines[self.id]
1466
+ self.runspace_pool.pipelines.pop(self.id, None)
1418
1467
 
1419
1468
  def _invoke(
1420
1469
  self,
@@ -1422,6 +1471,8 @@ class PowerShell(object):
1422
1471
  ) -> None:
1423
1472
  fragments = self.runspace_pool._fragmenter.fragment(msg, self.runspace_pool.id, self.id)
1424
1473
 
1474
+ self.runspace_pool.pipelines[self.id] = self
1475
+
1425
1476
  # send first fragment as Command message
1426
1477
  first_frag = base64.b64encode(fragments.pop(0)).decode("utf-8")
1427
1478
  resp = self.runspace_pool.shell.command("", arguments=[first_frag], command_id=self.id)
pypsrp/serializer.py CHANGED
@@ -62,7 +62,7 @@ class Serializer(object):
62
62
  self.cipher: typing.Any = None
63
63
  # Finds C0, C1 and surrogate pairs in a unicode string for us to
64
64
  # encode according to the PSRP rules
65
- self._serial_str = re.compile("[\u0000-\u001F\u007F-\u009F\U00010000-\U0010FFFF]")
65
+ self._serial_str = re.compile("[\u0000-\u001f\u007f-\u009f\U00010000-\U0010ffff]")
66
66
 
67
67
  # to support surrogate UTF-16 pairs we need to use a UTF-16 regex
68
68
  # so we can replace the UTF-16 string representation with the actual
@@ -615,7 +615,7 @@ class Serializer(object):
615
615
  element: ET.Element,
616
616
  metadata: ObjectMeta,
617
617
  ) -> typing.Any:
618
- obj = metadata.object() # type: ignore[misc] # Caller always sets object
618
+ obj = metadata.object()
619
619
  self.obj[element.attrib["RefId"]] = obj
620
620
 
621
621
  to_string_value = element.find("ToString")
@@ -669,7 +669,7 @@ class Serializer(object):
669
669
  element: ET.Element,
670
670
  metadata: ObjectMeta,
671
671
  ) -> typing.Any:
672
- obj = metadata.object() # type: ignore[misc] # Caller always sets object
672
+ obj = metadata.object()
673
673
  self.obj[element.attrib["RefId"]] = obj
674
674
 
675
675
  for obj_property in element:
@@ -703,13 +703,9 @@ class Serializer(object):
703
703
  element: ET.Element,
704
704
  metadata: typing.Optional[ObjectMeta] = None,
705
705
  ) -> typing.List:
706
- list_value = []
707
706
  value_meta = getattr(metadata, "list_value_meta", None)
708
707
 
709
- entries = element.find("LST")
710
- for entry in entries or []:
711
- entry_value = self.deserialize(entry, value_meta, clear=False)
712
- list_value.append(entry_value)
708
+ list_value = list(self._deserialize_list_values(element, "LST", value_meta=value_meta))
713
709
 
714
710
  return list_value
715
711
 
@@ -719,10 +715,8 @@ class Serializer(object):
719
715
  ) -> Queue:
720
716
  queue: Queue = Queue()
721
717
 
722
- entries = element.find("QUE")
723
- for entry in entries or []:
724
- entry_value = self.deserialize(entry, clear=False)
725
- queue.put(entry_value)
718
+ for entry in self._deserialize_list_values(element, "QUE"):
719
+ queue.put(entry)
726
720
 
727
721
  return queue
728
722
 
@@ -731,15 +725,24 @@ class Serializer(object):
731
725
  element: ET.Element,
732
726
  ) -> typing.List:
733
727
  # no native Stack object in Python so just use a list
734
- stack = []
735
-
736
- entries = element.find("STK")
737
- for entry in entries or []:
738
- entry_value = self.deserialize(entry, clear=False)
739
- stack.append(entry_value)
728
+ stack = list(self._deserialize_list_values(element, "STK"))
740
729
 
741
730
  return stack
742
731
 
732
+ def _deserialize_list_values(
733
+ self,
734
+ element: ET.Element,
735
+ list_type: str,
736
+ value_meta: typing.Optional[ObjectMeta] = None,
737
+ ) -> typing.Iterable[typing.Any]:
738
+ entries = element.find(list_type)
739
+ if entries is None:
740
+ return
741
+
742
+ for entry in entries:
743
+ entry_value = self.deserialize(entry, metadata=value_meta, clear=False)
744
+ yield entry_value
745
+
743
746
  def _deserialize_dct(
744
747
  self,
745
748
  element: ET.Element,
pypsrp/shell.py CHANGED
@@ -67,7 +67,7 @@ class WinRS(object):
67
67
  :param lifetime: The total lifetime of the shell
68
68
  :param name: The name (description) of the shell
69
69
  :param no_profile: Whether to create the shell with the user profile
70
- active or not
70
+ active or not. This may not work on newer hosts like Server 2012+.
71
71
  :param working_directory: The default working directory of the created
72
72
  shell
73
73
  """
@@ -197,8 +197,8 @@ class WinRS(object):
197
197
  # inherit the base options if it was passed in, otherwise use an empty
198
198
  # option set
199
199
  options = OptionSet() if base_options is None else base_options
200
- if self.no_profile is not None:
201
- options.add_option("WINRS_NOPROFILE", str(self.no_profile))
200
+ if self.no_profile:
201
+ options.add_option("WINRS_NOPROFILE", "TRUE", {"MustComply": "true"})
202
202
  if self.codepage is not None:
203
203
  options.add_option("WINRS_CODEPAGE", str(self.codepage))
204
204
 
pypsrp/wsman.py CHANGED
@@ -6,11 +6,11 @@ from __future__ import division
6
6
  import ipaddress
7
7
  import logging
8
8
  import re
9
+ import time
9
10
  import typing
10
11
  import uuid
11
12
  import warnings
12
13
  import xml.etree.ElementTree as ET
13
- from xml.dom.minidom import Element
14
14
 
15
15
  import requests
16
16
  from requests.packages.urllib3.util.retry import Retry
@@ -364,7 +364,18 @@ class WSMan(object):
364
364
  selector_set: typing.Optional["SelectorSet"] = None,
365
365
  timeout: typing.Optional[int] = None,
366
366
  ) -> ET.Element:
367
- res = self.invoke(WSManAction.RECEIVE, resource_uri, resource, option_set, selector_set, timeout)
367
+ # Receiving data can sometimes timeout if the server has bounced the
368
+ # network adapter. Luckily just sending the exact same request again
369
+ # will return the same data so we can safely retry
370
+ res = self.invoke(
371
+ WSManAction.RECEIVE,
372
+ resource_uri,
373
+ resource,
374
+ option_set,
375
+ selector_set,
376
+ timeout,
377
+ retries_on_read_timeout=5,
378
+ )
368
379
  return res.find("s:Body", namespaces=NAMESPACES) # type: ignore[return-value] # WSMan always has this present
369
380
 
370
381
  def reconnect(
@@ -437,6 +448,8 @@ class WSMan(object):
437
448
  option_set: typing.Optional["OptionSet"] = None,
438
449
  selector_set: typing.Optional["SelectorSet"] = None,
439
450
  timeout: typing.Optional[int] = None,
451
+ *,
452
+ retries_on_read_timeout: int = 0,
440
453
  ) -> ET.Element:
441
454
  """
442
455
  Send a generic WSMan request to the host.
@@ -451,6 +464,8 @@ class WSMan(object):
451
464
  :param selector_set: a wsman.SelectorSet to add to the request
452
465
  :param timeout: Override the default wsman:OperationTimeout value for
453
466
  the request, this should be an int in seconds.
467
+ :param retries_on_read_timeout: The number of retries to attempt if the
468
+ request fails due to a read timeout.
454
469
  :return: The ET Element of the response XML from the server
455
470
  """
456
471
  s = NAMESPACES["s"]
@@ -467,7 +482,7 @@ class WSMan(object):
467
482
  xml = ET.tostring(envelope, encoding="utf-8", method="xml")
468
483
 
469
484
  try:
470
- response = self.transport.send(xml)
485
+ response = self.transport.send(xml, retries_on_read_timeout=retries_on_read_timeout)
471
486
  except WinRMTransportError as err:
472
487
  try:
473
488
  # try and parse the XML and get the WSManFault
@@ -570,9 +585,9 @@ class WSMan(object):
570
585
  ET.SubElement(header, "{%s}OperationTimeout" % wsman).text = "PT%sS" % str(timeout or self.operation_timeout)
571
586
 
572
587
  reply_to = ET.SubElement(header, "{%s}ReplyTo" % wsa)
573
- ET.SubElement(
574
- reply_to, "{%s}Address" % wsa, attrib={"{%s}mustUnderstand" % s: "true"}
575
- ).text = "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"
588
+ ET.SubElement(reply_to, "{%s}Address" % wsa, attrib={"{%s}mustUnderstand" % s: "true"}).text = (
589
+ "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"
590
+ )
576
591
 
577
592
  ET.SubElement(header, "{%s}ResourceURI" % wsman, attrib={"{%s}mustUnderstand" % s: "true"}).text = resource_uri
578
593
 
@@ -617,7 +632,7 @@ class WSMan(object):
617
632
  if reason_info is not None:
618
633
  reason = reason_info.text
619
634
 
620
- wsman_fault = fault.find("s:Detail/wsmanfault:WSManFault", namespaces=NAMESPACES) if fault else None
635
+ wsman_fault = fault.find("s:Detail/wsmanfault:WSManFault", namespaces=NAMESPACES) if fault is not None else None
621
636
  if wsman_fault is not None:
622
637
  code = wsman_fault.attrib.get("Code", code)
623
638
  machine = wsman_fault.attrib.get("Machine")
@@ -790,17 +805,34 @@ class _TransportHTTP(object):
790
805
  if self.session:
791
806
  self.session.close()
792
807
 
793
- def send(self, message: bytes) -> bytes:
808
+ def send(
809
+ self,
810
+ message: bytes,
811
+ *,
812
+ retries_on_read_timeout: int = 0,
813
+ ) -> bytes:
794
814
  hostname = get_hostname(self.endpoint)
795
815
  if self.session is None:
796
816
  self.session = self._build_session()
797
817
 
818
+ attempt = 0
819
+ while True:
798
820
  # need to send an initial blank message to setup the security
799
821
  # context required for encryption
800
- if self.wrap_required:
822
+ if self.wrap_required and not self.encryption:
801
823
  request = requests.Request("POST", self.endpoint, data=None)
802
824
  prep_request = self.session.prepare_request(request)
803
- self._send_request(prep_request)
825
+
826
+ try:
827
+ self._send_request(prep_request)
828
+ except (requests.ReadTimeout, requests.ConnectTimeout) as e:
829
+ log.exception("%s during initial authentication request - attempt %d", (type(e).__name__, attempt))
830
+ if attempt == retries_on_read_timeout:
831
+ raise
832
+
833
+ attempt += 1
834
+ time.sleep(self.reconnection_backoff * (2**attempt))
835
+ continue
804
836
 
805
837
  protocol = WinRMEncryption.SPNEGO
806
838
  if isinstance(self.session.auth, HttpCredSSPAuth):
@@ -811,30 +843,42 @@ class _TransportHTTP(object):
811
843
 
812
844
  self.encryption = WinRMEncryption(self.session.auth.contexts[hostname], protocol) # type: ignore[union-attr] # This should not happen
813
845
 
814
- if log.isEnabledFor(logging.DEBUG):
815
- log.debug("Sending message: %s" % message.decode("utf-8"))
816
- # for testing, keep commented out
817
- # self._test_messages.append({"request": message.decode('utf-8'),
818
- # "response": None})
819
-
820
- headers = self.session.headers
821
- if self.wrap_required:
822
- content_type, payload = self.encryption.wrap_message(message) # type: ignore[union-attr] # This should not happen
823
- protocol = self.encryption.protocol if self.encryption else WinRMEncryption.SPNEGO
824
- type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' % (content_type, protocol)
825
- headers.update(
826
- {
827
- "Content-Type": type_header,
828
- "Content-Length": str(len(payload)),
829
- }
830
- )
831
- else:
832
- payload = message
833
- headers["Content-Type"] = "application/soap+xml;charset=UTF-8"
846
+ if log.isEnabledFor(logging.DEBUG):
847
+ log.debug("Sending message on attempt %d: %s" % (attempt, message.decode("utf-8")))
848
+ # for testing, keep commented out
849
+ # self._test_messages.append({"request": message.decode('utf-8'),
850
+ # "response": None})
851
+
852
+ headers = self.session.headers.copy() # type: ignore[attr-defined] # We cannot use copy.copy as it still is a ref to the original.
853
+ if self.wrap_required:
854
+ content_type, payload = self.encryption.wrap_message(message) # type: ignore[union-attr] # This should not happen
855
+ protocol = self.encryption.protocol if self.encryption else WinRMEncryption.SPNEGO
856
+ type_header = '%s;protocol="%s";boundary="Encrypted Boundary"' % (content_type, protocol)
857
+ headers.update(
858
+ {
859
+ "Content-Type": type_header,
860
+ "Content-Length": str(len(payload)),
861
+ }
862
+ )
863
+ else:
864
+ payload = message
865
+ headers["Content-Type"] = "application/soap+xml;charset=UTF-8"
866
+
867
+ request = requests.Request("POST", self.endpoint, data=payload, headers=headers)
868
+ prep_request = self.session.prepare_request(request)
869
+ try:
870
+ return self._send_request(prep_request)
871
+ except (requests.ReadTimeout, requests.ConnectTimeout) as e:
872
+ log.exception("%s during WSMan request - attempt %d", (type(e).__name__, attempt))
873
+ if attempt == retries_on_read_timeout:
874
+ raise
875
+
876
+ # On a failure the encryption state is invalidated and needs to
877
+ # be recreated after authentication is done.
878
+ self.encryption = None
834
879
 
835
- request = requests.Request("POST", self.endpoint, data=payload, headers=headers)
836
- prep_request = self.session.prepare_request(request)
837
- return self._send_request(prep_request)
880
+ attempt += 1
881
+ time.sleep(self.reconnection_backoff * (2**attempt - 1))
838
882
 
839
883
  def _send_request(self, request: requests.PreparedRequest) -> bytes:
840
884
  response = self.session.send(request, timeout=(self.connection_timeout, self.read_timeout)) # type: ignore[union-attr] # This should not happen
@@ -878,7 +922,7 @@ class _TransportHTTP(object):
878
922
 
879
923
  # get the env requests settings
880
924
  session.trust_env = True
881
- settings = session.merge_environment_settings( # type: ignore[no-untyped-call] # Not in types-requests
925
+ settings = session.merge_environment_settings(
882
926
  url=self.endpoint, proxies={}, stream=None, verify=None, cert=None
883
927
  )
884
928
 
@@ -1,30 +1,44 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pypsrp
3
- Version: 0.8.1
3
+ Version: 0.9.0rc1
4
4
  Summary: PowerShell Remoting Protocol and WinRM for Python
5
- Home-page: https://github.com/jborean93/pypsrp
6
- Author: Jordan Borean
7
- Author-email: jborean93@gmail.com
8
- License: MIT
5
+ Author-email: Jordan Borean <jborean93@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: homepage, https://github.com/jborean93/pypsrp
9
8
  Keywords: winrm,psrp,winrs,windows,powershell
10
- Platform: UNKNOWN
11
9
  Classifier: Development Status :: 4 - Beta
12
- Classifier: License :: OSI Approved :: MIT License
13
10
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.6
15
- Classifier: Programming Language :: Python :: 3.7
16
- Classifier: Programming Language :: Python :: 3.8
17
- Classifier: Programming Language :: Python :: 3.9
18
11
  Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
19
17
  Description-Content-Type: text/markdown
20
18
  License-File: LICENSE
21
- Requires-Dist: cryptography
22
- Requires-Dist: pyspnego (<1.0.0)
23
- Requires-Dist: requests (>=2.9.1)
19
+ Requires-Dist: cryptography>=3.1
20
+ Requires-Dist: pyspnego<1.0.0,>=0.7.0
21
+ Requires-Dist: requests>=2.9.1
24
22
  Provides-Extra: credssp
25
- Requires-Dist: requests-credssp (>=2.0.0) ; extra == 'credssp'
23
+ Requires-Dist: requests-credssp>=2.0.0; extra == "credssp"
26
24
  Provides-Extra: kerberos
27
- Requires-Dist: pyspnego[kerberos] ; extra == 'kerberos'
25
+ Requires-Dist: pyspnego[kerberos]; extra == "kerberos"
26
+ Provides-Extra: dev
27
+ Requires-Dist: black==25.11.0; extra == "dev"
28
+ Requires-Dist: build; extra == "dev"
29
+ Requires-Dist: isort==6.1.0; extra == "dev"
30
+ Requires-Dist: mypy==1.19.0; extra == "dev"
31
+ Requires-Dist: pre-commit; extra == "dev"
32
+ Requires-Dist: pyspnego[kerberos]; extra == "dev"
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Requires-Dist: pytest-cov; extra == "dev"
35
+ Requires-Dist: pytest-mock; extra == "dev"
36
+ Requires-Dist: PyYAML; extra == "dev"
37
+ Requires-Dist: requests-credssp; extra == "dev"
38
+ Requires-Dist: types-requests; extra == "dev"
39
+ Requires-Dist: types-PyYAML; extra == "dev"
40
+ Requires-Dist: xmldiff; extra == "dev"
41
+ Dynamic: license-file
28
42
 
29
43
  # pypsrp - Python PowerShell Remoting Protocol Client library
30
44
 
@@ -69,7 +83,7 @@ libraries to be installed.
69
83
 
70
84
  See `How to Install` for more details
71
85
 
72
- * CPython 3.6+
86
+ * CPython 3.10+
73
87
  * [cryptography](https://github.com/pyca/cryptography)
74
88
  * [pyspnego](https://github.com/jborean93/pyspnego)
75
89
  * [requests](https://github.com/requests/requests)
@@ -155,7 +169,19 @@ pip install pypsrp[credssp]
155
169
  ```
156
170
 
157
171
  If that fails you may need to update pip and setuptools to a newer version
158
- `pip install -U pip setuptools`.
172
+ `pip install -U pip setuptools`, otherwise the following system package may be
173
+ required;
174
+
175
+ ```bash
176
+ # For Debian/Ubuntu
177
+ apt-get install gcc python-dev
178
+
179
+ # For RHEL/Centos
180
+ yum install gcc python-devel
181
+
182
+ # For Fedora
183
+ dnf install gcc python-devel
184
+ ```
159
185
 
160
186
 
161
187
  ## How to Use
@@ -235,7 +261,7 @@ configure a `WinRS` shell;
235
261
  * `idle_time_out`: THe idle timeout in seconds of the shell
236
262
  * `lifetime`: The total lifetime of the shell
237
263
  * `name`: The name (description only) of the shell
238
- * `no_profile`: Whether to create the shell with the user profile loaded or not
264
+ * `no_profile`: Whether to create the shell with the user profile loaded or not. This no longer works on Server 2012/Windows 8 or newer
239
265
  * `working_directory`: The default working directory of the created shell
240
266
 
241
267
  `RunspacePool` is a shell used by the PSRP protocol, it is designed to be a
@@ -252,6 +278,7 @@ Here are the options that can be used to configure a `RunspacePool` shell;
252
278
  * `min_runspaces`: The minimuum number of runspaces that a pool can hold, default is 1
253
279
  * `max_runspaces`: The maximum number of runspaces that a pool can hold. Each PowerShell pipeline is run in a single Runspace, default is 1
254
280
  * `session_key_timeout_ms`: The maximum time to wait for a session key transfer from the server
281
+ * `no_profile`: Do not load the user profile on the remote Runspace Pool
255
282
 
256
283
  ### Process
257
284
 
@@ -357,9 +384,8 @@ from pypsrp.wsman import WSMan
357
384
  # creates a https connection with explicit kerberos auth and implicit credentials
358
385
  wsman = WSMan("server", auth="kerberos", cert_validation=False))
359
386
 
360
- with wsman, RunspacePool(wsman) as pool:
387
+ with wsman, RunspacePool(wsman) as pool, PowerShell(pool) as ps:
361
388
  # execute 'Get-Process | Select-Object Name'
362
- ps = PowerShell(pool)
363
389
  ps.add_cmdlet("Get-Process").add_cmdlet("Select-Object").add_argument("Name")
364
390
  output = ps.invoke()
365
391
 
@@ -390,6 +416,19 @@ with wsman, RunspacePool(wsman) as pool:
390
416
  ps.invoke(["string", 1])
391
417
  print(ps.output)
392
418
  print(ps.streams.debug)
419
+
420
+ # It is possible to run the PowerShell pipeline again with invoke() but it
421
+ # needs to be explicitly closed first and the commands/streams optionally
422
+ # cleared if desired.
423
+ ps.close()
424
+
425
+ # Clears out ps.output and ps.streams to a blank value. Not required but
426
+ # nice if the output should be separate from a previous run
427
+ ps.clear_streams()
428
+
429
+ # Removes all existing commands. Not required but needed if re-using the
430
+ # same pipeline with a different set of commands
431
+ ps.clear_commands()
393
432
  ```
394
433
 
395
434
 
@@ -443,17 +482,18 @@ information and should only be used for debugging purposes._
443
482
  ## Testing
444
483
 
445
484
  Any changes are more than welcome in pull request form, you can run the current
446
- test suite with tox like so;
485
+ test suite with:
447
486
 
448
487
  ```bash
449
- # make sure tox is installed
450
- pip install tox
451
-
452
- # run the tox suite
453
- tox
454
-
455
- # or run the test manually for the current Python environment
456
- py.test -v --pep8 --cov pypsrp --cov-report term-missing
488
+ pip install -e .[dev]
489
+
490
+ python -m pytest \
491
+ tests/tests_pypsrp \
492
+ --verbose \
493
+ --junitxml junit/test-results.xml \
494
+ --cov pypsrp \
495
+ --cov-report xml \
496
+ --cov-report term-missing
457
497
  ```
458
498
 
459
499
  A lot of the tests either simulate a remote Windows host but you can also run a
@@ -523,5 +563,3 @@ tests.
523
563
  * Add Ansible playbook for better integration tests
524
564
  * Improved serialization between Python and .NET objects
525
565
  * Live interactive console for PSRP
526
-
527
-
@@ -0,0 +1,22 @@
1
+ pypsrp/__init__.py,sha256=9aButMuBNIEph0LfHsodNd8uSS7XKZj9IPHJsPd8hjY,958
2
+ pypsrp/_utils.py,sha256=y9sezXQPl0vXnr1beyZ8xcn-Q_zdg_7cg_lPk9QmjVQ,3540
3
+ pypsrp/client.py,sha256=iJERbEXwwE4wswpqAL0Dcz8ACqLvXUT9Dcl_Qqa3830,13892
4
+ pypsrp/complex_objects.py,sha256=PkJ1F1Byw5RI_Vpfy1KfcMFJdOfioOSsIbi50S_fZ3g,62666
5
+ pypsrp/encryption.py,sha256=4wOontH-eF8BBxTyHTX9iAF0uIM6m-z3N9poatspP1A,3870
6
+ pypsrp/exceptions.py,sha256=SWdQ9UAPOIwr7B4twGqDoXLCEtXFK_5u3PYIVLvEIbA,4034
7
+ pypsrp/host.py,sha256=XQgnkwzsZprNmAr1TPJpEBmj3xRSWFrYQFSIi870aNE,45065
8
+ pypsrp/messages.py,sha256=mCuie1ywOQmkXltISPvrb6ITS6GUA0oSld2jfNLdBf4,34169
9
+ pypsrp/negotiate.py,sha256=YU6mGuDd8krOapiRcY6qID8v-GMDbIypCYUc7eTkzmw,10952
10
+ pypsrp/powershell.py,sha256=SjmD8LfbMtyoGfXpQmSV10ded4FO14J8vM5Qn37R0dI,67359
11
+ pypsrp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pypsrp/serializer.py,sha256=49rBxfo3fh511mU3uzoK9vFvIOrBtyX3J-CUugbezc0,33767
13
+ pypsrp/shell.py,sha256=EoGtElIYvNFPjBvAIMuLaHPyX0NgGtBN265Vt6YqkhQ,16748
14
+ pypsrp/wsman.py,sha256=V90G93O2bi9fPP09fPhyheYN4_G_8zo_Sxo1ea2pqPI,47500
15
+ pypsrp/pwsh_scripts/__init__.py,sha256=VckRBWWDUdh_LTwPspE2FXSo8TaT50QnZ1aZ-yvxJmU,139
16
+ pypsrp/pwsh_scripts/copy.ps1,sha256=sTy4jcnh0cV5Y0l2-dd1znc-eM_kgE-okk51xZzdIQk,5374
17
+ pypsrp/pwsh_scripts/fetch.ps1,sha256=7gvPxURnksZetYgjWRjbIDIWSJYv2xGXZjRymvNUj6w,1989
18
+ pypsrp-0.9.0rc1.dist-info/licenses/LICENSE,sha256=8uJLgs8Wb_Us_8XaTvOOn7Yf0dwzFi9pcFHVxfgJKoE,1079
19
+ pypsrp-0.9.0rc1.dist-info/METADATA,sha256=RsbvzIn44QmLDG7Pi5QCULyxtJ8Z2kZByhI7dkU6Fmo,23485
20
+ pypsrp-0.9.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ pypsrp-0.9.0rc1.dist-info/top_level.txt,sha256=9sFQD0PMIG07xbaR4j-1Fq7NaD9kwInn_HTc3Gqznq0,7
22
+ pypsrp-0.9.0rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- pypsrp/__init__.py,sha256=9aButMuBNIEph0LfHsodNd8uSS7XKZj9IPHJsPd8hjY,958
2
- pypsrp/_utils.py,sha256=BKXpmraKuyWt5N89ViuC91wsF7AB1XPyViE_1_Cl4w0,3541
3
- pypsrp/client.py,sha256=sp9rgNwAGJXXaMgdYdJ7yMd_q1FjYg1nlpVwT_yncSw,13941
4
- pypsrp/complex_objects.py,sha256=D2y7IaxN3_n72Er6vJxFFlui9yHORVopGxIVManlCDg,62744
5
- pypsrp/encryption.py,sha256=elgz6Fph6ZYsvN7ZT1FFSH7Q05UYS8Nh5kiJvSyWy-o,3871
6
- pypsrp/exceptions.py,sha256=SWdQ9UAPOIwr7B4twGqDoXLCEtXFK_5u3PYIVLvEIbA,4034
7
- pypsrp/host.py,sha256=XQgnkwzsZprNmAr1TPJpEBmj3xRSWFrYQFSIi870aNE,45065
8
- pypsrp/messages.py,sha256=GBKv2Ilm0oDV8lHVbYDkx41AckoOGIhQG-HovbLnvbA,34093
9
- pypsrp/negotiate.py,sha256=ZS1NL2U17CFiDE3B_U2AprRz_BfBO7iFC5jkSO_bi1E,11073
10
- pypsrp/powershell.py,sha256=10TpFRSvMi8VVtkkSklQCBftcagzLpN2ZginSbnyDVA,65595
11
- pypsrp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- pypsrp/serializer.py,sha256=KhT1l0ay2wQfncAXY-KY9ZQfVMKhjCWgjL7O1bywEhA,33759
13
- pypsrp/shell.py,sha256=DA-yj80PdtfxpOAmJrs6k5BmjqjE5-ZeDU2C9rgli6k,16697
14
- pypsrp/wsman.py,sha256=v2ZkKGfKY502VhA8XFdXKyEPbEoc3bTWJ486SRbW2do,45641
15
- pypsrp/pwsh_scripts/__init__.py,sha256=VckRBWWDUdh_LTwPspE2FXSo8TaT50QnZ1aZ-yvxJmU,139
16
- pypsrp/pwsh_scripts/copy.ps1,sha256=sTy4jcnh0cV5Y0l2-dd1znc-eM_kgE-okk51xZzdIQk,5374
17
- pypsrp/pwsh_scripts/fetch.ps1,sha256=7gvPxURnksZetYgjWRjbIDIWSJYv2xGXZjRymvNUj6w,1989
18
- pypsrp-0.8.1.dist-info/LICENSE,sha256=8uJLgs8Wb_Us_8XaTvOOn7Yf0dwzFi9pcFHVxfgJKoE,1079
19
- pypsrp-0.8.1.dist-info/METADATA,sha256=lB6x7GQTrp8L1flRqu7Np2XScvrS43pjj7mmR8LyEQU,22012
20
- pypsrp-0.8.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
21
- pypsrp-0.8.1.dist-info/top_level.txt,sha256=9sFQD0PMIG07xbaR4j-1Fq7NaD9kwInn_HTc3Gqznq0,7
22
- pypsrp-0.8.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
- pypsrp-0.8.1.dist-info/RECORD,,
@@ -1 +0,0 @@
1
-