Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.00% covered (warning)
56.00%
28 / 50
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PassivePortManager
56.00% covered (warning)
56.00%
28 / 50
33.33% covered (danger)
33.33%
1 / 3
30.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 getAvailablePort
44.74% covered (danger)
44.74%
17 / 38
0.00% covered (danger)
0.00%
0 / 1
26.88
 release
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 removeMocks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2namespace Apie\FtpServer;
3
4use Apie\Core\ApieLib;
5use Apie\FtpServer\Enums\PortStatus;
6use Apie\FtpServer\Factories\MockServer;
7use Apie\FtpServer\Factories\ServerFactoryInterface;
8use Evenement\EventEmitterInterface;
9use React\Socket\ServerInterface;
10use React\Socket\SocketServer;
11use Throwable;
12
13class PassivePortManager
14{
15    /**
16     * @var array<int, SocketServer> $usedPorts
17     */
18    private static array $usedPorts = [];
19
20    /**
21     * @var array<int, int> $errorPorts
22     */
23    private static array $errorPorts = [];
24
25    /**
26     * @var array<int, SocketServer> $releasedServers
27     */
28    private static array $releasedServers = [];
29
30    /**
31     * @codeCoverageIgnore
32     */
33    private function __construct()
34    {
35    }
36
37    public static function getAvailablePort(ServerFactoryInterface $serverFactory, int $minPort, int $maxPort): EventEmitterInterface
38    {
39        /** @vary array<int, PortStatus> $portStatuses */
40        $portStatuses = [];
41        $ports = range($minPort, $maxPort);
42        shuffle($ports); // shuffly ports to avoid security issues
43        foreach ($ports as $port) {
44            if (isset(self::$usedPorts[$port])) {
45                $portStatuses[$port] = PortStatus::InUse;
46                continue;
47            }
48            if ((self::$errorPorts[$port] ?? 0) > ApieLib::getPsrClock()->now()->getTimestamp()) {
49                $portStatuses[$port] = PortStatus::Error;
50                continue;
51            }
52            if (isset(self::$errorPorts[$port])) {
53                unset(self::$errorPorts[$port]);
54            }
55            if (isset(self::$releasedServers[$port])) {
56                $portStatuses[$port] = PortStatus::Available;
57                $server = self::$releasedServers[$port];
58                unset(self::$releasedServers[$port]);
59                self::$usedPorts[$port] = $server;
60                $server->on('close', function () use ($port) {
61                    if (isset(self::$usedPorts[$port])) {
62                        unset(self::$usedPorts[$port]);
63                    }
64                    if (isset(self::$releasedServers[$port])) {
65                        unset(self::$releasedServers[$port]);
66                    }
67                });
68                $server->on('error', function () use ($port) {
69                    self::$errorPorts[$port] = ApieLib::getPsrClock()->now()->getTimestamp() + 60;
70                });
71                return $server;
72            }
73            try {
74                $portStatuses[$port] = PortStatus::Available;
75                self::$usedPorts[$port] = $serverFactory->createServer($port);
76                return self::$usedPorts[$port];
77            } catch (Throwable) {
78                $portStatuses[$port] = PortStatus::Error;
79                self::$errorPorts[$port] = ApieLib::getPsrClock()->now()->getTimestamp() + 60;
80                continue;
81            }
82        }
83        $suffix = '';
84        if (count($portStatuses) < 30) {
85            $suffix = ' Port status: ' . json_encode($portStatuses);
86        }
87        throw new \RuntimeException("No available passive ports in range $minPort-$maxPort$suffix");
88    }
89
90    public static function release(ServerInterface $server): void
91    {
92        $address = $server->getAddress();
93        if ($address === null) {
94            return;
95        }
96        $parts = \explode(':', $address);
97        $port = (int) \array_pop($parts);
98        self::$releasedServers[$port] = $server;
99        unset(self::$usedPorts[$port]);
100    }
101
102    public static function removeMocks(): void
103    {
104        $callback = function (ServerInterface $server): bool {
105            return !($server instanceof MockServer);
106        };
107        self::$releasedServers = array_filter(self::$releasedServers, $callback);
108        self::$usedPorts = array_filter(self::$usedPorts, $callback);
109    }
110}