Completed
Push — master ( ecf71f...65af6f )
by Sam
09:47
created

Server::handleQueryFromStream()   A

Complexity

Conditions 2
Paths 6

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 2
eloc 19
c 4
b 0
f 0
nc 6
nop 2
dl 0
loc 25
ccs 18
cts 18
cp 1
crap 2
rs 9.6333
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
    /**
65
     * @var FilesystemManager
66
     */
67
    private $filesystemManager;
68
69
    /**
70
     * @var FileConfig
71
     */
72
    private $config;
73
74
    /**
75
     * @var bool
76
     */
77
    private $useFilesystem;
78
79
    /**
80
     * @var bool
81
     */
82
    private $isWindows;
83
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 7
    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 7
        if (!function_exists('socket_create') || !extension_loaded('sockets')) {
100
            throw new \Exception('Socket extension or socket_create() function not found.');
101
        }
102
103 7
        $this->dispatcher = $dispatcher;
104 7
        $this->resolver = $resolver;
105 7
        $this->config = $config;
106 7
        $this->port = $port;
107 7
        $this->ip = $ip;
108 7
        $this->useFilesystem = $useFilesystem;
109
110
        // detect os
111 7
        if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) {
112
            $this->isWindows = true;
113
        } else {
114 7
            $this->isWindows = false;
115
        }
116
117
        // only register filesystem if we want to use it
118 7
        if ($useFilesystem) {
119
            $this->filesystemManager = new FilesystemManager($storageDirectory);
120
            $this->resolver = new JsonFileSystemResolver($this->filesystemManager);
121
        }
122
123 7
        $this->loop = \React\EventLoop\Factory::create();
124 7
        $factory = new \React\Datagram\Factory($this->loop);
125
        $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) {
126
            $this->dispatch(Events::SERVER_START, new ServerStartEvent($server));
127
            $server->on('message', [$this, 'onMessage']);
128
        })->otherwise(function (\Exception $exception) {
129 7
            $this->dispatch(Events::SERVER_START_FAIL, new ServerExceptionEvent($exception));
130 7
        });
131 7
    }
132
133
    /**
134
     * Start the server.
135
     */
136
    public function start(): void
137
    {
138
        set_time_limit(0);
139
        $this->loop->run();
140
    }
141
142
    public function run()
143
    {
144
        $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 4
    public function onMessage(string $message, string $address, SocketInterface $socket)
155
    {
156
        try {
157 4
            $this->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
158 4
            $socket->send($this->handleQueryFromStream($message, $address), $address);
159
        } catch (\Exception $exception) {
160
            $this->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception));
161
        }
162 4
    }
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 7
    public function handleQueryFromStream(string $buffer, ?string $client = null): string
174
    {
175 7
        $message = Decoder::decodeMessage($buffer);
176 7
        $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message));
177
178 7
        $responseMessage = clone $message;
179 7
        $responseMessage->getHeader()
180 7
            ->setResponse(true)
181 7
            ->setRecursionAvailable($this->resolver->allowsRecursion())
182 7
            ->setAuthoritative($this->isAuthoritative($message->getQuestions()));
183
184
        try {
185 7
            $answers = $this->resolver->getAnswer($responseMessage->getQuestions(), $client);
186 7
            $responseMessage->setAnswers($answers);
187 7
            $this->needsAdditionalRecords($responseMessage);
188 7
            $this->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
189
190 7
            return Encoder::encodeMessage($responseMessage);
191 1
        } catch (UnsupportedTypeException $e) {
192
            $responseMessage
193 1
                ->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
        }
199
    }
200
201
    /**
202
     * @return EventDispatcherInterface
203
     */
204
    public function getDispatcher(): EventDispatcherInterface
205
    {
206
        return $this->dispatcher;
207
    }
208
209
    /**
210
     * @return ResolverInterface
211
     */
212
    public function getResolver(): ResolverInterface
213
    {
214
        return $this->resolver;
215
    }
216
217
    /**
218
     * @return int
219
     */
220
    public function getPort(): int
221
    {
222
        return $this->port;
223
    }
224
225
    /**
226
     * @return string
227
     */
228
    public function getIp(): string
229
    {
230
        return $this->ip;
231
    }
232
233
    /**
234
     * Populate the additional records of a message if required.
235
     *
236
     * @param Message $message
237
     */
238 7
    protected function needsAdditionalRecords(Message $message): void
239
    {
240 7
        foreach ($message->getAnswers() as $answer) {
241 6
            $name = null;
242 6
            switch ($answer->getType()) {
243 6
                case RecordTypeEnum::TYPE_NS:
244 1
                    $name = $answer->getRdata();
245 1
                    break;
246 5
                case RecordTypeEnum::TYPE_MX:
247 1
                    $name = $answer->getRdata()['exchange'];
248 1
                    break;
249 4
                case RecordTypeEnum::TYPE_SRV:
250 1
                    $name = $answer->getRdata()['target'];
251 1
                    break;
252
            }
253
254 6
            if (null === $name) {
255 3
                continue;
256
            }
257
258
            $query = [
259 3
                (new ResourceRecord())
260 3
                    ->setQuestion(true)
261 3
                    ->setType(RecordTypeEnum::TYPE_A)
262 3
                    ->setName($name),
263
264 3
                (new ResourceRecord())
265 3
                    ->setQuestion(true)
266 3
                    ->setType(RecordTypeEnum::TYPE_AAAA)
267 3
                    ->setName($name),
268
            ];
269
270 3
            foreach ($this->resolver->getAnswer($query) as $additional) {
271 3
                $message->addAdditional($additional);
272
            }
273
        }
274 7
    }
275
276
    /**
277
     * @param ResourceRecord[] $query
278
     *
279
     * @return bool
280
     */
281 7
    protected function isAuthoritative(array $query): bool
282
    {
283 7
        if (empty($query)) {
284 1
            return false;
285
        }
286
287 6
        $authoritative = true;
288 6
        foreach ($query as $rr) {
289 6
            $authoritative &= $this->resolver->isAuthority($rr->getName());
290
        }
291
292 6
        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 7
    protected function dispatch($eventName, ?Event $event = null): ?Event
302
    {
303 7
        if (null === $this->dispatcher) {
304
            return null;
305
        }
306
307 7
        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