I recently wanted to communicate via WinRM to Windows machines using PHP, as it allows for all sorts of automation tasks remotely over HTTP(S). And technically it has no dependency on Windows itself in order to make a working client. Just straight up HTTP communication and SOAP. How hard could it be...? Well, as it turns it...it can be extremely frustrating. Which is probably the reason why I have come across no WinRM implementations for PHP. And very few WinRM implementations in other languages. The most complete one I found was PyWinRM for Python. However, even that doesn't seem to use proper SOAP to make things work.
To back up a bit, WinRM is actually just a flavor of WSMan. WSMan itself is an open standard for SOAP to manage devices/ applications/etc. There is even an open source implementation of it used in SUSE Linux (using OpenWSMAN I think -- Client and server written in C). So it's kind of frustrating that there is so little in the way of client implementations in other languages. Though given how annoying working with SOAP has so far been for me, I guess I sort of understand it.
So problem 1 is that the SoapClient for the PHP SOAP extension needs a WSDL to point to. However, the WinRM WSDL doeesn't
seem to be exposed remotely (such as via
http://host.local.com:5985/winrm/?wsdl). The full WSDL can actually be found
in the Microsoft Open Specifications document MS-WSMV (in appendix A):
If you copy-paste that full WSDL to a local file you can then reference that in the PHP SoapClient. However, you then run immediately into problem 2.
Problem 2 is that the PHP SOAP extension doesn't handle namespaces very well. When you pass it the WSDL above you will
receive a SoapFault because it tries to redefine the
ReferenceEndpoint. This is because of all the import directives
used in the WSDL. The only way I found to get around this is to follow each import in the WSDL manually and copy/paste
the referenced import schema definition directly into the WSDL and then remove the import statement. Keep in mind that
some imports have imports of their own. Turtles all the way down.
Ultimately this is a bug with the PHP SOAP extension that has just been ignored for quite some time. Maybe no one is actively maintaining the SOAP extension? But it is valid to reference imported schema definitions in the WSDL that may in turn reference something that was already imported. Anyway, now that problem 2 is sorted out, it's on to problem number 3.
Problem number 3 is that the WSDL from Microsoft's document doesn't contain a service definition. So you receive a SoapFault that simply states "Couldn't bind to service". You can get around this by manually defining your own service definition in the WSDL:
<wsdl:service name="WSManService"> <wsdl:port name="WSManServicePort" binding="WSMANBinding"> <soap:address location="" /> </wsdl:port> </wsdl:service>
The binding referenced by
WSMANBinding in the port is the important piece. As that is defined within the WSDL already
and is what has all the information the SoapClient is looking for.
Ok, so now the SoapClient is happy and accepts our WinRM WSDL. But it sure would be nice if we could generate some PHP objects for it based off what it expects. And to this end the wsdl2phpgenerator project is pretty great:
Just feed it our modified WinRM WSDL file is the input and it will generate all of the types it finds. Pretty handy. However, for a complex WSDL like WinRM things start to fall apart. The few problems I encountered with this generator:
- Does not correctly handle multiple namespaces in the WSDL well at all, which leads to malformed Soap requests. Though this is less of a problem with the library and again more of a problem with PHPs Soap extension on which it relies.
- All of the generated classes are thrown under one PHP namespace. This makes it very difficult when the WinRM WSDL generates over 150 classes.
- No way to control class naming standards. You will end up with some weirdly named classes, such as starting with lower case characters or containing underscores. There really needs to be a naming strategy with some configurable options here.
- No control over the service class name or location.
- No way to control PSR/coding standards of generated code. Though you could easily handle this yourself afterwards with php-cs-fixer, it would be nice if it handled this for you.
Most of these issues are noted on the issue tracker of the project actually. Not knocking the work that went into this, it just doesn't meet what I need completely.
So after all this I still cannot get a valid XML SOAP request formed using standard PHP stuff. There will likely be a follow-up post to this. Though it seems like to do this right I need a PHP soap library that can handle all of the complexities of a WSDL this large. If I'm feeling ambitious enough I might sort through the Soap specs myself and see what kind of client I can come up with using PHP only, without the builtin extension. Though I imagine that will be very time consuming.