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.

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

Server

States the protocol versions offered by the client

InitRunspacePool

Server

Runspace Pool configuration settings

SessionCapability

Client

States the protocol versions offered by the server

ApplicationPrivateData

Client

Server provided data not processed by the PSRP layer.

RunspacePoolState

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.

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

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 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.

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 message. When receiving this message the client is expected to perform this exchange like normal.

Exchanged Events

Message

Role

Purpose

PublicKey

Server

Base64 encoded public key generated by the client

EncryptedSessionKey

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.

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

Server

Request to adjust the min runspace count

SetMaxRunspaces

Server

Request to adjust the max runspace count

RunspaceAvailability

Client

Whether the count was adjusted or not

Running a PowerShell Pipeline

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

Server

Contains the PowerShell pipeline details to invoke

PipelineInput

Server

Data to be sent as input to the first command in the pipeline

EndOfPipelineInput

Server

Signals no more input is expected

PipelineOutput

Client

Output from the running pipeline

ErrorRecordMsg

Client

Error record from the running pipeline

WarningRecordMsg

Client

Warning record from the running pipeline

VerboseRecordMsg

Client

Verbose record from the running pipeline

DebugRecordMsg

Client

Debug record from the running pipeline

InformationRecordMsg

Client

Information record from the running pipeline

ProgressRecordMsg

Client

Progress record from the running pipeline

PipelineState

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.

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 and ServerHostRequestor classes can be used to request and respond to host calls using well defined types.

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

Client

The requested call to run on the client Runspace Pool host

PipelineHostCall

Client

The requested call to run on the client Pipeline host

RunspacePoolHostResponse

Server

The response, if any, of the Runspace Pool host call

PipelineHostResponse

Server

The response, if any, of the Pipeline host call