xref: /plugin/pureldap/vendor/freedsx/socket/src/FreeDSx/Socket/Socket.php (revision dad993c57a70866aa1db59c43f043769c2eb7ed0)
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