Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.49% covered (warning)
58.49%
31 / 53
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PasvTransfer
58.49% covered (warning)
58.49%
31 / 53
28.57% covered (danger)
28.57%
2 / 7
22.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAddress
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataConnection
52.63% covered (warning)
52.63%
10 / 19
0.00% covered (danger)
0.00%
0 / 1
7.66
 connectOnly
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 send
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 end
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
1.02
1<?php
2namespace Apie\FtpServer\Transfers;
3
4use Apie\FtpServer\Factories\ServerFactoryInterface;
5use Apie\FtpServer\PassivePortManager;
6use React\EventLoop\Loop;
7use React\Promise\Deferred;
8use React\Promise\PromiseInterface;
9use React\Socket\ConnectionInterface;
10use React\Socket\ServerInterface;
11
12class PasvTransfer implements TransferInterface
13{
14    private ServerInterface $dataServer;
15    private ?PromiseInterface $lastAction = null;
16
17    public function __construct(
18        ServerFactoryInterface $serverFactory,
19        private readonly string $passiveMinPort = '49152',
20        private readonly string $passiveMaxPort = '65534',
21    ) {
22        $this->dataServer = PassivePortManager::getAvailablePort(
23            $serverFactory,
24            (int) $passiveMinPort,
25            (int) $passiveMaxPort
26        );
27    }
28
29    public function __destruct()
30    {
31        $this->end();
32    }
33
34    public function getAddress(): string
35    {
36        return $this->dataServer->getAddress();
37    }
38
39    /**
40     * Returns a promise that resolves to the established data connection.
41     */
42    private function getDataConnection(?float $timeout = 2.0): PromiseInterface
43    {
44        if ($this->lastAction) {
45            return $this->lastAction;
46        }
47
48        $deferred = new Deferred();
49
50        $timer = null;
51        if (null !== $timeout) {
52            // Timeout if no connection is made
53            $timer = Loop::get()->addTimer($timeout, function () use ($deferred) {
54                $deferred->reject(new \RuntimeException("Nobody connected with the server within the timeout period."));
55            });
56        }
57
58        $this->dataServer->once('connection', function (ConnectionInterface $conn) use ($deferred, $timer) {
59            if ($timer !== null) {
60                Loop::get()->cancelTimer($timer);
61            }
62            $deferred->resolve($conn);
63        });
64        $this->dataServer->once('close', function () use ($timer) {
65            if ($timer !== null) {
66                Loop::get()->cancelTimer($timer);
67            }
68        });
69
70        $this->lastAction = $deferred->promise();
71        return $this->lastAction;
72    }
73
74    public function connectOnly(): PromiseInterface
75    {
76        if ($this->lastAction) {
77            return $this->lastAction;
78        }
79
80        $deferred = new Deferred();
81        $deferred->resolve(null);
82        return $deferred->promise();
83    }
84
85    public function send(string $data, ?callable $onRejected = null): void
86    {
87        $this->lastAction = $this->getDataConnection()->then(
88            function (ConnectionInterface $conn) use ($data) {
89                $conn->write($data);
90
91                return $conn;
92            },
93            $onRejected
94        );
95    }
96
97    public function end(): void
98    {
99        // Gracefully close the connection and server
100        $promise = $this->getDataConnection(null);
101        $timer = Loop::get()->addTimer(2, function () {
102            PassivePortManager::release($this->dataServer);
103            $this->dataServer->close();
104            $this->lastAction = null;
105        });
106        $promise->then(
107            function (ConnectionInterface $conn) use ($timer) {
108                Loop::get()->cancelTimer($timer);
109                $conn->end();
110            }
111        )->finally(function () {
112            PassivePortManager::release($this->dataServer);
113            $this->lastAction = null;
114        });
115    }
116}