The SOAP-PHP extension does not handle NTLM Authentication used by IIS Server. So how can we solve this issue ? Well, by mixing some PHP modules :
- cURL : manage the connection throught NTLM Authentication
- Stream Functions : Create a NTLM Stream. PHP allows you to define or redefine a wrapper for a protocol (HTTP for instance), that means you can redefine functions such as fopen, fread, stat and so on for one protocol.
- NTMLSOAPClient : extends the object to send request trough cUrl
So this article we are going to create a stream object that open a NTML Wrapper with cURL and implements the basic functions require to make it work with the SOAPClient Object.
Documentations
You should consider to read the modules documentations if you want a better understanding about what’s happening next :
- PHP Stream Wrapper : https://php.net/manual/en/function.stream-wrapper-register.php
- PHP cURL : https://php.net/manual/en/ref.curl.php
- PHP SOAP: [https://php.net/manual/en/ref.soap.php](https://php.net/manual/en/ref.soap.php target=)
Licence of the code
/*
* Copyright (c) 2008 Invest-In-France Agency http://www.invest-in-france.org
*
* Author : Thomas Rabaix
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
NTLMStream
Class to connect to the webservice
class NTLMStream {
private $path;
private $mode;
private $options;
private $opened_path;
private $buffer;
private $pos;
/**
* Open the stream
*
* @param unknown_type $path
* @param unknown_type $mode
* @param unknown_type $options
* @param unknown_type $opened_path
* @return unknown
*/
public function stream_open($path, $mode, $options, $opened_path) {
echo "[NTLMStream::stream_open] $path , mode=$mode n";
$this->path = $path;
$this->mode = $mode;
$this->options = $options;
$this->opened_path = $opened_path;
$this->createBuffer($path);
return true;
}
/**
* Close the stream
*
*/
public function stream_close() {
echo "[NTLMStream::stream_close] n";
curl_close($this->ch);
}
/**
* Read the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_read($count) {
echo "[NTLMStream::stream_read] $count n";
if(strlen($this->buffer) == 0) {
return false;
}
$read = substr($this->buffer,$this->pos, $count);
$this->pos += $count;
return $read;
}
/**
* write the stream
*
* @param int $count number of bytes to read
* @return content from pos to count
*/
public function stream_write($data) {
echo "[NTLMStream::stream_write] n";
if(strlen($this->buffer) == 0) {
return false;
}
return true;
}
/**
*
* @return true if eof else false
*/
public function stream_eof() {
echo "[NTLMStream::stream_eof] ";
if($this->pos > strlen($this->buffer)) {
echo "true n";
return true;
}
echo "false n";
return false;
}
/**
* @return int the position of the current read pointer
*/
public function stream_tell() {
echo "[NTLMStream::stream_tell] n";
return $this->pos;
}
/**
* Flush stream data
*/
public function stream_flush() {
echo "[NTLMStream::stream_flush] n";
$this->buffer = null;
$this->pos = null;
}
/**
* Stat the file, return only the size of the buffer
*
* @return array stat information
*/
public function stream_stat() {
echo "[NTLMStream::stream_stat] n";
$this->createBuffer($this->path);
$stat = array(
'size' => strlen($this->buffer),
);
return $stat;
}
/**
* Stat the url, return only the size of the buffer
*
* @return array stat information
*/
public function url_stat($path, $flags) {
echo "[NTLMStream::url_stat] n";
$this->createBuffer($path);
$stat = array(
'size' => strlen($this->buffer),
);
return $stat;
}
/**
* Create the buffer by requesting the url through cURL
*
* @param unknown_type $path
*/
private function createBuffer($path) {
if($this->buffer) {
return;
}
echo "[NTLMStream::createBuffer] create buffer from : $pathn";
$this->ch = curl_init($path);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($this->ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($this->ch, CURLOPT_USERPWD, $this->user.':'.$this->password);
echo $this->buffer = curl_exec($this->ch);
echo "[NTLMStream::createBuffer] buffer size : ".strlen($this->buffer)."bytesn";
$this->pos = 0;
}
}
Now we have to create a class for your custom SOAP Provider
class MyServiceProviderNTLMStream extends NTLMStream {
protected $user = 'myuser';
protected $password = '*******';
}
Request the Webservice
$url = 'http://myIISServer.com/xmlservice?wsdl';
// we unregister the current HTTP wrapper
stream_wrapper_unregister('http');
// we register the new HTTP wrapper
stream_wrapper_register('http', 'MyServiceProviderNTLMStream') or die("Failed to register protocol");
// so now all request to a http page will be done by MyServiceProviderNTLMStream.
// ok now, let's request the wsdl file
// if everything works fine, you should see the content of the wsdl file
$client = new SoapClient($url, $options);
// but this will failed
$client->mySoapFunction();
// restore the original http protocole
stream_wrapper_restore('http');
The unexpected issue
The unexpected issue is that the SOAP object does not use the new HTTP Stream to send the query to the server ! So the request is not done through NTLM Authentication. Let’s fix that by reimplement the SOAPClient::__doRequest method. The __doRequest method is the low level method that send the request to the webservice.
class NTLMSoapClient extends SoapClient {
function __doRequest($request, $location, $action, $version) {
$headers = array(
'Method: POST',
'Connection: Keep-Alive',
'User-Agent: PHP-SOAP-CURL',
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "'.$action.'"',
);
$this->__last_request_headers = $headers;
$ch = curl_init($location);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true );
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_NTLM);
curl_setopt($ch, CURLOPT_USERPWD, $this->user.':'.$this->password);
$response = curl_exec($ch);
return $response;
}
function __getLastRequestHeaders() {
return implode("n", $this->__last_request_headers)."n";
}
}
// Authentification parameter
class MyServiceNTLMSoapClient extends NTLMSoapClient {
protected $user = 'myuser';
protected $password = '*******';
}
Request the webservice II
$url = 'http://myIISServer.com/xmlservice?wsdl';
// we unregister the current HTTP wrapper
stream_wrapper_unregister('http');
// we register the new HTTP wrapper
stream_wrapper_register('http', 'MyServiceProviderNTLMStream') or die("Failed to register protocol");
// so now all request to a http page will be done by MyServiceProviderNTLMStream.
// ok now, let's request the wsdl file
// if everything works fine, you should see the content of the wsdl file
$client = new MyServiceNTLMSoapClient($url, $options);
// should display your reply
echo $client->mySoapFunction();
// restore the original http protocole
stream_wrapper_restore('http');
Conclusion
- The stream_wrapper_register PHP feature is a well-hidden feature and very useful to extend missing features.
- Due to a bug in the SOAPClient, the stream wrapper does not work in ‘write’ mode
- The code needs some cleanup before it can be used.
- This code is not optimized for large reply and binary information
- This code has not been tested on production server, and uses this code at your own risk.
Licence of this document
This work is licensed under a Creative Commons Attribution 2.0 France License.