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 * TCP socket server to accept client connections. 170b3fd2d3SAndreas Gohr * 180b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 190b3fd2d3SAndreas Gohr */ 200b3fd2d3SAndreas Gohrclass SocketServer extends Socket 210b3fd2d3SAndreas Gohr{ 220b3fd2d3SAndreas Gohr /** 23*dad993c5SAndreas Gohr * Supported transport types. 24*dad993c5SAndreas Gohr */ 25*dad993c5SAndreas Gohr public const TRANSPORTS = [ 26*dad993c5SAndreas Gohr 'tcp', 27*dad993c5SAndreas Gohr 'udp', 28*dad993c5SAndreas Gohr 'unix', 29*dad993c5SAndreas Gohr ]; 30*dad993c5SAndreas Gohr 31*dad993c5SAndreas Gohr /** 320b3fd2d3SAndreas Gohr * @var array 330b3fd2d3SAndreas Gohr */ 340b3fd2d3SAndreas Gohr protected $serverOpts = [ 35*dad993c5SAndreas Gohr 'use_ssl' => false, 360b3fd2d3SAndreas Gohr 'ssl_cert' => null, 370b3fd2d3SAndreas Gohr 'ssl_cert_key' => null, 380b3fd2d3SAndreas Gohr 'ssl_cert_passphrase' => null, 390b3fd2d3SAndreas Gohr 'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER | STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLS_SERVER, 400b3fd2d3SAndreas Gohr 'ssl_validate_cert' => false, 410b3fd2d3SAndreas Gohr 'idle_timeout' => 600, 420b3fd2d3SAndreas Gohr ]; 430b3fd2d3SAndreas Gohr 440b3fd2d3SAndreas Gohr /** 450b3fd2d3SAndreas Gohr * @var Socket[] 460b3fd2d3SAndreas Gohr */ 470b3fd2d3SAndreas Gohr protected $clients = []; 480b3fd2d3SAndreas Gohr 490b3fd2d3SAndreas Gohr /** 500b3fd2d3SAndreas Gohr * @param array $options 510b3fd2d3SAndreas Gohr */ 520b3fd2d3SAndreas Gohr public function __construct(array $options = []) 530b3fd2d3SAndreas Gohr { 54*dad993c5SAndreas Gohr parent::__construct( 55*dad993c5SAndreas Gohr null, 56*dad993c5SAndreas Gohr \array_merge( 57*dad993c5SAndreas Gohr $this->serverOpts, 58*dad993c5SAndreas Gohr $options 59*dad993c5SAndreas Gohr ) 60*dad993c5SAndreas Gohr ); 61*dad993c5SAndreas Gohr if (!\in_array($this->options['transport'], self::TRANSPORTS, true)) { 62*dad993c5SAndreas Gohr throw new \RuntimeException(sprintf( 63*dad993c5SAndreas Gohr 'The transport "%s" is not valid. It must be one of: %s', 64*dad993c5SAndreas Gohr $this->options['transport'], 65*dad993c5SAndreas Gohr implode(',', self::TRANSPORTS) 66*dad993c5SAndreas Gohr )); 67*dad993c5SAndreas Gohr } 680b3fd2d3SAndreas Gohr } 690b3fd2d3SAndreas Gohr 700b3fd2d3SAndreas Gohr /** 710b3fd2d3SAndreas Gohr * Create the socket server and bind to a specific port to listen for clients. 720b3fd2d3SAndreas Gohr * 730b3fd2d3SAndreas Gohr * @param string $ip 74*dad993c5SAndreas Gohr * @param int|null $port 750b3fd2d3SAndreas Gohr * @return $this 760b3fd2d3SAndreas Gohr * @throws ConnectionException 770b3fd2d3SAndreas Gohr * @internal param string $ip 780b3fd2d3SAndreas Gohr */ 79*dad993c5SAndreas Gohr public function listen(string $ip, ?int $port): self 800b3fd2d3SAndreas Gohr { 810b3fd2d3SAndreas Gohr $flags = STREAM_SERVER_BIND; 820b3fd2d3SAndreas Gohr if ($this->options['transport'] !== 'udp') { 830b3fd2d3SAndreas Gohr $flags |= STREAM_SERVER_LISTEN; 840b3fd2d3SAndreas Gohr } 85*dad993c5SAndreas Gohr 86*dad993c5SAndreas Gohr $transport = $this->options['transport']; 87*dad993c5SAndreas Gohr if ($transport === 'tcp' && $this->options['use_ssl'] === true) { 88*dad993c5SAndreas Gohr $transport = 'ssl'; 89*dad993c5SAndreas Gohr } 90*dad993c5SAndreas Gohr 91*dad993c5SAndreas Gohr if ($transport !== 'unix' && $port === null) { 92*dad993c5SAndreas Gohr throw new ConnectionException('The port must be set if not using a unix based socket.'); 93*dad993c5SAndreas Gohr } 94*dad993c5SAndreas Gohr 95*dad993c5SAndreas Gohr $uri = $transport.'://'.$ip; 96*dad993c5SAndreas Gohr if ($port !== null && $transport !== 'unix') { 97*dad993c5SAndreas Gohr $uri .= ':' . $port; 98*dad993c5SAndreas Gohr } 99*dad993c5SAndreas Gohr 1000b3fd2d3SAndreas Gohr $socket = @\stream_socket_server( 101*dad993c5SAndreas Gohr $uri, 1020b3fd2d3SAndreas Gohr $this->errorNumber, 1030b3fd2d3SAndreas Gohr $this->errorMessage, 1040b3fd2d3SAndreas Gohr $flags, 1050b3fd2d3SAndreas Gohr $this->createSocketContext() 1060b3fd2d3SAndreas Gohr ); 1070b3fd2d3SAndreas Gohr if ($socket === false) { 1080b3fd2d3SAndreas Gohr throw new ConnectionException(sprintf( 1090b3fd2d3SAndreas Gohr 'Unable to open %s socket (%s): %s', 1100b3fd2d3SAndreas Gohr \strtoupper($this->options['transport']), 1110b3fd2d3SAndreas Gohr $this->errorNumber, 1120b3fd2d3SAndreas Gohr $this->errorMessage 1130b3fd2d3SAndreas Gohr )); 1140b3fd2d3SAndreas Gohr } 1150b3fd2d3SAndreas Gohr $this->socket = $socket; 1160b3fd2d3SAndreas Gohr 1170b3fd2d3SAndreas Gohr return $this; 1180b3fd2d3SAndreas Gohr } 1190b3fd2d3SAndreas Gohr 1200b3fd2d3SAndreas Gohr /** 1210b3fd2d3SAndreas Gohr * @param int $timeout 1220b3fd2d3SAndreas Gohr * @return null|Socket 1230b3fd2d3SAndreas Gohr */ 124*dad993c5SAndreas Gohr public function accept(int $timeout = -1): ?Socket 1250b3fd2d3SAndreas Gohr { 1260b3fd2d3SAndreas Gohr $socket = @\stream_socket_accept($this->socket, $timeout); 1270b3fd2d3SAndreas Gohr if (\is_resource($socket)) { 1280b3fd2d3SAndreas Gohr $socket = new Socket($socket, \array_merge($this->options, [ 1290b3fd2d3SAndreas Gohr 'timeout_read' => $this->options['idle_timeout'] 1300b3fd2d3SAndreas Gohr ])); 1310b3fd2d3SAndreas Gohr $this->clients[] = $socket; 1320b3fd2d3SAndreas Gohr } 1330b3fd2d3SAndreas Gohr 1340b3fd2d3SAndreas Gohr return $socket instanceof Socket ? $socket : null; 1350b3fd2d3SAndreas Gohr } 1360b3fd2d3SAndreas Gohr 1370b3fd2d3SAndreas Gohr /** 1380b3fd2d3SAndreas Gohr * Receive data from a UDP based socket. Optionally get the IP address the data was received from. 1390b3fd2d3SAndreas Gohr * 1400b3fd2d3SAndreas Gohr * @todo Buffer size should be adjustable. Max UDP packet size is 65507. Currently this avoids possible truncation. 1410b3fd2d3SAndreas Gohr * @param null $ipAddress 1420b3fd2d3SAndreas Gohr * @return null|string 1430b3fd2d3SAndreas Gohr */ 1440b3fd2d3SAndreas Gohr public function receive(&$ipAddress = null) 1450b3fd2d3SAndreas Gohr { 1460b3fd2d3SAndreas Gohr $this->block(true); 1470b3fd2d3SAndreas Gohr 148*dad993c5SAndreas Gohr return \stream_socket_recvfrom( 149*dad993c5SAndreas Gohr $this->socket, 150*dad993c5SAndreas Gohr 65507, 151*dad993c5SAndreas Gohr 0, 152*dad993c5SAndreas Gohr $ipAddress 153*dad993c5SAndreas Gohr ); 1540b3fd2d3SAndreas Gohr } 1550b3fd2d3SAndreas Gohr 1560b3fd2d3SAndreas Gohr /** 1570b3fd2d3SAndreas Gohr * @return Socket[] 1580b3fd2d3SAndreas Gohr */ 159*dad993c5SAndreas Gohr public function getClients(): array 1600b3fd2d3SAndreas Gohr { 1610b3fd2d3SAndreas Gohr return $this->clients; 1620b3fd2d3SAndreas Gohr } 1630b3fd2d3SAndreas Gohr 1640b3fd2d3SAndreas Gohr /** 1650b3fd2d3SAndreas Gohr * @param Socket $socket 1660b3fd2d3SAndreas Gohr */ 1670b3fd2d3SAndreas Gohr public function removeClient(Socket $socket): void 1680b3fd2d3SAndreas Gohr { 1690b3fd2d3SAndreas Gohr if (($index = \array_search($socket, $this->clients, true)) !== false) { 1700b3fd2d3SAndreas Gohr unset($this->clients[$index]); 1710b3fd2d3SAndreas Gohr } 1720b3fd2d3SAndreas Gohr } 1730b3fd2d3SAndreas Gohr 1740b3fd2d3SAndreas Gohr /** 1750b3fd2d3SAndreas Gohr * Create the socket server. Binds and listens on a specific port 1760b3fd2d3SAndreas Gohr * 1770b3fd2d3SAndreas Gohr * @param string $ip 178*dad993c5SAndreas Gohr * @param int|null $port 1790b3fd2d3SAndreas Gohr * @param array $options 1800b3fd2d3SAndreas Gohr * @return SocketServer 1810b3fd2d3SAndreas Gohr * @throws ConnectionException 1820b3fd2d3SAndreas Gohr */ 183*dad993c5SAndreas Gohr public static function bind( 184*dad993c5SAndreas Gohr string $ip, 185*dad993c5SAndreas Gohr ?int $port, 186*dad993c5SAndreas Gohr array $options = [] 187*dad993c5SAndreas Gohr ): SocketServer { 188*dad993c5SAndreas Gohr return (new self($options))->listen( 189*dad993c5SAndreas Gohr $ip, 190*dad993c5SAndreas Gohr $port 191*dad993c5SAndreas Gohr ); 1920b3fd2d3SAndreas Gohr } 1930b3fd2d3SAndreas Gohr 1940b3fd2d3SAndreas Gohr /** 1950b3fd2d3SAndreas Gohr * Create a TCP based socket server. 1960b3fd2d3SAndreas Gohr * 1970b3fd2d3SAndreas Gohr * @param string $ip 1980b3fd2d3SAndreas Gohr * @param int $port 1990b3fd2d3SAndreas Gohr * @param array $options 2000b3fd2d3SAndreas Gohr * @return SocketServer 2010b3fd2d3SAndreas Gohr * @throws ConnectionException 2020b3fd2d3SAndreas Gohr */ 203*dad993c5SAndreas Gohr public static function bindTcp( 204*dad993c5SAndreas Gohr string $ip, 205*dad993c5SAndreas Gohr int $port, 206*dad993c5SAndreas Gohr array $options = [] 207*dad993c5SAndreas Gohr ): SocketServer { 208*dad993c5SAndreas Gohr return static::bind( 209*dad993c5SAndreas Gohr $ip, 210*dad993c5SAndreas Gohr $port, 211*dad993c5SAndreas Gohr \array_merge( 212*dad993c5SAndreas Gohr $options, 213*dad993c5SAndreas Gohr ['transport' => 'tcp'] 214*dad993c5SAndreas Gohr ) 215*dad993c5SAndreas Gohr ); 2160b3fd2d3SAndreas Gohr } 2170b3fd2d3SAndreas Gohr 2180b3fd2d3SAndreas Gohr /** 2190b3fd2d3SAndreas Gohr * Created a UDP based socket server. 2200b3fd2d3SAndreas Gohr * 2210b3fd2d3SAndreas Gohr * @param string $ip 2220b3fd2d3SAndreas Gohr * @param int $port 2230b3fd2d3SAndreas Gohr * @param array $options 2240b3fd2d3SAndreas Gohr * @return SocketServer 2250b3fd2d3SAndreas Gohr * @throws ConnectionException 2260b3fd2d3SAndreas Gohr */ 227*dad993c5SAndreas Gohr public static function bindUdp( 228*dad993c5SAndreas Gohr string $ip, 229*dad993c5SAndreas Gohr int $port, 230*dad993c5SAndreas Gohr array $options = [] 231*dad993c5SAndreas Gohr ): SocketServer { 232*dad993c5SAndreas Gohr return static::bind( 233*dad993c5SAndreas Gohr $ip, 234*dad993c5SAndreas Gohr $port, 235*dad993c5SAndreas Gohr \array_merge( 236*dad993c5SAndreas Gohr $options, 237*dad993c5SAndreas Gohr ['transport' => 'udp'] 238*dad993c5SAndreas Gohr ) 239*dad993c5SAndreas Gohr ); 240*dad993c5SAndreas Gohr } 241*dad993c5SAndreas Gohr 242*dad993c5SAndreas Gohr /** 243*dad993c5SAndreas Gohr * Created a UNIX based socket server. 244*dad993c5SAndreas Gohr * 245*dad993c5SAndreas Gohr * @param string $socketFile 246*dad993c5SAndreas Gohr * @param array $options 247*dad993c5SAndreas Gohr * @return SocketServer 248*dad993c5SAndreas Gohr * @throws ConnectionException 249*dad993c5SAndreas Gohr */ 250*dad993c5SAndreas Gohr public static function bindUnix( 251*dad993c5SAndreas Gohr string $socketFile, 252*dad993c5SAndreas Gohr array $options = [] 253*dad993c5SAndreas Gohr ): SocketServer { 254*dad993c5SAndreas Gohr return static::bind( 255*dad993c5SAndreas Gohr $socketFile, 256*dad993c5SAndreas Gohr null, 257*dad993c5SAndreas Gohr \array_merge( 258*dad993c5SAndreas Gohr $options, 259*dad993c5SAndreas Gohr ['transport' => 'unix'] 260*dad993c5SAndreas Gohr ) 261*dad993c5SAndreas Gohr ); 2620b3fd2d3SAndreas Gohr } 2630b3fd2d3SAndreas Gohr} 264