Using SOAP PHP with NTLM Authentication

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

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