Completed
Push — master ( fdcc8b...f8e0a8 )
by Sam
02:22
created

Server::isAuthoritative()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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