Completed
Push — master ( 0cd2cd...e2ec22 )
by Sam
02:48
created

Server::handleQueryFromStream()   A

Complexity

Conditions 2
Paths 6

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 19
nc 6
nop 1
dl 0
loc 26
ccs 18
cts 18
cp 1
crap 2
rs 9.6333
c 0
b 0
f 0
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 yswery\DNS\Event\ServerExceptionEvent;
19
use yswery\DNS\Event\MessageEvent;
20
use yswery\DNS\Event\QueryReceiveEvent;
21
use yswery\DNS\Event\QueryResponseEvent;
22
use yswery\DNS\Event\ServerStartEvent;
23
use yswery\DNS\Resolver\ResolverInterface;
24
use yswery\DNS\Event\Events;
25
26
class Server
27
{
28
    /**
29
     * @var EventDispatcherInterface
30
     */
31
    private $dispatcher;
32
33
    /**
34
     * @var ResolverInterface
35
     */
36
    private $resolver;
37
38
    /**
39
     * @var int
40
     */
41
    private $port;
42
43
    /**
44
     * @var string
45
     */
46
    private $ip;
47
48
    /**
49
     * @var LoopInterface
50
     */
51
    private $loop;
52
53
    /**
54
     * Server constructor.
55
     *
56
     * @param ResolverInterface        $resolver
57
     * @param EventDispatcherInterface $dispatcher
58
     * @param string                   $ip
59
     * @param int                      $port
60
     *
61
     * @throws \Exception
62
     */
63 7
    public function __construct(ResolverInterface $resolver, EventDispatcherInterface $dispatcher, string $ip = '0.0.0.0', int $port = 53)
64
    {
65 7
        if (!function_exists('socket_create') || !extension_loaded('sockets')) {
66
            throw new \Exception('Socket extension or socket_create() function not found.');
67
        }
68
69 7
        $this->dispatcher = $dispatcher;
70 7
        $this->resolver = $resolver;
71 7
        $this->port = $port;
72 7
        $this->ip = $ip;
73
74 7
        $this->loop = \React\EventLoop\Factory::create();
75 7
        $factory = new \React\Datagram\Factory($this->loop);
76
        $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) {
77
            $this->dispatcher->dispatch(Events::SERVER_START, new ServerStartEvent($server));
78
            $server->on('message', [$this, 'onMessage']);
79
        })->otherwise(function (\Exception $exception) {
80 7
            $this->dispatcher->dispatch(Events::SERVER_START_FAIL, new ServerExceptionEvent($exception));
81 7
        });
82 7
    }
83
84
    /**
85
     * Start the server.
86
     */
87
    public function start(): void
88
    {
89
        set_time_limit(0);
90
        $this->loop->run();
91
    }
92
93
    /**
94
     * This methods gets called each time a query is received.
95
     *
96
     * @param string          $message
97
     * @param string          $address
98
     * @param SocketInterface $socket
99
     */
100 4
    public function onMessage(string $message, string $address, SocketInterface $socket)
101
    {
102
        try {
103 4
            $this->dispatcher->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
104 4
            $socket->send($this->handleQueryFromStream($message), $address);
105
        } catch (\Exception $exception) {
106
            $this->dispatcher->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception));
107
        }
108 4
    }
109
110
    /**
111
     * Decode a message and return an encoded response.
112
     *
113
     * @param string $buffer
114
     *
115
     * @return string
116
     *
117
     * @throws UnsupportedTypeException
118
     */
119 7
    public function handleQueryFromStream(string $buffer): string
120
    {
121 7
        $message = Decoder::decodeMessage($buffer);
122 7
        $this->dispatcher->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message));
123
124 7
        $responseMessage = clone $message;
125 7
        $responseMessage->getHeader()
126 7
            ->setResponse(true)
127 7
            ->setRecursionAvailable($this->resolver->allowsRecursion())
128 7
            ->setAuthoritative($this->isAuthoritative($message->getQuestions()));
129
130
        try {
131 7
            $answers = $this->resolver->getAnswer($responseMessage->getQuestions());
132 7
            $responseMessage->setAnswers($answers);
133 7
            $this->needsAdditionalRecords($responseMessage);
134
135 7
            $this->dispatcher->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
136
137 7
            return Encoder::encodeMessage($responseMessage);
138 1
        } catch (UnsupportedTypeException $e) {
139
            $responseMessage
140 1
                    ->setAnswers([])
141 1
                    ->getHeader()->setRcode(Header::RCODE_NOT_IMPLEMENTED);
142 1
            $this->dispatcher->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
143
144 1
            return Encoder::encodeMessage($responseMessage);
145
        }
146
    }
147
148
    /**
149
     * @return EventDispatcherInterface
150
     */
151
    public function getDispatcher(): EventDispatcherInterface
152
    {
153
        return $this->dispatcher;
154
    }
155
156
    /**
157
     * @return ResolverInterface
158
     */
159
    public function getResolver(): ResolverInterface
160
    {
161
        return $this->resolver;
162
    }
163
164
    /**
165
     * @return int
166
     */
167
    public function getPort(): int
168
    {
169
        return $this->port;
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    public function getIp(): string
176
    {
177
        return $this->ip;
178
    }
179
180
    /**
181
     * Populate the additional records of a message if required.
182
     *
183
     * @param Message $message
184
     */
185 7
    private function needsAdditionalRecords(Message $message): void
186
    {
187 7
        foreach ($message->getAnswers() as $answer) {
188 6
            $name = null;
189 6
            switch ($answer->getType()) {
190
                case RecordTypeEnum::TYPE_NS:
191 1
                    $name = $answer->getRdata();
192 1
                    break;
193
                case RecordTypeEnum::TYPE_MX:
194 1
                    $name = $answer->getRdata()['exchange'];
195 1
                    break;
196
                case RecordTypeEnum::TYPE_SRV:
197 1
                    $name = $answer->getRdata()['target'];
198 1
                    break;
199
            }
200
201 6
            if (null === $name) {
202 3
                continue;
203
            }
204
205
            $query = [
206 3
                (new ResourceRecord())
207 3
                    ->setQuestion(true)
208 3
                    ->setType(RecordTypeEnum::TYPE_A)
209 3
                    ->setName($name),
210
211 3
                (new ResourceRecord())
212 3
                    ->setQuestion(true)
213 3
                    ->setType(RecordTypeEnum::TYPE_AAAA)
214 3
                    ->setName($name),
215
            ];
216
217 3
            foreach ($this->resolver->getAnswer($query) as $additional) {
218 3
                $message->addAdditional($additional);
219
            }
220
        }
221 7
    }
222
223
    /**
224
     * @param ResourceRecord[] $query
225
     *
226
     * @return bool
227
     */
228 7
    private function isAuthoritative(array $query): bool
229
    {
230 7
        if (empty($query)) {
231 1
            return false;
232
        }
233
234 6
        $authoritative = true;
235 6
        foreach ($query as $rr) {
236 6
            $authoritative &= $this->resolver->isAuthority($rr->getName());
237
        }
238
239 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...
240
    }
241
}
242