Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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_merge_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_TLSv1_0_CLIENT
 148                      | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
 149                      | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
 150              } else {
 151                  $mode = STREAM_CRYPTO_METHOD_TLS_CLIENT;
 152              }
 153              if (@stream_socket_enable_crypto($this->_stream, true, $mode) === true) {
 154                  $this->_secure = true;
 155                  return true;
 156              }
 157          }
 158  
 159          return false;
 160      }
 161  
 162      /**
 163       * Close the connection.
 164       */
 165      public function close()
 166      {
 167          if ($this->connected) {
 168              @fclose($this->_stream);
 169              $this->_connected = $this->_secure = false;
 170              $this->_stream = null;
 171          }
 172      }
 173  
 174      /**
 175       * Returns information about the connection.
 176       *
 177       * Currently returns four entries in the result array:
 178       *  - timed_out (bool): The socket timed out waiting for data
 179       *  - blocked (bool): The socket was blocked
 180       *  - eof (bool): Indicates EOF event
 181       *  - unread_bytes (int): Number of bytes left in the socket buffer
 182       *
 183       * @throws Horde\Socket\Client\Exception
 184       * @return array  Information about existing socket resource.
 185       */
 186      public function getStatus()
 187      {
 188          $this->_checkStream();
 189          return stream_get_meta_data($this->_stream);
 190      }
 191  
 192      /**
 193       * Returns a line of data.
 194       *
 195       * @param int $size  Reading ends when $size - 1 bytes have been read,
 196       *                   or a newline or an EOF (whichever comes first).
 197       *
 198       * @throws Horde\Socket\Client\Exception
 199       * @return string  $size bytes of data from the socket
 200       */
 201      public function gets($size)
 202      {
 203          $this->_checkStream();
 204          $data = @fgets($this->_stream, $size);
 205          if ($data === false) {
 206              throw new Client\Exception('Error reading data from socket');
 207          }
 208          return $data;
 209      }
 210  
 211      /**
 212       * Returns a specified amount of data.
 213       *
 214       * @param integer $size  The number of bytes to read from the socket.
 215       *
 216       * @throws Horde\Socket\Client\Exception
 217       * @return string  $size bytes of data from the socket.
 218       */
 219      public function read($size)
 220      {
 221          $this->_checkStream();
 222          $data = @fread($this->_stream, $size);
 223          if ($data === false) {
 224              throw new Client\Exception('Error reading data from socket');
 225          }
 226          return $data;
 227      }
 228  
 229      /**
 230       * Writes data to the stream.
 231       *
 232       * @param string $data  Data to write.
 233       *
 234       * @throws Horde\Socket\Client\Exception
 235       */
 236      public function write($data)
 237      {
 238          $this->_checkStream();
 239          if (!@fwrite($this->_stream, $data)) {
 240              $meta_data = $this->getStatus();
 241              if (!empty($meta_data['timed_out'])) {
 242                  throw new Client\Exception('Timed out writing data to socket');
 243              }
 244              throw new Client\Exception('Error writing data to socket');
 245          }
 246      }
 247  
 248      /* Internal methods. */
 249  
 250      /**
 251       * Connect to the remote server.
 252       *
 253       * @see __construct()
 254       *
 255       * @throws Horde\Socket\Client\Exception
 256       */
 257      protected function _connect(
 258          $host, $port, $timeout, $secure, $context, $retries = 0
 259      )
 260      {
 261          $conn = '';
 262          if (!strpos($host, '://')) {
 263              switch (strval($secure)) {
 264              case 'ssl':
 265              case 'sslv2':
 266              case 'sslv3':
 267                  $conn = $secure . '://';
 268                  $this->_secure = true;
 269                  break;
 270  
 271              case 'tlsv1':
 272                  $conn = 'tls://';
 273                  $this->_secure = true;
 274                  break;
 275  
 276              case 'tls':
 277              default:
 278                  $conn = 'tcp://';
 279                  break;
 280              }
 281          }
 282          $conn .= $host;
 283          if ($port) {
 284              $conn .= ':' . $port;
 285          }
 286  
 287          $this->_stream = @stream_socket_client(
 288              $conn,
 289              $error_number,
 290              $error_string,
 291              $timeout,
 292              STREAM_CLIENT_CONNECT,
 293              stream_context_create($context)
 294          );
 295  
 296          if ($this->_stream === false) {
 297              /* From stream_socket_client() page: a function return of false,
 298               * with an error code of 0, indicates a "problem initializing the
 299               * socket". These kind of issues are seen on the same server
 300               * (and even the same user account) as sucessful connections, so
 301               * these are likely transient issues. Retry up to 3 times in these
 302               * instances. */
 303              if (!$error_number && ($retries < 3)) {
 304                  return $this->_connect($host, $port, $timeout, $secure, $context, ++$retries);
 305              }
 306  
 307              $e = new Client\Exception(
 308                  'Error connecting to server.'
 309              );
 310              $e->details = sprintf("[%u] %s", $error_number, $error_string);
 311              throw $e;
 312          }
 313  
 314          stream_set_timeout($this->_stream, $timeout);
 315  
 316          if (function_exists('stream_set_read_buffer')) {
 317              stream_set_read_buffer($this->_stream, 0);
 318          }
 319          stream_set_write_buffer($this->_stream, 0);
 320  
 321          $this->_connected = true;
 322      }
 323  
 324      /**
 325       * Throws an exception is the stream is not a resource.
 326       *
 327       * @throws Horde\Socket\Client\Exception
 328       */
 329      protected function _checkStream()
 330      {
 331          if (!is_resource($this->_stream)) {
 332              throw new Client\Exception('Not connected');
 333          }
 334      }
 335  
 336  }