Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]

   1  <?php
   2  
   3  namespace Horde\Socket;
   4  
   5  /**
   6   * Copyright 2013-2017 Horde LLC (http://www.horde.org/)
   7   *
   8   * See the enclosed file LICENSE for license information (LGPL). If you
   9   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  10   *
  11   * @category  Horde
  12   * @copyright 2013-2017 Horde LLC
  13   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  14   * @package   Socket_Client
  15   */
  16  
  17  /**
  18   * Utility interface for establishing a stream socket client.
  19   *
  20   * @author    Michael Slusarz <slusarz@horde.org>
  21   * @author    Jan Schneider <jan@horde.org>
  22   * @category  Horde
  23   * @copyright 2013-2017 Horde LLC
  24   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  25   * @package   Socket_Client
  26   *
  27   * @property-read boolean $connected  Is there an active connection?
  28   * @property-read boolean $secure  Is the active connection secure?
  29   */
  30  class Client
  31  {
  32      /**
  33       * Is there an active connection?
  34       *
  35       * @var boolean
  36       */
  37      protected $_connected = false;
  38  
  39      /**
  40       * Configuration parameters.
  41       *
  42       * @var array
  43       */
  44      protected $_params;
  45  
  46      /**
  47       * Is the connection secure?
  48       *
  49       * @var boolean
  50       */
  51      protected $_secure = false;
  52  
  53      /**
  54       * The actual socket.
  55       *
  56       * @var resource
  57       */
  58      protected $_stream;
  59  
  60      /**
  61       * Constructor.
  62       *
  63       * @param string $host      Hostname of remote server (can contain
  64       *                          protocol prefx).
  65       * @param integer $port     Port number of remote server.
  66       * @param integer $timeout  Connection timeout (in seconds).
  67       * @param mixed $secure     Security layer requested. One of:
  68       *   - false: (No encryption) [DEFAULT]
  69       *   - 'ssl': (Auto-detect SSL version)
  70       *   - 'sslv2': (Force SSL version 3)
  71       *   - 'sslv3': (Force SSL version 2)
  72       *   - 'tls': (TLS; started via protocol-level negotation over unencrypted
  73       *     channel)
  74       *   - 'tlsv1': (TLS version 1.x connection)
  75       *   - true: (TLS if available/necessary)
  76       * @param array $context    Any context parameters passed to
  77       *                          stream_create_context().
  78       * @param array $params     Additional options.
  79       *
  80       * @throws Horde\Socket\Client\Exception
  81       */
  82      public function __construct(
  83          $host, $port = null, $timeout = 30, $secure = false,
  84          $context = array(), array $params = array()
  85      )
  86      {
  87          if ($secure && !extension_loaded('openssl')) {
  88              if ($secure !== true) {
  89                  throw new \InvalidArgumentException('Secure connections require the PHP openssl extension.');
  90              }
  91              $secure = false;
  92          }
  93  
  94          $context = array_replace_recursive(
  95              array(
  96                  'ssl' => array(
  97                      'verify_peer' => false,
  98                      'verify_peer_name' => false
  99                  )
 100              ),
 101              $context
 102          );
 103  
 104          $this->_params = $params;
 105  
 106          $this->_connect($host, $port, $timeout, $secure, $context);
 107      }
 108  
 109      /**
 110       */
 111      public function __get($name)
 112      {
 113          switch ($name) {
 114          case 'connected':
 115              return $this->_connected;
 116  
 117          case 'secure':
 118              return $this->_secure;
 119          }
 120      }
 121  
 122      /**
 123       * This object can not be cloned.
 124       */
 125      public function __clone()
 126      {
 127          throw new \LogicException('Object cannot be cloned.');
 128      }
 129  
 130      /**
 131       * This object can not be serialized.
 132       */
 133      public function __sleep()
 134      {
 135          throw new \LogicException('Object can not be serialized.');
 136      }
 137  
 138      /**
 139       * Start a TLS connection.
 140       *
 141       * @return boolean  Whether TLS was successfully started.
 142       */
 143      public function startTls()
 144      {
 145          if ($this->connected && !$this->secure) {
 146              if (defined('STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT')) {
 147                  $mode = STREAM_CRYPTO_METHOD_TLS_CLIENT
 148                      | STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
 149                      | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
 150                      | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
 151              } else {
 152                  $mode = STREAM_CRYPTO_METHOD_TLS_CLIENT;
 153              }
 154              if (@stream_socket_enable_crypto($this->_stream, true, $mode) === true) {
 155                  $this->_secure = true;
 156                  return true;
 157              }
 158          }
 159  
 160          return false;
 161      }
 162  
 163      /**
 164       * Close the connection.
 165       */
 166      public function close()
 167      {
 168          if ($this->connected) {
 169              @fclose($this->_stream);
 170              $this->_connected = $this->_secure = false;
 171              $this->_stream = null;
 172          }
 173      }
 174  
 175      /**
 176       * Returns information about the connection.
 177       *
 178       * Currently returns four entries in the result array:
 179       *  - timed_out (bool): The socket timed out waiting for data
 180       *  - blocked (bool): The socket was blocked
 181       *  - eof (bool): Indicates EOF event
 182       *  - unread_bytes (int): Number of bytes left in the socket buffer
 183       *
 184       * @throws Horde\Socket\Client\Exception
 185       * @return array  Information about existing socket resource.
 186       */
 187      public function getStatus()
 188      {
 189          $this->_checkStream();
 190          return stream_get_meta_data($this->_stream);
 191      }
 192  
 193      /**
 194       * Returns a line of data.
 195       *
 196       * @param int $size  Reading ends when $size - 1 bytes have been read,
 197       *                   or a newline or an EOF (whichever comes first).
 198       *
 199       * @throws Horde\Socket\Client\Exception
 200       * @return string  $size bytes of data from the socket
 201       */
 202      public function gets($size)
 203      {
 204          $this->_checkStream();
 205          $data = @fgets($this->_stream, $size);
 206          if ($data === false) {
 207              throw new Client\Exception('Error reading data from socket');
 208          }
 209          return $data;
 210      }
 211  
 212      /**
 213       * Returns a specified amount of data.
 214       *
 215       * @param integer $size  The number of bytes to read from the socket.
 216       *
 217       * @throws Horde\Socket\Client\Exception
 218       * @return string  $size bytes of data from the socket.
 219       */
 220      public function read($size)
 221      {
 222          $this->_checkStream();
 223          $data = @fread($this->_stream, $size);
 224          if ($data === false) {
 225              throw new Client\Exception('Error reading data from socket');
 226          }
 227          return $data;
 228      }
 229  
 230      /**
 231       * Writes data to the stream.
 232       *
 233       * @param string $data  Data to write.
 234       *
 235       * @throws Horde\Socket\Client\Exception
 236       */
 237      public function write($data)
 238      {
 239          $this->_checkStream();
 240          if (!@fwrite($this->_stream, $data)) {
 241              $meta_data = $this->getStatus();
 242              if (!empty($meta_data['timed_out'])) {
 243                  throw new Client\Exception('Timed out writing data to socket');
 244              }
 245              throw new Client\Exception('Error writing data to socket');
 246          }
 247      }
 248  
 249      /* Internal methods. */
 250  
 251      /**
 252       * Connect to the remote server.
 253       *
 254       * @see __construct()
 255       *
 256       * @throws Horde\Socket\Client\Exception
 257       */
 258      protected function _connect(
 259          $host, $port, $timeout, $secure, $context, $retries = 0
 260      )
 261      {
 262          $conn = '';
 263          if (!strpos($host, '://')) {
 264              switch (strval($secure)) {
 265              case 'ssl':
 266              case 'sslv2':
 267              case 'sslv3':
 268                  $conn = $secure . '://';
 269                  $this->_secure = true;
 270                  break;
 271  
 272              case 'tlsv1':
 273                  $conn = 'tls://';
 274                  $this->_secure = true;
 275                  break;
 276  
 277              case 'tls':
 278              default:
 279                  $conn = 'tcp://';
 280                  break;
 281              }
 282          }
 283          $conn .= $host;
 284          if ($port) {
 285              $conn .= ':' . $port;
 286          }
 287  
 288          $this->_stream = @stream_socket_client(
 289              $conn,
 290              $error_number,
 291              $error_string,
 292              $timeout,
 293              STREAM_CLIENT_CONNECT,
 294              stream_context_create($context)
 295          );
 296  
 297          if ($this->_stream === false) {
 298              /* From stream_socket_client() page: a function return of false,
 299               * with an error code of 0, indicates a "problem initializing the
 300               * socket". These kind of issues are seen on the same server
 301               * (and even the same user account) as sucessful connections, so
 302               * these are likely transient issues. Retry up to 3 times in these
 303               * instances. */
 304              if (!$error_number && ($retries < 3)) {
 305                  return $this->_connect($host, $port, $timeout, $secure, $context, ++$retries);
 306              }
 307  
 308              $e = new Client\Exception(
 309                  'Error connecting to server.'
 310              );
 311              $e->details = sprintf("[%u] %s", $error_number, $error_string);
 312              throw $e;
 313          }
 314  
 315          stream_set_timeout($this->_stream, $timeout);
 316  
 317          if (function_exists('stream_set_read_buffer')) {
 318              stream_set_read_buffer($this->_stream, 0);
 319          }
 320          stream_set_write_buffer($this->_stream, 0);
 321  
 322          $this->_connected = true;
 323      }
 324  
 325      /**
 326       * Throws an exception is the stream is not a resource.
 327       *
 328       * @throws Horde\Socket\Client\Exception
 329       */
 330      protected function _checkStream()
 331      {
 332          if (!is_resource($this->_stream)) {
 333              throw new Client\Exception('Not connected');
 334          }
 335      }
 336  
 337  }