10b3fd2d3SAndreas Gohr<?php 20b3fd2d3SAndreas Gohr/** 30b3fd2d3SAndreas Gohr * This file is part of the FreeDSx Socket package. 40b3fd2d3SAndreas Gohr * 50b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 60b3fd2d3SAndreas Gohr * 70b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE 80b3fd2d3SAndreas Gohr * file that was distributed with this source code. 90b3fd2d3SAndreas Gohr */ 100b3fd2d3SAndreas Gohr 110b3fd2d3SAndreas Gohrnamespace FreeDSx\Socket; 120b3fd2d3SAndreas Gohr 130b3fd2d3SAndreas Gohruse FreeDSx\Socket\Exception\ConnectionException; 140b3fd2d3SAndreas Gohr 150b3fd2d3SAndreas Gohr/** 160b3fd2d3SAndreas Gohr * Represents a generic socket. 170b3fd2d3SAndreas Gohr * 180b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 190b3fd2d3SAndreas Gohr */ 200b3fd2d3SAndreas Gohrclass Socket 210b3fd2d3SAndreas Gohr{ 220b3fd2d3SAndreas Gohr /** 230b3fd2d3SAndreas Gohr * Supported transport types. 240b3fd2d3SAndreas Gohr */ 250b3fd2d3SAndreas Gohr public const TRANSPORTS = [ 260b3fd2d3SAndreas Gohr 'tcp', 270b3fd2d3SAndreas Gohr 'udp', 28*dad993c5SAndreas Gohr 'unix', 290b3fd2d3SAndreas Gohr ]; 300b3fd2d3SAndreas Gohr 310b3fd2d3SAndreas Gohr /** 320b3fd2d3SAndreas Gohr * @var bool 330b3fd2d3SAndreas Gohr */ 340b3fd2d3SAndreas Gohr protected $isEncrypted = false; 350b3fd2d3SAndreas Gohr 360b3fd2d3SAndreas Gohr /** 370b3fd2d3SAndreas Gohr * @var resource|null 380b3fd2d3SAndreas Gohr */ 390b3fd2d3SAndreas Gohr protected $socket; 400b3fd2d3SAndreas Gohr 410b3fd2d3SAndreas Gohr /** 420b3fd2d3SAndreas Gohr * @var resource|null 430b3fd2d3SAndreas Gohr */ 440b3fd2d3SAndreas Gohr protected $context; 450b3fd2d3SAndreas Gohr 460b3fd2d3SAndreas Gohr /** 470b3fd2d3SAndreas Gohr * @var string 480b3fd2d3SAndreas Gohr */ 490b3fd2d3SAndreas Gohr protected $errorMessage; 500b3fd2d3SAndreas Gohr 510b3fd2d3SAndreas Gohr /** 520b3fd2d3SAndreas Gohr * @var int 530b3fd2d3SAndreas Gohr */ 540b3fd2d3SAndreas Gohr protected $errorNumber; 550b3fd2d3SAndreas Gohr 560b3fd2d3SAndreas Gohr /** 570b3fd2d3SAndreas Gohr * @var array 580b3fd2d3SAndreas Gohr */ 590b3fd2d3SAndreas Gohr protected $sslOptsMap = [ 600b3fd2d3SAndreas Gohr 'ssl_allow_self_signed' => 'allow_self_signed', 610b3fd2d3SAndreas Gohr 'ssl_ca_cert' => 'cafile', 620b3fd2d3SAndreas Gohr 'ssl_crypto_type' => 'crypto_type', 630b3fd2d3SAndreas Gohr 'ssl_peer_name' => 'peer_name', 640b3fd2d3SAndreas Gohr 'ssl_cert' => 'local_cert', 650b3fd2d3SAndreas Gohr 'ssl_cert_key' => 'local_pk', 660b3fd2d3SAndreas Gohr 'ssl_cert_passphrase' => 'passphrase', 670b3fd2d3SAndreas Gohr ]; 680b3fd2d3SAndreas Gohr 690b3fd2d3SAndreas Gohr /** 700b3fd2d3SAndreas Gohr * @var array 710b3fd2d3SAndreas Gohr */ 720b3fd2d3SAndreas Gohr protected $sslOpts = [ 730b3fd2d3SAndreas Gohr 'allow_self_signed' => false, 740b3fd2d3SAndreas Gohr 'verify_peer' => true, 750b3fd2d3SAndreas Gohr 'verify_peer_name' => true, 760b3fd2d3SAndreas Gohr 'capture_peer_cert' => true, 770b3fd2d3SAndreas Gohr 'capture_peer_cert_chain' => true, 780b3fd2d3SAndreas Gohr ]; 790b3fd2d3SAndreas Gohr 800b3fd2d3SAndreas Gohr /** 810b3fd2d3SAndreas Gohr * @var array 820b3fd2d3SAndreas Gohr */ 830b3fd2d3SAndreas Gohr protected $options = [ 840b3fd2d3SAndreas Gohr 'transport' => 'tcp', 850b3fd2d3SAndreas Gohr 'port' => 389, 860b3fd2d3SAndreas Gohr 'use_ssl' => false, 870b3fd2d3SAndreas Gohr 'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLS_CLIENT, 880b3fd2d3SAndreas Gohr 'ssl_validate_cert' => true, 890b3fd2d3SAndreas Gohr 'ssl_allow_self_signed' => null, 900b3fd2d3SAndreas Gohr 'ssl_ca_cert' => null, 910b3fd2d3SAndreas Gohr 'ssl_peer_name' => null, 920b3fd2d3SAndreas Gohr 'timeout_connect' => 3, 930b3fd2d3SAndreas Gohr 'timeout_read' => 15, 940b3fd2d3SAndreas Gohr 'buffer_size' => 8192, 950b3fd2d3SAndreas Gohr ]; 960b3fd2d3SAndreas Gohr 970b3fd2d3SAndreas Gohr /** 980b3fd2d3SAndreas Gohr * @param resource|null $resource 990b3fd2d3SAndreas Gohr * @param array $options 1000b3fd2d3SAndreas Gohr */ 1010b3fd2d3SAndreas Gohr public function __construct($resource = null, array $options = []) 1020b3fd2d3SAndreas Gohr { 1030b3fd2d3SAndreas Gohr $this->socket = $resource; 1040b3fd2d3SAndreas Gohr $this->options = \array_merge($this->options, $options); 1050b3fd2d3SAndreas Gohr if (!\in_array($this->options['transport'], self::TRANSPORTS, true)) { 1060b3fd2d3SAndreas Gohr throw new \RuntimeException(sprintf( 1070b3fd2d3SAndreas Gohr 'The transport "%s" is not valid. It must be one of: %s', 1080b3fd2d3SAndreas Gohr $this->options['transport'], 1090b3fd2d3SAndreas Gohr implode(',', self::TRANSPORTS) 1100b3fd2d3SAndreas Gohr )); 1110b3fd2d3SAndreas Gohr } 1120b3fd2d3SAndreas Gohr if ($this->socket !== null) { 1130b3fd2d3SAndreas Gohr $this->setStreamOpts(); 1140b3fd2d3SAndreas Gohr } 1150b3fd2d3SAndreas Gohr } 1160b3fd2d3SAndreas Gohr 1170b3fd2d3SAndreas Gohr /** 1180b3fd2d3SAndreas Gohr * @param bool $block 1190b3fd2d3SAndreas Gohr * @return string|false 1200b3fd2d3SAndreas Gohr */ 1210b3fd2d3SAndreas Gohr public function read(bool $block = true) 1220b3fd2d3SAndreas Gohr { 1230b3fd2d3SAndreas Gohr $data = false; 1240b3fd2d3SAndreas Gohr 1250b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, $block); 1260b3fd2d3SAndreas Gohr while (\strlen((string) ($buffer = \fread($this->socket, $this->options['buffer_size']))) > 0) { 1270b3fd2d3SAndreas Gohr $data .= $buffer; 1280b3fd2d3SAndreas Gohr if ($block) { 1290b3fd2d3SAndreas Gohr $block = false; 1300b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, false); 1310b3fd2d3SAndreas Gohr } 1320b3fd2d3SAndreas Gohr } 1330b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, true); 1340b3fd2d3SAndreas Gohr 1350b3fd2d3SAndreas Gohr return $data; 1360b3fd2d3SAndreas Gohr } 1370b3fd2d3SAndreas Gohr 1380b3fd2d3SAndreas Gohr /** 1390b3fd2d3SAndreas Gohr * @param string $data 1400b3fd2d3SAndreas Gohr * @return $this 1410b3fd2d3SAndreas Gohr */ 1420b3fd2d3SAndreas Gohr public function write(string $data) 1430b3fd2d3SAndreas Gohr { 1440b3fd2d3SAndreas Gohr @\fwrite($this->socket, $data); 1450b3fd2d3SAndreas Gohr 1460b3fd2d3SAndreas Gohr return $this; 1470b3fd2d3SAndreas Gohr } 1480b3fd2d3SAndreas Gohr 1490b3fd2d3SAndreas Gohr /** 1500b3fd2d3SAndreas Gohr * @param bool $block 1510b3fd2d3SAndreas Gohr * @return $this 1520b3fd2d3SAndreas Gohr */ 1530b3fd2d3SAndreas Gohr public function block(bool $block) 1540b3fd2d3SAndreas Gohr { 1550b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, $block); 1560b3fd2d3SAndreas Gohr 1570b3fd2d3SAndreas Gohr return $this; 1580b3fd2d3SAndreas Gohr } 1590b3fd2d3SAndreas Gohr 1600b3fd2d3SAndreas Gohr /** 1610b3fd2d3SAndreas Gohr * @return bool 1620b3fd2d3SAndreas Gohr */ 1630b3fd2d3SAndreas Gohr public function isConnected() : bool 1640b3fd2d3SAndreas Gohr { 1650b3fd2d3SAndreas Gohr return $this->socket !== null && !@\feof($this->socket); 1660b3fd2d3SAndreas Gohr } 1670b3fd2d3SAndreas Gohr 1680b3fd2d3SAndreas Gohr /** 1690b3fd2d3SAndreas Gohr * @return bool 1700b3fd2d3SAndreas Gohr */ 1710b3fd2d3SAndreas Gohr public function isEncrypted() : bool 1720b3fd2d3SAndreas Gohr { 1730b3fd2d3SAndreas Gohr return $this->isEncrypted; 1740b3fd2d3SAndreas Gohr } 1750b3fd2d3SAndreas Gohr 1760b3fd2d3SAndreas Gohr /** 1770b3fd2d3SAndreas Gohr * @return $this 1780b3fd2d3SAndreas Gohr */ 1790b3fd2d3SAndreas Gohr public function close() 1800b3fd2d3SAndreas Gohr { 1810b3fd2d3SAndreas Gohr if ($this->socket !== null) { 1820b3fd2d3SAndreas Gohr \stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); 1830b3fd2d3SAndreas Gohr } 1840b3fd2d3SAndreas Gohr $this->socket = null; 1850b3fd2d3SAndreas Gohr $this->isEncrypted = false; 1860b3fd2d3SAndreas Gohr $this->context = null; 1870b3fd2d3SAndreas Gohr 1880b3fd2d3SAndreas Gohr return $this; 1890b3fd2d3SAndreas Gohr } 1900b3fd2d3SAndreas Gohr 1910b3fd2d3SAndreas Gohr /** 1920b3fd2d3SAndreas Gohr * Enable/Disable encryption on the TCP connection stream. 1930b3fd2d3SAndreas Gohr * 1940b3fd2d3SAndreas Gohr * @param bool $encrypt 1950b3fd2d3SAndreas Gohr * @return $this 1960b3fd2d3SAndreas Gohr * @throws ConnectionException 1970b3fd2d3SAndreas Gohr */ 1980b3fd2d3SAndreas Gohr public function encrypt(bool $encrypt) 1990b3fd2d3SAndreas Gohr { 2000b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, true); 2010b3fd2d3SAndreas Gohr $result = \stream_socket_enable_crypto($this->socket, $encrypt, $this->options['ssl_crypto_type']); 2020b3fd2d3SAndreas Gohr \stream_set_blocking($this->socket, false); 2030b3fd2d3SAndreas Gohr 2040b3fd2d3SAndreas Gohr if ((bool) $result == false) { 2050b3fd2d3SAndreas Gohr throw new ConnectionException(sprintf( 2060b3fd2d3SAndreas Gohr 'Unable to %s encryption on TCP connection. %s', 2070b3fd2d3SAndreas Gohr $encrypt ? 'enable' : 'disable', 2080b3fd2d3SAndreas Gohr $this->errorMessage 2090b3fd2d3SAndreas Gohr )); 2100b3fd2d3SAndreas Gohr } 2110b3fd2d3SAndreas Gohr $this->isEncrypted = $encrypt; 2120b3fd2d3SAndreas Gohr 2130b3fd2d3SAndreas Gohr return $this; 2140b3fd2d3SAndreas Gohr } 2150b3fd2d3SAndreas Gohr 2160b3fd2d3SAndreas Gohr /** 2170b3fd2d3SAndreas Gohr * @param string $host 2180b3fd2d3SAndreas Gohr * @return $this 2190b3fd2d3SAndreas Gohr * @throws ConnectionException 2200b3fd2d3SAndreas Gohr */ 2210b3fd2d3SAndreas Gohr public function connect(string $host) 2220b3fd2d3SAndreas Gohr { 2230b3fd2d3SAndreas Gohr $transport = $this->options['transport']; 2240b3fd2d3SAndreas Gohr if ($transport === 'tcp' && (bool) $this->options['use_ssl'] === true) { 2250b3fd2d3SAndreas Gohr $transport = 'ssl'; 2260b3fd2d3SAndreas Gohr } 227*dad993c5SAndreas Gohr 228*dad993c5SAndreas Gohr $uri = $transport . '://' . $host; 229*dad993c5SAndreas Gohr 230*dad993c5SAndreas Gohr if ($transport !== 'unix') { 231*dad993c5SAndreas Gohr $uri .= ':' . $this->options['port']; 232*dad993c5SAndreas Gohr } 2330b3fd2d3SAndreas Gohr 2340b3fd2d3SAndreas Gohr $socket = @\stream_socket_client( 2350b3fd2d3SAndreas Gohr $uri, 2360b3fd2d3SAndreas Gohr $this->errorNumber, 2370b3fd2d3SAndreas Gohr $this->errorMessage, 2380b3fd2d3SAndreas Gohr $this->options['timeout_connect'], 2390b3fd2d3SAndreas Gohr STREAM_CLIENT_CONNECT, 2400b3fd2d3SAndreas Gohr $this->createSocketContext() 2410b3fd2d3SAndreas Gohr ); 2420b3fd2d3SAndreas Gohr if ($socket === false) { 2430b3fd2d3SAndreas Gohr throw new ConnectionException(sprintf( 2440b3fd2d3SAndreas Gohr 'Unable to connect to %s: %s', 2450b3fd2d3SAndreas Gohr $host, 2460b3fd2d3SAndreas Gohr $this->errorMessage 2470b3fd2d3SAndreas Gohr )); 2480b3fd2d3SAndreas Gohr } 2490b3fd2d3SAndreas Gohr $this->socket = $socket; 2500b3fd2d3SAndreas Gohr $this->setStreamOpts(); 2510b3fd2d3SAndreas Gohr $this->isEncrypted = $this->options['use_ssl']; 2520b3fd2d3SAndreas Gohr 2530b3fd2d3SAndreas Gohr return $this; 2540b3fd2d3SAndreas Gohr } 2550b3fd2d3SAndreas Gohr 2560b3fd2d3SAndreas Gohr /** 2570b3fd2d3SAndreas Gohr * Get the options set for the socket. 2580b3fd2d3SAndreas Gohr * 2590b3fd2d3SAndreas Gohr * @return array 2600b3fd2d3SAndreas Gohr */ 2610b3fd2d3SAndreas Gohr public function getOptions() : array 2620b3fd2d3SAndreas Gohr { 2630b3fd2d3SAndreas Gohr return $this->options; 2640b3fd2d3SAndreas Gohr } 2650b3fd2d3SAndreas Gohr 2660b3fd2d3SAndreas Gohr /** 2670b3fd2d3SAndreas Gohr * Create a socket by connecting to a specific host. 2680b3fd2d3SAndreas Gohr * 2690b3fd2d3SAndreas Gohr * @param string $host 2700b3fd2d3SAndreas Gohr * @param array $options 2710b3fd2d3SAndreas Gohr * @return Socket 2720b3fd2d3SAndreas Gohr * @throws ConnectionException 2730b3fd2d3SAndreas Gohr */ 2740b3fd2d3SAndreas Gohr public static function create(string $host, array $options = []) : Socket 2750b3fd2d3SAndreas Gohr { 2760b3fd2d3SAndreas Gohr return (new self(null, $options))->connect($host); 2770b3fd2d3SAndreas Gohr } 2780b3fd2d3SAndreas Gohr 2790b3fd2d3SAndreas Gohr /** 280*dad993c5SAndreas Gohr * Create a UNIX based socket. 281*dad993c5SAndreas Gohr * 282*dad993c5SAndreas Gohr * @param string $file The full path to the unix socket. 283*dad993c5SAndreas Gohr * @param array $options Any additional options. 284*dad993c5SAndreas Gohr * @return Socket 285*dad993c5SAndreas Gohr * @throws ConnectionException 286*dad993c5SAndreas Gohr */ 287*dad993c5SAndreas Gohr public static function unix( 288*dad993c5SAndreas Gohr string $file, 289*dad993c5SAndreas Gohr array $options = [] 290*dad993c5SAndreas Gohr ): Socket { 291*dad993c5SAndreas Gohr return self::create( 292*dad993c5SAndreas Gohr $file, 293*dad993c5SAndreas Gohr \array_merge( 294*dad993c5SAndreas Gohr $options, 295*dad993c5SAndreas Gohr ['transport' => 'unix'] 296*dad993c5SAndreas Gohr ) 297*dad993c5SAndreas Gohr ); 298*dad993c5SAndreas Gohr } 299*dad993c5SAndreas Gohr 300*dad993c5SAndreas Gohr /** 3010b3fd2d3SAndreas Gohr * Create a TCP based socket. 3020b3fd2d3SAndreas Gohr * 3030b3fd2d3SAndreas Gohr * @param string $host 3040b3fd2d3SAndreas Gohr * @param array $options 3050b3fd2d3SAndreas Gohr * @return Socket 3060b3fd2d3SAndreas Gohr * @throws ConnectionException 3070b3fd2d3SAndreas Gohr */ 3080b3fd2d3SAndreas Gohr public static function tcp(string $host, array $options = []) : Socket 3090b3fd2d3SAndreas Gohr { 3100b3fd2d3SAndreas Gohr return self::create($host, \array_merge($options, ['transport' => 'tcp'])); 3110b3fd2d3SAndreas Gohr } 3120b3fd2d3SAndreas Gohr 3130b3fd2d3SAndreas Gohr /** 3140b3fd2d3SAndreas Gohr * Create a UDP based socket. 3150b3fd2d3SAndreas Gohr * 3160b3fd2d3SAndreas Gohr * @param string $host 3170b3fd2d3SAndreas Gohr * @param array $options 3180b3fd2d3SAndreas Gohr * @return Socket 3190b3fd2d3SAndreas Gohr * @throws ConnectionException 3200b3fd2d3SAndreas Gohr */ 3210b3fd2d3SAndreas Gohr public static function udp(string $host, array $options = []) : Socket 3220b3fd2d3SAndreas Gohr { 3230b3fd2d3SAndreas Gohr return self::create($host, \array_merge($options, [ 3240b3fd2d3SAndreas Gohr 'transport' => 'udp', 3250b3fd2d3SAndreas Gohr 'buffer_size' => 65507, 3260b3fd2d3SAndreas Gohr ])); 3270b3fd2d3SAndreas Gohr } 3280b3fd2d3SAndreas Gohr 3290b3fd2d3SAndreas Gohr /** 3300b3fd2d3SAndreas Gohr * @return resource 3310b3fd2d3SAndreas Gohr */ 3320b3fd2d3SAndreas Gohr protected function createSocketContext() 3330b3fd2d3SAndreas Gohr { 3340b3fd2d3SAndreas Gohr $sslOpts = $this->sslOpts; 3350b3fd2d3SAndreas Gohr foreach ($this->sslOptsMap as $optName => $sslOptsName) { 3360b3fd2d3SAndreas Gohr if (isset($this->options[$optName])) { 3370b3fd2d3SAndreas Gohr $sslOpts[$sslOptsName] = $this->options[$optName]; 3380b3fd2d3SAndreas Gohr } 3390b3fd2d3SAndreas Gohr } 3400b3fd2d3SAndreas Gohr if ($this->options['ssl_validate_cert'] === false) { 3410b3fd2d3SAndreas Gohr $sslOpts = \array_merge($sslOpts, [ 3420b3fd2d3SAndreas Gohr 'allow_self_signed' => true, 3430b3fd2d3SAndreas Gohr 'verify_peer' => false, 3440b3fd2d3SAndreas Gohr 'verify_peer_name' => false, 3450b3fd2d3SAndreas Gohr ]); 3460b3fd2d3SAndreas Gohr } 3470b3fd2d3SAndreas Gohr $this->context = \stream_context_create([ 3480b3fd2d3SAndreas Gohr 'ssl' => $sslOpts, 3490b3fd2d3SAndreas Gohr ]); 3500b3fd2d3SAndreas Gohr 3510b3fd2d3SAndreas Gohr return $this->context; 3520b3fd2d3SAndreas Gohr } 3530b3fd2d3SAndreas Gohr 3540b3fd2d3SAndreas Gohr /** 3550b3fd2d3SAndreas Gohr * Sets options on the stream that must be done after it is a resource. 3560b3fd2d3SAndreas Gohr */ 3570b3fd2d3SAndreas Gohr protected function setStreamOpts() : void 3580b3fd2d3SAndreas Gohr { 3590b3fd2d3SAndreas Gohr \stream_set_timeout($this->socket, $this->options['timeout_read']); 3600b3fd2d3SAndreas Gohr } 3610b3fd2d3SAndreas Gohr} 362