Using SOAP PHP with NTLM Authentication 13/03/08

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 :

Licence of the code

/*
 * Copyright (c) 2008 Invest-In-France Agency http://www.invest-in-france.org
 *
 * Author : Thomas Rabaix <thomas@rabaix.net>
 *
 * 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 : $path\n";
		$this->ch = curl_init($path);
		curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
		curl_setopt($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)."bytes\n";
		$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 NTMLSoapClient 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

Creative Commons License
This work is licensed under a Creative Commons Attribution 2.0 France License.

Comments

kj33p - 12/06/08 19:04:22
Great work, i am connecting to a https wsdl which is using a self-signed certificate. I can't find anything regarding this when constructing the soap client. I need to set the "CURLOPT_SSL_VERIFYPEER" curl option to false. When can i find the code for SoapClient->__construct to duplicate and set the right curl opts? Ideas?
Sam McDonald - 19/03/08 11:09:20
Thanks a lot. This is very useful I am trying to get web services working with a company that doesn't help you out at all if you are using php, and this was a big help. When you subclass NTMLSoapClient, you spell it incorrectly, but other than that everything worked great. You just helped me out a ton!
Jamie Talbot - 18/03/08 18:29:16
Nice work, I've been waiting for this in the PHP core, but this will hopefully work as an interim measure! One question though; how come you need to register the stream wrapper, if you're making a direct cURL request in the SoapClient? It seems redundant (not having worked through the code yet). Cheers, Jamie
Thomas Rabaix - 19/03/08 07:10:59
@Jamie : I was looking for the solution which is the more transparent. Due to the bug, I have created the NTMLSoapClient Object, I will move the un/registering code into the NTMLSoapClient::__construct. This solution required to have curl with ssl and works like a charm (but still required testing).

Add comment





37c8d88b762a9c3ffd292276097d17fedd273304