SocketListener::stop()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Dazzle\Socket;
4
5
use Dazzle\Event\BaseEventEmitter;
6
use Dazzle\Throwable\Exception\Runtime\ReadException;
7
use Dazzle\Throwable\Exception\Logic\InstantiationException;
8
use Dazzle\Throwable\Exception\LogicException;
9
use Dazzle\Loop\LoopAwareTrait;
10
use Dazzle\Loop\LoopInterface;
11
use Error;
12
use Exception;
13
14
class SocketListener extends BaseEventEmitter implements SocketListenerInterface
15
{
16
    use LoopAwareTrait;
17
18
    /**
19
     * @var int
20
     */
21
    const DEFAULT_BACKLOG = 1024;
22
23
    /**
24
     * @var mixed
25
     */
26
    const CONFIG_DEFAULT_SSL = false;
27
28
    /**
29
     * @var mixed
30
     */
31
    const CONFIG_DEFAULT_SSL_METHOD = STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
32
33
    /**
34
     * @var mixed
35
     */
36
    const CONFIG_DEFAULT_SSL_NAME = '';
37
38
    /**
39
     * @var mixed
40
     */
41
    const CONFIG_DEFAULT_SSL_VERIFY_SIGN = false;
42
43
    /**
44
     * @var mixed
45
     */
46
    const CONFIG_DEFAULT_SSL_VERIFY_PEER = false;
47
48
    /**
49
     * @var mixed
50
     */
51
    const CONFIG_DEFAULT_SSL_VERIFY_DEPTH = 10;
52
53
    /**
54
     * @var resource
55
     */
56
    protected $socket;
57
58
    /**
59
     * @var bool
60
     */
61
    protected $started;
62
63
    /**
64
     * @var bool
65
     */
66
    protected $paused;
67
68
    /**
69
     * @var array
70
     */
71
    protected $config;
72
73
    /**
74
     * @var string|resource
75
     */
76
    protected $endpoint;
77
78
    /**
79
     * @param string|resource $endpointOrResource
80
     * @param LoopInterface $loop
81
     * @param mixed[] $config
82
     * @throws InstantiationException
83
     */
84 30
    public function __construct($endpointOrResource, LoopInterface $loop, $config = [])
85
    {
86 30
        $this->configure($config);
87 30
        $this->endpoint = $endpointOrResource;
88 30
        $this->socket = null;
89 30
        $this->loop = $loop;
90 30
        $this->started = false;
91 30
        $this->paused = true;
92 30
    }
93
94
    /**
95
     *
96
     */
97 27
    public function __destruct()
98
    {
99 27
        $this->handleClose();
100
101 27
        parent::__destruct();
102
103 27
        unset($this->socket);
104 27
        unset($this->loop);
105 27
        unset($this->started);
106 27
        unset($this->paused);
107 27
        unset($this->config);
108 27
        unset($this->endpoint);
109 27
    }
110
111
    /**
112
     * @override
113
     * @inheritDoc
114
     */
115 16
    public function start()
116
    {
117 16
        if ($this->isOpen())
118
        {
119 2
            return;
120
        }
121
122
        try
123
        {
124 16
            if (!is_resource($this->endpoint))
125
            {
126 16
                $this->socket = $this->createServer($this->endpoint, $this->config);
127
            }
128
            else
129
            {
130
                $this->socket = &$this->endpoint;
131
            }
132
133 14
            $this->resume();
134 14
            $this->started = true;
135
        }
136 2
        catch (Error $ex)
137
        {
138
            throw new InstantiationException('SocketListener could not be created.', 0, $ex);
139
        }
140 2
        catch (Exception $ex)
141
        {
142 2
            throw new InstantiationException('SocketListener could not be created.', 0, $ex);
143
        }
144 14
    }
145
146
    /**
147
     * @override
148
     * @inheritDoc
149
     */
150
    public function stop()
151
    {
152
        $this->close();
153
    }
154
155
    /**
156
     * @override
157
     * @inheritDoc
158
     */
159 4
    public function getLocalEndpoint()
160
    {
161 4
        return $this->parseEndpoint();
162
    }
163
164
    /**
165
     * @override
166
     * @inheritDoc
167
     */
168 3
    public function getLocalAddress()
169
    {
170 3
        $endpoint = explode('://', $this->getLocalEndpoint(), 2);
171
172 3
        return isset($endpoint[1]) ? $endpoint[1] : $endpoint[0];
173
    }
174
175
    /**
176
     * @override
177
     * @inheritDoc
178
     */
179 1
    public function getLocalHost()
180
    {
181 1
        $address = explode(':', $this->getLocalAddress(), 2);
182
183 1
        return $address[0];
184
    }
185
186
    /**
187
     * @override
188
     * @inheritDoc
189
     */
190 1
    public function getLocalPort()
191
    {
192 1
        $address = explode(':', $this->getLocalAddress(), 2);
193
194 1
        return isset($address[1]) ? $address[1] : '';
195
    }
196
197
    /**
198
     * @override
199
     * @inheritDoc
200
     */
201
    public function getLocalProtocol()
202
    {
203
        $endpoint = explode('://', $this->getLocalEndpoint(), 2);
204
205
        return isset($endpoint[0]) ? $endpoint[0]:'';
206
    }
207
208
    /**
209
     * @override
210
     * @inheritDoc
211
     */
212 1
    public function getResource()
213
    {
214 1
        return $this->socket;
215
    }
216
217
    /**
218
     * @override
219
     * @inheritDoc
220
     */
221 1
    public function getResourceId()
222
    {
223 1
        return (int) $this->socket;
224
    }
225
226
    /**
227
     * @override
228
     * @inheritDoc
229
     */
230 16
    public function getMetadata()
231
    {
232 16
        if ($this->isOpen())
233
        {
234 9
            return stream_get_meta_data($this->socket);
235
        }
236 7
        return [];
237
    }
238
239
    /**
240
     * @override
241
     * @inheritDoc
242
     */
243 14
    public function getStreamType()
244
    {
245 14
        $data = $this->getMetadata();
246
247 14
        return isset($data['stream_type']) ? $data['stream_type'] : 'undefined';
248
    }
249
250
    /**
251
     * @override
252
     * @inheritDoc
253
     */
254 1
    public function getWrapperType()
255
    {
256 1
        $data = $this->getMetadata();
257
258 1
        return isset($data['wrapper_type']) ? $data['wrapper_type'] : 'undefined';
259
    }
260
261
    /**
262
     * @override
263
     * @inheritDoc
264
     */
265 22
    public function isOpen()
266
    {
267 22
        return $this->started;
268
    }
269
270
    /**
271
     * @override
272
     * @inheritDoc
273
     */
274 5
    public function isPaused()
275
    {
276 5
        return $this->paused;
277
    }
278
279
    /**
280
     * @override
281
     * @inheritDoc
282
     */
283
    public function isEncrypted()
284
    {
285
        return isset($this->config['ssl']) && $this->config['ssl'] === true;
286
    }
287
288
    /**
289
     * @override
290
     * @inheritDoc
291
     */
292 5
    public function close()
293
    {
294 5
        if (!$this->isOpen())
295
        {
296
            return;
297
        }
298
299 5
        $this->started = false;
300
301 5
        $this->emit('close', [ $this ]);
302 5
        $this->handleClose();
303 5
        $this->emit('done', [ $this ]);
304 5
    }
305
306
    /**
307
     * @override
308
     * @inheritDoc
309
     */
310 30
    public function pause()
311
    {
312 30
        if (!$this->paused)
313
        {
314 16
            $this->paused = true;
315
316 16
            if (isset($this->loop))
317
            {
318 16
                $this->loop->removeReadStream($this->socket);
319
            }
320
        }
321 30
    }
322
323
    /**
324
     * @override
325
     * @inheritDoc
326
     */
327 16
    public function resume()
328
    {
329 16
        if ($this->paused)
330
        {
331 16
            $this->paused = false;
332
333 16
            if (isset($this->loop))
334
            {
335 16
                $this->loop->addReadStream($this->socket, [ $this, 'handleConnect' ]);
336
            }
337
        }
338 16
    }
339
340
    /**
341
     * Create the server resource.
342
     *
343
     * This method creates server resource for socket connections.
344
     *
345
     * @param string $endpoint
346
     * @param mixed[] $config
347
     * @return resource
348
     * @throws LogicException
349
     */
350 16
    protected function createServer($endpoint, $config = [])
351
    {
352 16
        if (stripos($endpoint, 'unix://') !== false)
353
        {
354 1
            if ($endpoint[7] === DIRECTORY_SEPARATOR)
355
            {
356
                $path = substr($endpoint, 7);
357
            }
358
            else
359
            {
360 1
                $path = getcwd() . DIRECTORY_SEPARATOR . substr($endpoint, 7);
361 1
                $endpoint = 'unix://' . $path;
362
            }
363
364 1
            if (file_exists($path))
365
            {
366
                unlink($path);
367
            }
368
        }
369
370 16
        $ssl = $this->config['ssl'];
371 16
        $name = $this->config['ssl_name'];
372 16
        $verifySign = $this->config['ssl_verify_sign'];
373 16
        $verifyPeer = $this->config['ssl_verify_peer'];
374 16
        $verifyDepth = $this->config['ssl_verify_depth'];
375
376 16
        $backlog = (int) (isset($config['backlog']) ? $config['backlog'] : self::DEFAULT_BACKLOG);
377 16
        $reuseaddr = (bool) (isset($config['reuseaddr']) ? $config['reuseaddr'] : false);
378 16
        $reuseport = (bool) (isset($config['reuseport']) ? $config['reuseport'] : false);
379
380 16
        $context = [];
381 16
        $context['socket'] = [
382 16
            'bindto' => $endpoint,
383 16
            'backlog' => $backlog,
384
            'ipv6_v6only' => true,
385 16
            'so_reuseaddr' => $reuseaddr,
386 16
            'so_reuseport' => $reuseport,
387
        ];
388 16
        $context['ssl'] = [
389 16
            'allow_self_signed' => !$verifySign,
390 16
            'verify_peer' => $verifyPeer,
391 16
            'verify_peer_name' => $verifyPeer,
392 16
            'verify_depth' => $verifyDepth,
393
            'disable_compression' => true,
394 16
            'SNI_enabled' => $name !== '',
395 16
            'SNI_server_name' => $name,
396 16
            'peer_name' => $name,
397
        ];
398
399 16
        if ($ssl && isset($config['ssl_cert']))
400
        {
401 4
            $context['ssl']['local_cert'] = $config['ssl_cert'];
402
        }
403
404 16
        if ($ssl && isset($config['ssl_key']))
405
        {
406 4
            $context['ssl']['local_pk'] = $config['ssl_key'];
407
        }
408
409 16
        if ($ssl && isset($config['ssl_secret']))
410
        {
411 4
            $context['ssl']['passphrase'] = $config['ssl_secret'];
412
        }
413
414 16
        $bitmask = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
415
416 16
        $context = stream_context_create($context);
417
418
        // Error reporting suppressed since stream_socket_server() emits an E_WARNING on failure.
419 16
        $socket = @stream_socket_server(
420 16
            $endpoint,
421 16
            $errno,
422 16
            $errstr,
423 16
            $bitmask,
424 16
            $context
425
        );
426
427 16 View Code Duplication
        if (!$socket || $errno > 0)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
        {
429 2
            throw new LogicException(
430 2
                sprintf('Could not bind socket [%s] because of error [%d; %s]', $endpoint, $errno, $errstr)
431
            );
432
        }
433
434 14
        return $socket;
435
    }
436
437
    /**
438
     * Create the client resource.
439
     *
440
     * This method creates client resource for socket connections.
441
     *
442
     * @param resource $resource
443
     * @param string[] $config
444
     * @return SocketInterface
445
     */
446 3
    protected function createClient($resource, $config = [])
447
    {
448 3
        return new Socket($resource, $this->loop, $config);
0 ignored issues
show
Bug introduced by
It seems like $this->loop can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
449
    }
450
451
    /**
452
     * Handle the new connection.
453
     *
454
     * @internal
455
     */
456 3
    public function handleConnect()
457
    {
458 3
        $socket = @stream_socket_accept($this->socket);
459
460 3
        if ($socket === false)
461
        {
462
            $this->emit('error', [ $this, new ReadException('Socket could not accept new connection.') ]);
463
            return;
464
        }
465
466 3
        $client = null;
467 3
        $ex = null;
468
469
        try
470
        {
471 3
            $client = $this->createClient($socket, [
472 3
                'ssl' => $this->config['ssl'],
473 3
                'ssl_method' => $this->config['ssl_method'],
474
            ]);
475
476 3
            $this->emit('connect', [ $this, $client ]);
477
        }
478
        catch (Error $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
479
        {}
480
        catch (Exception $ex)
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
481
        {}
482
483 3
        if ($ex !== null)
484
        {
485
            $this->handleDisconnect($socket);
486
            $this->emit('error', [ $this, new ReadException('Socket could not wrap new connection!') ]);
487
        }
488 3
    }
489
490
    private function handleDisconnect($resource)
491
    {
492
        if (is_resource($resource))
493
        {
494
            // http://chat.stackoverflow.com/transcript/message/7727858#7727858
495
            stream_socket_shutdown($resource, STREAM_SHUT_RDWR);
496
            stream_set_blocking($resource, 0);
497
            fclose($resource);
498
        }
499
    }
500
501
    /**
502
     * Handle closing event.
503
     *
504
     * @internal
505
     */
506 30
    public function handleClose()
507
    {
508 30
        $this->pause();
509
510 30
        if (is_resource($this->socket))
511
        {
512 14
            if ($this->getStreamType() === Socket::TYPE_UNIX)
513
            {
514
                $path = substr($this->parseEndpoint(), 7);
515
                unlink($path);
516
            }
517 14
            fclose($this->socket);
518
        }
519 30
    }
520
521
    /**
522
     * Configure socket.
523
     *
524
     * @param string[] $config
525
     */
526 30 View Code Duplication
    private function configure($config = [])
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527
    {
528 30
        $this->config = $config;
529
530 30
        $this->configureVariable('ssl');
531 30
        $this->configureVariable('ssl_method');
532 30
        $this->configureVariable('ssl_name');
533 30
        $this->configureVariable('ssl_verify_sign');
534 30
        $this->configureVariable('ssl_verify_peer');
535 30
        $this->configureVariable('ssl_verify_depth');
536 30
    }
537
538
    /**
539
     * Configure static key
540
     *
541
     * @param $configKey
542
     */
543 30
    private function configureVariable($configKey)
544
    {
545 30
        $configStaticKey = 'CONFIG_DEFAULT_' . strtoupper($configKey);
546 30
        $this->config[$configKey] = isset($this->config[$configKey]) ? $this->config[$configKey] : constant("static::$configStaticKey");
547 30
    }
548
549
    /**
550
     * @return string
551
     */
552 4
    private function parseEndpoint()
553
    {
554 4
        if ($this->isOpen())
555
        {
556
            $name = stream_socket_get_name($this->socket, false);
557
            $type = $this->getStreamType();
558
559 View Code Duplication
            switch ($type)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
560
            {
561
                case Socket::TYPE_UNIX:
562
                    $transport = 'unix://';
563
                    $endpoint = $transport . $name;
564
                    break;
565
566
                case Socket::TYPE_TCP:
567
                    $transport = 'tcp://';
568
                    if (substr_count($name, ':') > 1)
569
                    {
570
                        $parts = explode(':', $name);
571
                        $count = count($parts);
572
                        $port = $parts[$count - 1];
573
                        unset($parts[$count - 1]);
574
                        $endpoint = $transport.'[' . implode(':', $parts) . ']:' . $port;
575
                    }
576
                    else
577
                    {
578
                        $endpoint = $transport . $name;
579
                    }
580
                    break;
581
582
                case Socket::TYPE_UDP:
583
                    $transport = 'udp://';
584
                    $endpoint = $transport . $name;
585
                    break;
586
587
                default:
588
                    $endpoint = '';
589
            }
590
591
            return $endpoint;
592
        }
593
        else
594
        {
595 4
            return is_string($this->endpoint) ? $this->endpoint : '';
596
        }
597
    }
598
}
599