# PSRPCore Scenarios Here are some common scenarios in the PowerShell Remoting Protocol and how they can be implemented with this library. The examples here are all based on an IO-less connection, this layer still needs to be provided by a higher layer. ## Opening a Runspace Pool This is the first step that must be performed in PSRP. ```python import psrpcore client_pool = psrpcore.ClientRunspacePool() server_pool = psrpcore.ServerRunspacePool() # Start opening the client which generates data to exchange client_pool.open() # Continue to send and receive data until the state has changed. while runspace_pool.state == psrpcore.types.RunspacePoolState.Opening: client_outgoing = client_pool.data_to_send() # Have the server process all incoming messages - no more events server_pool.receive_data(client_outgoing) while True: event = server_pool.next_event() if event is None: break server_outgoing = server_pool.data_to_send() # Process server response and fail if the eventual state was broken. client_pool.receive_data(server_outgoing) while True: event = runspace_pool.next_event() if event is None: break elif isinstance(event, psrpcore.RunspacePoolStateEvent): if event.state == psrpcore.types.RunspacePoolState.Broken: raise Exception(f"Open failed {event.reason}") ``` ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[SessionCapability](psrpcore.SessionCapabilityEvent)|Server|States the protocol versions offered by the client| |[InitRunspacePool](psrpcore.InitRunspacePoolEvent)|Server|Runspace Pool configuration settings| |[SessionCapability](psrpcore.SessionCapabilityEvent)|Client|States the protocol versions offered by the server| |[ApplicationPrivateData](psrpcore.ApplicationPrivateDataEvent)|Client|Server provided data not processed by the PSRP layer.| |[RunspacePoolState](psrpcore.RunspacePoolStateEvent)|Client|The final state which should be ``Opened``| ## Closing a Runspace Pool Closing a Runspace Pool is done by a transport specific message and is not covered by PSRP itself. ```python import psrpcore client_pool = psrpcore.ClientRunspacePool() server_pool = psrpcore.ServerRunspacePool() # Opened the pool ... # begin_close() isn't necessary but it's helpful to signal a higher layer. client_pool.begin_close() # The transport would signal the server in some way that the pool should be closed server_pool.begin_close() server_pool.close() # The transport would signal the client the pool has been closed client_pool.close() ``` Before closing a Runspace Pool, all pipelines must be stopped or completed. Failure to do so could result in an indefinite hang until they have stopped naturally. ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[RunspacePoolState](psrpcore.RunspacePoolStateEvent)|Client|Final state to denote the pool has been closed| _Note: This message is not guaranteed to be sent and merely a formality. Actually closing the pool should be signaled by the transport protocol._ ## Exchange a Session Key To serialize a [Secure String](psrpcore.types.PSSecureString) the client must first perform a session key exchange. This exchange has the client generate a public key unique to that runspace pool, send that to the server, and await the encrypted key response. Once the key has been exchange the client is able to send and decrypt secure strings. ```python import psrpcore client_pool = psrpcore.ClientRunspacePool() server_pool = psrpcore.ServerRunspacePool() # Opened the pool ... # Start the key exchange client_pool.exchange_key() # Send the public key data to the server and have it process the msg server_pool.receive_data(client_pool.data_to_send()) server_pool.next_event() # Send the encrypted session key back to the client and process the msg client_pool.receive_data(server_pool.data_to_send()) client_pool.next_event() ``` Older PowerShell versions could have the server request the key exchange by sending a [PublicKeyRequest](psrpcore.PublicKeyRequestEvent) message. When receiving this message the client is expected to perform this exchange like normal. ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[PublicKey](psrpcore.PublicKeyEvent)|Server|Base64 encoded public key generated by the client| |[EncryptedSessionKey](psrpcore.EncryptedSessionKeyEvent)|Client|Encrypted session key generated by the server| ## Adjusting Runspace Count Once the Runspace Pool has been created it is possible to adjust the minimum and maximum runspace count in the pool. This is done by sending the request to adjust the count and awaiting the response from the server as to whether that was successful or not. ```python import psrpcore # Opens with a min and max runspace count of 1 and 2 client_pool = psrpcore.ClientRunspacePool(min_runspaces=1, max_runspaces=2) server_pool = psrpcore.ServerRunspacePool() # Opened the pool ... # Request the max runspace count to be 5 call_id = client_pool.set_max_runspaces(5) # Send request to server and process the data server_pool.receive_data(client_pool.data_to_send()) set_max_event = server_pool.next_event() # Designate whether adjustment was successful or not server_pool.runspace_availability_response(set_max_event.ci, True) assert server_pool.max_runspaces == 5 # Automatically adjusted of the response was True. # Send response back to the client and process the data client_pool.receive_data(server_pool.data_to_send()) avail_resp = client_pool.next_event() assert avail_resp.success ``` ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[SetMinRunspaces](psrpcore.SetMinRunspacesEvent)|Server|Request to adjust the min runspace count| |[SetMaxRunspaces](psrpcore.SetMaxRunspacesEvent)|Server|Request to adjust the max runspace count| |[RunspaceAvailability](psrpcore.SetRunspaceAvailabilityEvent)|Client|Whether the count was adjusted or not| ## Running a PowerShell Pipeline ```python import psrpcore client_pool = psrpcore.ClientRunspacePool() server_pool = psrpcore.ServerRunspacePool() # Opened the pool ... # Create the client PowerShell pipeline c_ps = psrpcore.ClientPowerShell(client_pool) c_ps.add_script("echo 'hi'") c_ps.start() # Create the server pipeline - the pipeline id is exchanged in a transport specific message. pipeline_id = "from transport specific message" s_ps = psrpcore.ServerPipeline(server_pool, pipeline_id) # Send pipeline details to server and process the data server_pool.receive_data(client_pool.data_to_send()) create_pipe = server_pool.next_event() # Pipeline details are stored either on the event or the pipeline object create_pipe.pipeline is s_ps.metadata s_ps.start() # Data is exchanged between the client and server until it has stopped running. while c_ps.state == psrpcore.types.PSInvocationState.Running: # The server can choose whether to send that initial state or not (not mandatory) # client_pool.receive_data(server_pool.data_to_send()) # client_pool.next_event() # Server runs requested command(s) and sends output as necessary s_ps.write_output("data") s_ps.write_verbose("verbose record") s_ps.write_error(net_exception, ...) # Once the pipeline is done the server marks it as complete the pipeline s_ps.complete() # Client processes any result from the server client_pool.receive_data(server_pool.data_to_send()) while True: event = client_pool.next_event() if event is None: break elif isinstance(event, psrpcore.PipelineStateEvent): if event.state == psrpcore.types.PSInvocationState.Failed: raise Exception(f"Pipe failed {event.reason}") # Handle output data accordingly # Once the pipeline is done both the client and server need to close it # Sending the close signal is a transport specific message and not done in PSRP c_ps.close() s_ps.close() ``` One key thing to note is that the data is still exchanged on the underlying Runspace Pool. A call to ``data_to_send()`` is guaranteed to not mix messages targeted towards different pipelines or the pool in general. ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[CreatePipeline](psrpcore.CreatePipelineEvent)|Server|Contains the PowerShell pipeline details to invoke| |[PipelineInput](psrpcore.PipelineInputEvent)|Server|Data to be sent as input to the first command in the pipeline| |[EndOfPipelineInput](psrpcore.EndOfPipelineInputEvent)|Server|Signals no more input is expected| |[PipelineOutput](psrpcore.PipelineOutputEvent)|Client|Output from the running pipeline| |[ErrorRecordMsg](psrpcore.ErrorRecordEvent)|Client|Error record from the running pipeline| |[WarningRecordMsg](psrpcore.WarningRecordEvent)|Client|Warning record from the running pipeline| |[VerboseRecordMsg](psrpcore.VerboseRecordEvent)|Client|Verbose record from the running pipeline| |[DebugRecordMsg](psrpcore.DebugRecordEvent)|Client|Debug record from the running pipeline| |[InformationRecordMsg](psrpcore.InformationRecordEvent)|Client|Information record from the running pipeline| |[ProgressRecordMsg](psrpcore.ProgressRecordEvent)|Client|Progress record from the running pipeline| |[PipelineState](psrpcore.PipelineStateEvent)|Client|The pipeline state and optional error message if it failed| The ``InformationRecordMsg`` is only used when talking to a PowerShell v5.1 or newer server. _Note: While running a pipeline can send a ``PipelineHostCall`` message as per [Sending a Host Call](#scenarios-sending-a-host-call)._ (scenarios-sending-a-host-call)= ## Sending a Host Call Interacting directly with the user is typically done through a host call. There are strict set of host calls defined in PSRP and the methods available depend on the host information that was defined on the Runspace Pool or Pipeline. Host calls are initiated by the server and, depending on the method invoked, can block further processing until a response is received. The [ClientHostResponder](psrpcore.ClientHostResponder) and [ServerHostRequestor](psrpcore.ServerHostRequestor) classes can be used to request and respond to host calls using well defined types. ```python import psrpcore import sys client_pool = psrpcore.ClientRunspacePool() client_host = psrpcore.ClientHostResponder(client_pool) server_pool = psrpcore.ServerRunspacePool() server_host = psrpcore.ServerHostRequestor(server_pool) # Opened the pool ... # Request the client host to write a line and read the response. server_host.write_line("Please enter your name:") call_id = server_host.read_line() # Send the request to the client and process the data client_pool.receive_data(server_pool.data_to_send()) # Get the first call - a client should check .method_identifier to see what to invoke write_call = client_pool.next_event() sys.stdout.write(write_call.method_parameters[0]) # Process the read call read_call = client_pool.next_event() client_host.read_line(read_call.ci, "Jordan") # Send response back to the server to process server_pool.receive_data(client_pool.data_to_send()) read_resp = server_pool.next_event() assert read_resp.result == "Jordan" ``` _Note: While it is possible to send a host call as defined on the Runspace Pool it is typically done through a running Pipeline._ ### Exchanged Events |Message|Role|Purpose| |-|-|-| |[RunspacePoolHostCall](psrpcore.RunspacePoolHostCallEvent)|Client|The requested call to run on the client Runspace Pool host| |[PipelineHostCall](psrpcore.PipelineHostCallEvent)|Client|The requested call to run on the client Pipeline host| |[RunspacePoolHostResponse](psrpcore.RunspacePoolHostResponseEvent)|Server|The response, if any, of the Runspace Pool host call| |[PipelineHostResponse](psrpcore.PipelineHostResponseEvent)|Server|The response, if any, of the Pipeline host call|