1<?php 2/** 3 * This file is part of the FreeDSx Socket package. 4 * 5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace FreeDSx\Socket; 12 13use FreeDSx\Socket\Exception\ConnectionException; 14 15/** 16 * Represents a generic socket. 17 * 18 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 19 */ 20class Socket 21{ 22 /** 23 * Supported transport types. 24 */ 25 public const TRANSPORTS = [ 26 'tcp', 27 'udp', 28 ]; 29 30 /** 31 * @var bool 32 */ 33 protected $isEncrypted = false; 34 35 /** 36 * @var resource|null 37 */ 38 protected $socket; 39 40 /** 41 * @var resource|null 42 */ 43 protected $context; 44 45 /** 46 * @var string 47 */ 48 protected $errorMessage; 49 50 /** 51 * @var int 52 */ 53 protected $errorNumber; 54 55 /** 56 * @var array 57 */ 58 protected $sslOptsMap = [ 59 'ssl_allow_self_signed' => 'allow_self_signed', 60 'ssl_ca_cert' => 'cafile', 61 'ssl_crypto_type' => 'crypto_type', 62 'ssl_peer_name' => 'peer_name', 63 'ssl_cert' => 'local_cert', 64 'ssl_cert_key' => 'local_pk', 65 'ssl_cert_passphrase' => 'passphrase', 66 ]; 67 68 /** 69 * @var array 70 */ 71 protected $sslOpts = [ 72 'allow_self_signed' => false, 73 'verify_peer' => true, 74 'verify_peer_name' => true, 75 'capture_peer_cert' => true, 76 'capture_peer_cert_chain' => true, 77 ]; 78 79 /** 80 * @var array 81 */ 82 protected $options = [ 83 'transport' => 'tcp', 84 'port' => 389, 85 'use_ssl' => false, 86 'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLS_CLIENT, 87 'ssl_validate_cert' => true, 88 'ssl_allow_self_signed' => null, 89 'ssl_ca_cert' => null, 90 'ssl_peer_name' => null, 91 'timeout_connect' => 3, 92 'timeout_read' => 15, 93 'buffer_size' => 8192, 94 ]; 95 96 /** 97 * @param resource|null $resource 98 * @param array $options 99 */ 100 public function __construct($resource = null, array $options = []) 101 { 102 $this->socket = $resource; 103 $this->options = \array_merge($this->options, $options); 104 if (!\in_array($this->options['transport'], self::TRANSPORTS, true)) { 105 throw new \RuntimeException(sprintf( 106 'The transport "%s" is not valid. It must be one of: %s', 107 $this->options['transport'], 108 implode(',', self::TRANSPORTS) 109 )); 110 } 111 if ($this->socket !== null) { 112 $this->setStreamOpts(); 113 } 114 } 115 116 /** 117 * @param bool $block 118 * @return string|false 119 */ 120 public function read(bool $block = true) 121 { 122 $data = false; 123 124 \stream_set_blocking($this->socket, $block); 125 while (\strlen((string) ($buffer = \fread($this->socket, $this->options['buffer_size']))) > 0) { 126 $data .= $buffer; 127 if ($block) { 128 $block = false; 129 \stream_set_blocking($this->socket, false); 130 } 131 } 132 \stream_set_blocking($this->socket, true); 133 134 return $data; 135 } 136 137 /** 138 * @param string $data 139 * @return $this 140 */ 141 public function write(string $data) 142 { 143 @\fwrite($this->socket, $data); 144 145 return $this; 146 } 147 148 /** 149 * @param bool $block 150 * @return $this 151 */ 152 public function block(bool $block) 153 { 154 \stream_set_blocking($this->socket, $block); 155 156 return $this; 157 } 158 159 /** 160 * @return bool 161 */ 162 public function isConnected() : bool 163 { 164 return $this->socket !== null && !@\feof($this->socket); 165 } 166 167 /** 168 * @return bool 169 */ 170 public function isEncrypted() : bool 171 { 172 return $this->isEncrypted; 173 } 174 175 /** 176 * @return $this 177 */ 178 public function close() 179 { 180 if ($this->socket !== null) { 181 \stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); 182 } 183 $this->socket = null; 184 $this->isEncrypted = false; 185 $this->context = null; 186 187 return $this; 188 } 189 190 /** 191 * Enable/Disable encryption on the TCP connection stream. 192 * 193 * @param bool $encrypt 194 * @return $this 195 * @throws ConnectionException 196 */ 197 public function encrypt(bool $encrypt) 198 { 199 \stream_set_blocking($this->socket, true); 200 $result = \stream_socket_enable_crypto($this->socket, $encrypt, $this->options['ssl_crypto_type']); 201 \stream_set_blocking($this->socket, false); 202 203 if ((bool) $result == false) { 204 throw new ConnectionException(sprintf( 205 'Unable to %s encryption on TCP connection. %s', 206 $encrypt ? 'enable' : 'disable', 207 $this->errorMessage 208 )); 209 } 210 $this->isEncrypted = $encrypt; 211 212 return $this; 213 } 214 215 /** 216 * @param string $host 217 * @return $this 218 * @throws ConnectionException 219 */ 220 public function connect(string $host) 221 { 222 $transport = $this->options['transport']; 223 if ($transport === 'tcp' && (bool) $this->options['use_ssl'] === true) { 224 $transport = 'ssl'; 225 } 226 $uri = $transport.'://'.$host.':'.$this->options['port']; 227 228 $socket = @\stream_socket_client( 229 $uri, 230 $this->errorNumber, 231 $this->errorMessage, 232 $this->options['timeout_connect'], 233 STREAM_CLIENT_CONNECT, 234 $this->createSocketContext() 235 ); 236 if ($socket === false) { 237 throw new ConnectionException(sprintf( 238 'Unable to connect to %s: %s', 239 $host, 240 $this->errorMessage 241 )); 242 } 243 $this->socket = $socket; 244 $this->setStreamOpts(); 245 $this->isEncrypted = $this->options['use_ssl']; 246 247 return $this; 248 } 249 250 /** 251 * Get the options set for the socket. 252 * 253 * @return array 254 */ 255 public function getOptions() : array 256 { 257 return $this->options; 258 } 259 260 /** 261 * Create a socket by connecting to a specific host. 262 * 263 * @param string $host 264 * @param array $options 265 * @return Socket 266 * @throws ConnectionException 267 */ 268 public static function create(string $host, array $options = []) : Socket 269 { 270 return (new self(null, $options))->connect($host); 271 } 272 273 /** 274 * Create a TCP based socket. 275 * 276 * @param string $host 277 * @param array $options 278 * @return Socket 279 * @throws ConnectionException 280 */ 281 public static function tcp(string $host, array $options = []) : Socket 282 { 283 return self::create($host, \array_merge($options, ['transport' => 'tcp'])); 284 } 285 286 /** 287 * Create a UDP based socket. 288 * 289 * @param string $host 290 * @param array $options 291 * @return Socket 292 * @throws ConnectionException 293 */ 294 public static function udp(string $host, array $options = []) : Socket 295 { 296 return self::create($host, \array_merge($options, [ 297 'transport' => 'udp', 298 'buffer_size' => 65507, 299 ])); 300 } 301 302 /** 303 * @return resource 304 */ 305 protected function createSocketContext() 306 { 307 $sslOpts = $this->sslOpts; 308 foreach ($this->sslOptsMap as $optName => $sslOptsName) { 309 if (isset($this->options[$optName])) { 310 $sslOpts[$sslOptsName] = $this->options[$optName]; 311 } 312 } 313 if ($this->options['ssl_validate_cert'] === false) { 314 $sslOpts = \array_merge($sslOpts, [ 315 'allow_self_signed' => true, 316 'verify_peer' => false, 317 'verify_peer_name' => false, 318 ]); 319 } 320 $this->context = \stream_context_create([ 321 'ssl' => $sslOpts, 322 ]); 323 324 return $this->context; 325 } 326 327 /** 328 * Sets options on the stream that must be done after it is a resource. 329 */ 330 protected function setStreamOpts() : void 331 { 332 \stream_set_timeout($this->socket, $this->options['timeout_read']); 333 } 334} 335