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     * Supported transport types.
24     */
25    public const TRANSPORTS = [
26        'tcp',
27        'udp',
28        'unix',
29    ];
30
31    /**
32     * @var array
33     */
34    protected $serverOpts = [
35        'use_ssl' => false,
36        'ssl_cert' => null,
37        'ssl_cert_key' => null,
38        'ssl_cert_passphrase' => null,
39        'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER | STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLS_SERVER,
40        'ssl_validate_cert' => false,
41        'idle_timeout' => 600,
42    ];
43
44    /**
45     * @var Socket[]
46     */
47    protected $clients = [];
48
49    /**
50     * @param array $options
51     */
52    public function __construct(array $options = [])
53    {
54        parent::__construct(
55            null,
56            \array_merge(
57                $this->serverOpts,
58                $options
59            )
60        );
61        if (!\in_array($this->options['transport'], self::TRANSPORTS, true)) {
62            throw new \RuntimeException(sprintf(
63                'The transport "%s" is not valid. It must be one of: %s',
64                $this->options['transport'],
65                implode(',', self::TRANSPORTS)
66            ));
67        }
68    }
69
70    /**
71     * Create the socket server and bind to a specific port to listen for clients.
72     *
73     * @param string $ip
74     * @param int|null $port
75     * @return $this
76     * @throws ConnectionException
77     * @internal param string $ip
78     */
79    public function listen(string $ip, ?int $port): self
80    {
81        $flags = STREAM_SERVER_BIND;
82        if ($this->options['transport'] !== 'udp') {
83            $flags |= STREAM_SERVER_LISTEN;
84        }
85
86        $transport = $this->options['transport'];
87        if ($transport === 'tcp' && $this->options['use_ssl'] === true) {
88            $transport = 'ssl';
89        }
90
91        if ($transport !== 'unix' && $port === null) {
92            throw new ConnectionException('The port must be set if not using a unix based socket.');
93        }
94
95        $uri = $transport.'://'.$ip;
96        if ($port !== null && $transport !== 'unix') {
97            $uri .= ':' . $port;
98        }
99
100        $socket = @\stream_socket_server(
101            $uri,
102            $this->errorNumber,
103            $this->errorMessage,
104            $flags,
105            $this->createSocketContext()
106        );
107        if ($socket === false) {
108            throw new ConnectionException(sprintf(
109                'Unable to open %s socket (%s): %s',
110                \strtoupper($this->options['transport']),
111                $this->errorNumber,
112                $this->errorMessage
113            ));
114        }
115        $this->socket = $socket;
116
117        return $this;
118    }
119
120    /**
121     * @param int $timeout
122     * @return null|Socket
123     */
124    public function accept(int $timeout = -1): ?Socket
125    {
126        $socket = @\stream_socket_accept($this->socket, $timeout);
127        if (\is_resource($socket)) {
128            $socket = new Socket($socket, \array_merge($this->options, [
129                'timeout_read' => $this->options['idle_timeout']
130            ]));
131            $this->clients[] = $socket;
132        }
133
134        return $socket instanceof Socket ? $socket : null;
135    }
136
137    /**
138     * Receive data from a UDP based socket. Optionally get the IP address the data was received from.
139     *
140     * @todo Buffer size should be adjustable. Max UDP packet size is 65507. Currently this avoids possible truncation.
141     * @param null $ipAddress
142     * @return null|string
143     */
144    public function receive(&$ipAddress = null)
145    {
146        $this->block(true);
147
148        return \stream_socket_recvfrom(
149            $this->socket,
150            65507,
151            0,
152            $ipAddress
153        );
154    }
155
156    /**
157     * @return Socket[]
158     */
159    public function getClients(): array
160    {
161        return $this->clients;
162    }
163
164    /**
165     * @param Socket $socket
166     */
167    public function removeClient(Socket $socket): void
168    {
169        if (($index = \array_search($socket, $this->clients, true)) !== false) {
170            unset($this->clients[$index]);
171        }
172    }
173
174    /**
175     * Create the socket server. Binds and listens on a specific port
176     *
177     * @param string $ip
178     * @param int|null $port
179     * @param array $options
180     * @return SocketServer
181     * @throws ConnectionException
182     */
183    public static function bind(
184        string $ip,
185        ?int $port,
186        array $options = []
187    ): SocketServer {
188        return (new self($options))->listen(
189            $ip,
190            $port
191        );
192    }
193
194    /**
195     * Create a TCP based socket server.
196     *
197     * @param string $ip
198     * @param int $port
199     * @param array $options
200     * @return SocketServer
201     * @throws ConnectionException
202     */
203    public static function bindTcp(
204        string $ip,
205        int $port,
206        array $options = []
207    ): SocketServer {
208        return static::bind(
209            $ip,
210            $port,
211            \array_merge(
212                $options,
213                ['transport' => 'tcp']
214            )
215        );
216    }
217
218    /**
219     * Created a UDP based socket server.
220     *
221     * @param string $ip
222     * @param int $port
223     * @param array $options
224     * @return SocketServer
225     * @throws ConnectionException
226     */
227    public static function bindUdp(
228        string $ip,
229        int $port,
230        array $options = []
231    ): SocketServer {
232        return static::bind(
233            $ip,
234            $port,
235            \array_merge(
236                $options,
237                ['transport' => 'udp']
238            )
239        );
240    }
241
242    /**
243     * Created a UNIX based socket server.
244     *
245     * @param string $socketFile
246     * @param array $options
247     * @return SocketServer
248     * @throws ConnectionException
249     */
250    public static function bindUnix(
251        string $socketFile,
252        array $options = []
253    ): SocketServer {
254        return static::bind(
255            $socketFile,
256            null,
257            \array_merge(
258                $options,
259                ['transport' => 'unix']
260            )
261        );
262    }
263}
264