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