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 * TCP socket server to accept client connections.
17 *
18 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
19 */
20class SocketServer extends Socket
21{
22    /**
23     * @var array
24     */
25    protected $serverOpts = [
26        'ssl_cert' => null,
27        'ssl_cert_key' => null,
28        'ssl_cert_passphrase' => null,
29        'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER | STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLS_SERVER,
30        'ssl_validate_cert' => false,
31        'idle_timeout' => 600,
32    ];
33
34    /**
35     * @var Socket[]
36     */
37    protected $clients = [];
38
39    /**
40     * @param array $options
41     */
42    public function __construct(array $options = [])
43    {
44        parent::__construct(null, \array_merge($this->serverOpts, $options));
45    }
46
47    /**
48     * Create the socket server and bind to a specific port to listen for clients.
49     *
50     * @param string $ip
51     * @param int $port
52     * @return $this
53     * @throws ConnectionException
54     * @internal param string $ip
55     */
56    public function listen(string $ip, int $port)
57    {
58        $flags = STREAM_SERVER_BIND;
59        if ($this->options['transport'] !== 'udp') {
60            $flags |= STREAM_SERVER_LISTEN;
61        }
62        $socket = @\stream_socket_server(
63            $this->options['transport'].'://'.$ip.':'.$port,
64            $this->errorNumber,
65            $this->errorMessage,
66            $flags,
67            $this->createSocketContext()
68        );
69        if ($socket === false) {
70            throw new ConnectionException(sprintf(
71                'Unable to open %s socket (%s): %s',
72                \strtoupper($this->options['transport']),
73                $this->errorNumber,
74                $this->errorMessage
75            ));
76        }
77        $this->socket = $socket;
78
79        return $this;
80    }
81
82    /**
83     * @param int $timeout
84     * @return null|Socket
85     */
86    public function accept($timeout = -1) : ?Socket
87    {
88        $socket = @\stream_socket_accept($this->socket, $timeout);
89        if (\is_resource($socket)) {
90            $socket = new Socket($socket, \array_merge($this->options, [
91                'timeout_read' => $this->options['idle_timeout']
92            ]));
93            $this->clients[] = $socket;
94        }
95
96        return $socket instanceof Socket ? $socket : null;
97    }
98
99    /**
100     * Receive data from a UDP based socket. Optionally get the IP address the data was received from.
101     *
102     * @todo Buffer size should be adjustable. Max UDP packet size is 65507. Currently this avoids possible truncation.
103     * @param null $ipAddress
104     * @return null|string
105     */
106    public function receive(&$ipAddress = null)
107    {
108        $this->block(true);
109
110        return \stream_socket_recvfrom($this->socket, 65507, 0, $ipAddress);
111    }
112
113    /**
114     * @return Socket[]
115     */
116    public function getClients()
117    {
118        return $this->clients;
119    }
120
121    /**
122     * @param Socket $socket
123     */
124    public function removeClient(Socket $socket) : void
125    {
126        if (($index = \array_search($socket, $this->clients, true)) !== false) {
127            unset($this->clients[$index]);
128        }
129    }
130
131    /**
132     * Create the socket server. Binds and listens on a specific port
133     *
134     * @param string $ip
135     * @param int $port
136     * @param array $options
137     * @return SocketServer
138     * @throws ConnectionException
139     */
140    public static function bind(string $ip, int $port, array $options = []) : SocketServer
141    {
142        return (new self($options))->listen($ip, $port);
143    }
144
145    /**
146     * Create a TCP based socket server.
147     *
148     * @param string $ip
149     * @param int $port
150     * @param array $options
151     * @return SocketServer
152     * @throws ConnectionException
153     */
154    public static function bindTcp(string $ip, int $port, array $options = []) : SocketServer
155    {
156        return static::bind($ip, $port, \array_merge($options, ['transport' => 'tcp']));
157    }
158
159    /**
160     * Created a UDP based socket server.
161     *
162     * @param string $ip
163     * @param int $port
164     * @param array $options
165     * @return SocketServer
166     * @throws ConnectionException
167     */
168    public static function bindUdp(string $ip, int $port, array $options = []) : SocketServer
169    {
170        return static::bind($ip, $port, \array_merge($options, ['transport' => 'udp']));
171    }
172}
173