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