Test Setup Failed
Push — master ( 46ef65...ecf71f )
by Sam
05:21
created

Server::__construct()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0729

Importance

Changes 12
Bugs 1 Features 1
Metric Value
cc 5
eloc 22
c 12
b 1
f 1
nc 5
nop 7
dl 0
loc 33
ccs 12
cts 14
cp 0.8571
crap 5.0729
rs 9.2568
1
<?php
2
3
/*
4
 * This file is part of PHP DNS Server.
5
 *
6
 * (c) Yif Swery <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace yswery\DNS;
13
14
use React\Datagram\Socket;
15
use React\Datagram\SocketInterface;
16
use React\EventLoop\LoopInterface;
17
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18
use Symfony\Component\EventDispatcher\Event;
19
use yswery\DNS\Config\FileConfig;
20
use yswery\DNS\Event\ServerExceptionEvent;
21
use yswery\DNS\Event\MessageEvent;
22
use yswery\DNS\Event\QueryReceiveEvent;
23
use yswery\DNS\Event\QueryResponseEvent;
24
use yswery\DNS\Event\ServerStartEvent;
25
use yswery\DNS\Resolver\JsonFileSystemResolver;
26
use yswery\DNS\Resolver\ResolverInterface;
27
use yswery\DNS\Event\Events;
28
use yswery\DNS\Filesystem\FilesystemManager;
29
30
class Server
31
{
32
    /**
33
     * The version of PhpDnsServer we are running.
34
     *
35
     * @var string
36
     */
37
    const VERSION = '1.4.0';
38
39
    /**
40
     * @var EventDispatcherInterface
41
     */
42
    protected $dispatcher;
43
44
    /**
45
     * @var ResolverInterface
46
     */
47
    protected $resolver;
48
49
    /**
50
     * @var int
51
     */
52
    protected $port;
53
54
    /**
55
     * @var string
56
     */
57
    protected $ip;
58
59
    /**
60
     * @var LoopInterface
61
     */
62
    protected $loop;
63
64 7
    /**
65
     * @var FilesystemManager
66 7
     */
67
    private $filesystemManager;
68
69
    /**
70 7
     * @var FileConfig
71 7
     */
72 7
    private $config;
73 7
74
    /**
75 7
     * @var bool
76 7
     */
77
    private $useFilesystem;
78
79
    /**
80
     * @var bool
81 7
     */
82 7
    private $isWindows;
83 7
84
    /**
85
     * Server constructor.
86
     *
87
     * @param ResolverInterface        $resolver
88
     * @param EventDispatcherInterface $dispatcher
89
     * @param FileConfig               $config
90
     * @param string|null              $storageDirectory
91
     * @param bool                     $useFilesystem
92
     * @param string                   $ip
93
     * @param int                      $port
94
     *
95
     * @throws \Exception
96
     */
97
    public function __construct(?ResolverInterface $resolver = null, ?EventDispatcherInterface $dispatcher = null, ?FileConfig $config = null, string $storageDirectory = null, bool $useFilesystem = false, string $ip = '0.0.0.0', int $port = 53)
98
    {
99
        if (!function_exists('socket_create') || !extension_loaded('sockets')) {
100
            throw new \Exception('Socket extension or socket_create() function not found.');
101 4
        }
102
103
        $this->dispatcher = $dispatcher;
104 4
        $this->resolver = $resolver;
105 4
        $this->config = $config;
106
        $this->port = $port;
107
        $this->ip = $ip;
108
        $this->useFilesystem = $useFilesystem;
109 4
110
        // detect os
111
        if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) {
112
            $this->isWindows = true;
113
        } else {
114
            $this->isWindows = false;
115
        }
116
117
        // only register filesystem if we want to use it
118
        if ($useFilesystem) {
119
            $this->filesystemManager = new FilesystemManager($storageDirectory);
120 7
            $this->resolver = new JsonFileSystemResolver($this->filesystemManager);
121
        }
122 7
123 7
        $this->loop = \React\EventLoop\Factory::create();
124
        $factory = new \React\Datagram\Factory($this->loop);
125 7
        $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) {
126 7
            $this->dispatch(Events::SERVER_START, new ServerStartEvent($server));
127 7
            $server->on('message', [$this, 'onMessage']);
128 7
        })->otherwise(function (\Exception $exception) {
129 7
            $this->dispatch(Events::SERVER_START_FAIL, new ServerExceptionEvent($exception));
130
        });
131
    }
132 7
133 7
    /**
134 7
     * Start the server.
135 7
     */
136
    public function start(): void
137 7
    {
138 1
        set_time_limit(0);
139
        $this->loop->run();
140 1
    }
141 1
142 1
    public function run()
143
    {
144 1
        $this->start();
145
    }
146
147
    /**
148
     * This methods gets called each time a query is received.
149
     *
150
     * @param string          $message
151
     * @param string          $address
152
     * @param SocketInterface $socket
153
     */
154
    public function onMessage(string $message, string $address, SocketInterface $socket)
155
    {
156
        try {
157
            $this->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
158
            $socket->send($this->handleQueryFromStream($message, $address), $address);
159
        } catch (\Exception $exception) {
160
            $this->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception));
161
        }
162
    }
163
164
    /**
165
     * Decode a message and return an encoded response.
166
     *
167
     * @param string $buffer
168
     *
169
     * @return string
170
     *
171
     * @throws UnsupportedTypeException
172
     */
173
    public function handleQueryFromStream(string $buffer, ?string $client = null): string
174
    {
175
        $message = Decoder::decodeMessage($buffer);
176
        $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message));
177
178
        $responseMessage = clone $message;
179
        $responseMessage->getHeader()
180
            ->setResponse(true)
181
            ->setRecursionAvailable($this->resolver->allowsRecursion())
182
            ->setAuthoritative($this->isAuthoritative($message->getQuestions()));
183
184
        try {
185 7
            $answers = $this->resolver->getAnswer($responseMessage->getQuestions(), $client);
186
            $responseMessage->setAnswers($answers);
187 7
            $this->needsAdditionalRecords($responseMessage);
188 6
            $this->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
189 6
190
            return Encoder::encodeMessage($responseMessage);
191 1
        } catch (UnsupportedTypeException $e) {
192 1
            $responseMessage
193
                ->setAnswers([])
194 1
                ->getHeader()->setRcode(Header::RCODE_NOT_IMPLEMENTED);
195 1
            $this->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
196
197 1
            return Encoder::encodeMessage($responseMessage);
198 1
        }
199
    }
200
201 6
    /**
202 3
     * @return EventDispatcherInterface
203
     */
204
    public function getDispatcher(): EventDispatcherInterface
205
    {
206 3
        return $this->dispatcher;
207 3
    }
208 3
209 3
    /**
210
     * @return ResolverInterface
211 3
     */
212 3
    public function getResolver(): ResolverInterface
213 3
    {
214 3
        return $this->resolver;
215
    }
216
217 3
    /**
218 3
     * @return int
219
     */
220
    public function getPort(): int
221 7
    {
222
        return $this->port;
223
    }
224
225
    /**
226
     * @return string
227
     */
228 7
    public function getIp(): string
229
    {
230 7
        return $this->ip;
231 1
    }
232
233
    /**
234 6
     * Populate the additional records of a message if required.
235 6
     *
236 6
     * @param Message $message
237
     */
238
    protected function needsAdditionalRecords(Message $message): void
239 6
    {
240
        foreach ($message->getAnswers() as $answer) {
241
            $name = null;
242
            switch ($answer->getType()) {
243
                case RecordTypeEnum::TYPE_NS:
244
                    $name = $answer->getRdata();
245
                    break;
246
                case RecordTypeEnum::TYPE_MX:
247
                    $name = $answer->getRdata()['exchange'];
248 7
                    break;
249
                case RecordTypeEnum::TYPE_SRV:
250 7
                    $name = $answer->getRdata()['target'];
251
                    break;
252
            }
253
254 7
            if (null === $name) {
255
                continue;
256
            }
257
258
            $query = [
259
                (new ResourceRecord())
260
                    ->setQuestion(true)
261
                    ->setType(RecordTypeEnum::TYPE_A)
262
                    ->setName($name),
263
264
                (new ResourceRecord())
265
                    ->setQuestion(true)
266
                    ->setType(RecordTypeEnum::TYPE_AAAA)
267
                    ->setName($name),
268
            ];
269
270
            foreach ($this->resolver->getAnswer($query) as $additional) {
271
                $message->addAdditional($additional);
272
            }
273
        }
274
    }
275
276
    /**
277
     * @param ResourceRecord[] $query
278
     *
279
     * @return bool
280
     */
281
    protected function isAuthoritative(array $query): bool
282
    {
283
        if (empty($query)) {
284
            return false;
285
        }
286
287
        $authoritative = true;
288
        foreach ($query as $rr) {
289
            $authoritative &= $this->resolver->isAuthority($rr->getName());
290
        }
291
292
        return $authoritative;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $authoritative could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
293
    }
294
295
    /**
296
     * @param string     $eventName
297
     * @param Event|null $event
298
     *
299
     * @return Event|null
300
     */
301
    protected function dispatch($eventName, ?Event $event = null): ?Event
302
    {
303
        if (null === $this->dispatcher) {
304
            return null;
305
        }
306
307
        return $this->dispatcher->dispatch($eventName, $event);
0 ignored issues
show
Bug introduced by
$eventName of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
        return $this->dispatcher->dispatch(/** @scrutinizer ignore-type */ $eventName, $event);
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
        return $this->dispatcher->/** @scrutinizer ignore-call */ dispatch($eventName, $event);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
308
    }
309
}
310