Condividi tramite


Using WS-Man to invoke a Powershell Cmdlet

First, let me apologize for the lack of posts to this blog.  Out original team goal was to have a new post every month, but holidays/vacations/work got in the way.  I’ll try to restart this rhythm.  For this first post of the New Year, I’m going to cover a more technical topic.

Most Windows administrators should already be aware that Powershell is the preferred ITPro scripting and command-line experience.  Many products from Microsoft and also third-parties expose their management interface as Powershell cmdlets.  Since Powershell is currently only available on Windows, how can you manage Windows from a non-Windows box?

Although Powershell remoting is built on top of WS-Management, it’s really a protocol within a protocol and trying to interop with PSRP (Powershell Remoting Protocol) directly would essentially require replicating Powershell on the client.  The alternative is to make use of a lesser known remote command-line tool called WinRS.  WinRS is a simple utility allows remote cmd.exe and is also built on top of WS-Management.  The difference is that WinRS reuses Create and Delete from WS-Transfer and introduces a few new custom SOAP web-methods.  For this blog post, I’ll be focusing on the WinRS “protocol” and won’t be discussing WS-Transfer, SOAP, nor HTTP in detail.  The complete details of the WinRS WS-Management Protocol extensions is documented here: https://msdn.microsoft.com/en-us/library/cc251526(v=PROT.10).aspx

WinRS has a relatively simple protocol, the workflow is:

1. WS-Transfer Create to create a shell, an EPR (End-Point Reference) to the created Shell is returned which is used for the next set of operations
2. Invoke the Command custom SOAP action to start a new command
3. Invoke the Receive custom SOAP action to receive output from the command (there is a corresponding Send for sending input, but it’s not needed for this scenario)
4. repeat step 3 until CommandState is Done
5. WS-Transfer Delete on the shell EPR

Let’s go through each step in a bit more detail.

For the WS-Transfer Create SOAP message, the body should contain the streams you want to send and receive.  The resource URI should be:  https://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd.  We are essentially creating a cmd.exe shell which we use to run powershell.exe.

<Shell xmlns='https://schemas.microsoft.com/wbem/wsman/1/windows/shell’>   <InputStreams>stdin</InputStreams>   <OutputStreams>stdout stderr</OutputStreams> </Shell>

If the request was successful, you’ll receive a standard WS-Transfer Create SOAP response which contains the newly created Shell EPR which resembles:

<w:SelectorSet>   <w:Selector Name="ShellId">AFCFB065-F551-4604-BFDFD9B706798B5D</w:Selector> </w:SelectorSet>

This EPR should be cached for all subsequent operations.  The first custom SOAP action Command uses the Action uri: https://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command.  WinRS supports two console modes: interactive and batch.  For an interactive session, WinRS will wait for input (even if the command completes) until the client indicates there is no more.  For a batch session, WinRS will expect input to be sent only during the lifetime of the running command.  For this scenario, it is important to specify the WS-Management option WINRS_CONSOLEMODE_STDIN with a value of true which means to use batch mode.  The command-line is split into separate Command and Arguments.  The SOAP fragment would look like:

…   <w:OptionSet>     <w:Option Name='WINRS_CONSOLEMODE_STDIN'>TRUE</w:Option>   </w:OptionSet> </s:Header> <s:Body> <CommandLine xmlns='https://schemas.microsoft.com/wbem/wsman/1/windows/shell'>   <Command>powershell</Command>   <Arguments>get-service | format-csv </Arguments> </CommandLine> </s:Body>

If this request was successful, the response will contain a CommandId element in the body that should be cached and used for subsequent operations for receiving the output.  Although the protocol was defined to allow one shell to host multiple commands, WinRS is limited to a single command per shell.  An example body of this response would resemble:

<rsp:CommandResponse>   <rsp:CommandId>772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E</rsp:CommandId> </rsp:CommandResponse>

Once the Command response is received, the command is running on the server.  WinRS will block output (and therefore the command) once the maximum envelope size is reached.  The custom SOAP action Receive uses the action uri: .  Since the resulting output may exceed a single SOAP envelope, the client needs to specify an incrementing SequenceId in case any packets were lost. WinRS will only cache the last sent packet.  The request should contain the streams you want to read from and the CommandId associated to the stream in the body:

<Receive SequenceId='0'    xmlns='https://schemas.microsoft.com/wbem/wsman/1/windows/shell'>   <DesiredStream CommandId='772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E'>     stdout stderr   </DesiredStream> </Receive>

The response will contain the text output of the streams base64 encoded (to keep the SOAP xml well-formed and valid).  The client should check the state of the command to know whether to continue to invoke Receive for more output. 

<rsp:ReceiveResponse>   <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E">DQo=</rsp:Stream>   <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E">     U3RhdHVzICAgTmFtZSAgICAgICAgICAgICAgIERpc3BsYXlOYW1lICAgICAgICAgICAgICAgICAgICAgICAgICAg</rsp:Stream>   <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E">     DQotLS0tLS0gICAtLS0tICAgICAgICAgICAgICAgLS0tLS0tLS0tLS0gICAgICAgICAgICAgICAgICAgICAgICAgICANClJ1bm5pbmcgIH   dpbm1nbXQgICAgICAgICAgICBXaW5kb3dzIE1hbmFnZW1lbnQgSW5zdHJ1bWVudGF0aW9uICAgIA0KDQoNCg==</rsp:Stream>   <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E" End="true"></rsp:Stream>   <rsp:Stream Name="stderr" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E" End="true"></rsp:Stream>   <rsp:CommandState CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E"       State="https://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">   <rsp:ExitCode>0</rsp:ExitCode>   </rsp:CommandState> </rsp:ReceiveResponse>

Once the CommandState is “Done”, there is no more output and WS-Transfer Delete should be called on the shell EPR.  This will clean-up any resources being used on the server. 

The example code shows how to invoke a Powershell cmdlet.  It does not make use of any WinRM api’s, but instead creates the necessary SOAP messages from templates and uses System.Net.HttpWebRequest to send it over the wire.  To enable using the sample code in Windows, you’ll need to enable Basic authentication in the WinRM service config (which only works for local acocunts), you can run this Powershell command as admin: Set-Item WSMan:\localhost\Service\Auth\Basic $true.  You will need to deploy a server cert to use HTTPS or for testing purposes, set the WinRM service config to allow unencrypted HTTP.  Here's an example command line to use with the sample code:

WinRSPsh https://server:5985/wsman user password "get-service"

If you want the output in a application consumable form, you can convert to XML (.Net serialization):

WinRSPsh https://server:5985/wsman user password "(get-service ^| convertto-xml).OuterXml"

Note that in the above example, you have to escape the pipe so that cmd.exe doesn't try to interpret it.

WinRSPsh.zip

Comments

  • Anonymous
    January 27, 2011
    Great post! Thanks for sharing... I just wish it had been posted a few months ago ;-)  I wrote a Ruby based winrm client (http://bit.ly/ruby_winrm) and I wasn't aware of the <Arguments/> element to <Command/>. The way I got around this that may come in handy for some people is passing a Base64 encoded script across the wire using Powershell's 'encodedCommand' switch. That way you can write your script file on the client in your favorite editor and pass it this way: script = Base64.strict_encode64(myscript) ... soap req... <Command>"powershell -encodedCommand #{script}"</Command> Just thought someone might find it of interest. Thanks again for your great posting. Looking forward to many more. Cheers!

  • Anonymous
    January 27, 2011
    @Dan, thanks for the feedback.  The <Arguments> element isn't strictly needed/validated today (as you noticed), but it keeps the xml a bit cleaner.   If you have suggestions for any topics you'd like to see covered, let me know.

  • Anonymous
    January 28, 2011
    Steve, One thing that has been of interest to me while using the wsman interface is maintaining a current working directory. When I open up a shell and issue a command "cd Desktop", for instance, the next command to that same shell_id is not from the Desktop directory. I can get around this somewhat using syntax like this 'cd Desktop && dir' but it would be nice if there was a way to maintain the state for the shell. Any tips? Cheers, Dan

  • Anonymous
    January 28, 2011
    @Dan, this is possible by using an interactive pattern than a batch pattern (what I presented here).  The difference is not huge, but significant as the client now owns the lifetime of the shell instead of the server which includes use of the Send and potentially Signal actions.  Let me see if I can find some time next week to provide an article on how to achieve this.

  • Anonymous
    January 31, 2011
    The comment has been removed

  • Anonymous
    January 31, 2011
    @Sandy: Although the sample code is written in C# and uses .Net, it is not doing anything Windows specific (you might even be able to re-compile the code using Mono).  The code is intended to show how to craft the necessary SOAP messages and send them via HTTP.  You should be able to use the sample and re-write it in Perl/Java/etc... In fact, Dan above has a link to a Ruby implementation he wrote you could use on Unix.  

  • Anonymous
    February 01, 2011
    This is off topic (I've been looking for an appropriate forum to discuss programattic (C#) WinRM usage... the MSDN forums aren't really all that helpful here)... Is there any way to access WMI Performance classes through WinRM?  As far as I can tell, there's no refresher object to work with, so I'm working if there's a trick that isn't made clear from the documentation...

  • Anonymous
    February 01, 2011
    You are correct that there isn't a WinRM equivalent of the WMI Refresher object.  For perf counters that don't require multiple samples to compute, you can make use of the Win32_PerfFormattedData classes, otherwise, you should use several samples of the Win32_PerfRawData instance and compute the result yourself.  This sounds like a good topic for a future blog post.  Thanks.

  • Anonymous
    October 04, 2011
    As you indicsted, able to compile the sample on mono on linux.  When I run it as in     mono Program.exe http://WIN-IP/winrm user passwd get-services seem to be able to get past cred check but end up with this.   Exception: Error getting response stream (Write): SendFailure Any ideas as to why?

  • Anonymous
    October 05, 2011
    @beegee the URL should be http://WIN-IP/wsman unless you explicitly changed the "urlprefix" in the configuration to be "winrm"

  • Anonymous
    November 23, 2011
    Hi I’ve been trying to get the interactive pattern to work so I can create a custom remote powershell client. When I invoke powershell with a script as argument, input and output work fine with multiple receive messages. But when I invoke powershell with a script and “-NoExit”, receive stops working as soon as the script is done executing. I can send the request, but it’s waiting on something and never returns a response. I can still send messages such as “del file.txt” and they are processed correctly, so it’s just the receive message that stops working. Same thing happens when I invoke powershell without any arguments, the copyright text is displayed, but after that receive never returns anything. Any idea why I’m getting this behavior? Best regards Johan

  • Anonymous
    November 23, 2011
    The comment has been removed

  • Anonymous
    November 23, 2011
    @JMD Software, I did some research and it appears this was a known issue in Win7/Win2k8R2.  PowerShell is designed to not prompt if stdin is redirected (which would be the case via WinRS).  So there is no way to use WinRS Protocol to establish an interactive PowerShell session.  

  • Anonymous
    May 07, 2012
    Kind of late to be asking this, but is there any way to use the same technique to transfer a file from the client to the server? Maybe pass a base64 encoded string as a parameter somehow?

  • Anonymous
    May 08, 2012
    You could certainly use this technique to transfer a file using base64 encoding.  However, it won't perform very well since the binary file would go through several levels of encoding (even if you skip PowerShell and just use cmd.exe).  Recommendation is to use BITS to transfer the file if possible which goes over HTTP.  msdn.microsoft.com/.../aa362708(v=vs.85).aspx If you could articulate your specific scenario further (assuming BITS is not something you are able to use), I could give you a better answer.

  • Anonymous
    May 08, 2012
    The comment has been removed

  • Anonymous
    May 09, 2012
    For cross-platform, it'll probably be easier to enable HTTPS instead of trying to implement session key based encryption (although you could follow the spec here: msdn.microsoft.com/.../cc251574(v=PROT.10).aspx).

  • Anonymous
    May 09, 2012
    It appears that this Open Source project successfully implemented session key HTTP encryption: github.com/.../WinRM.  I have not tried that Ruby client myself so I can't speak to it's quality.

  • Anonymous
    September 26, 2012
    Steve Lee: Thanks for the informative post ! We are performing a series of Powershell commands in batch mode. Objects are returned from one powershell command are to be used in the subsequent command. This if I run by embedding a batch script within args[3] or the commandline param, there will be a problem as all$variables will be substituted. One option then is to run each command of the script as a separate "Command custom SOAP action".

  1. In that case, is it possible to de-serialize all objects to text and then using the stderr+stdout of a command and again serialize the text into objects for the next command.
  2. We have one more problem where when another powershell module is loaded in one "Command custom SOAP Action". We get an error - "Process is terminated due to StackOverflowException" as part of the output. Are there any known restrictions in each Powershell shell EPR ? How to avoid this/handle this."