Completed
Push — master ( 484f58...46ef65 )
by Sam
03:14
created

Server   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Test Coverage

Coverage 78.56%

Importance

Changes 0
Metric Value
wmc 24
eloc 83
dl 0
loc 228
ccs 66
cts 84
cp 0.7856
rs 10
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isAuthoritative() 0 12 3
B needsAdditionalRecords() 0 34 7
A getPort() 0 3 1
A getDispatcher() 0 3 1
A getResolver() 0 3 1
A getIp() 0 3 1
A start() 0 4 1
A dispatch() 0 7 2
A onMessage() 0 7 2
A __construct() 0 18 3
A handleQueryFromStream() 0 25 2
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\Event\ServerExceptionEvent;
20
use yswery\DNS\Event\MessageEvent;
21
use yswery\DNS\Event\QueryReceiveEvent;
22
use yswery\DNS\Event\QueryResponseEvent;
23
use yswery\DNS\Event\ServerStartEvent;
24
use yswery\DNS\Resolver\ResolverInterface;
25
use yswery\DNS\Event\Events;
26
27
class Server
28
{
29
    /**
30
     * @var EventDispatcherInterface
31
     */
32
    private $dispatcher;
33
34
    /**
35
     * @var ResolverInterface
36
     */
37
    private $resolver;
38
39
    /**
40
     * @var int
41
     */
42
    private $port;
43
44
    /**
45
     * @var string
46
     */
47
    private $ip;
48
49
    /**
50
     * @var LoopInterface
51
     */
52
    private $loop;
53
54
    /**
55
     * Server constructor.
56
     *
57
     * @param ResolverInterface        $resolver
58
     * @param EventDispatcherInterface $dispatcher
59
     * @param string                   $ip
60
     * @param int                      $port
61
     *
62
     * @throws \Exception
63
     */
64 7
    public function __construct(ResolverInterface $resolver, ?EventDispatcherInterface $dispatcher = null, string $ip = '0.0.0.0', int $port = 53)
65
    {
66 7
        if (!function_exists('socket_create') || !extension_loaded('sockets')) {
67
            throw new \Exception('Socket extension or socket_create() function not found.');
68
        }
69
70 7
        $this->dispatcher = $dispatcher;
71 7
        $this->resolver = $resolver;
72 7
        $this->port = $port;
73 7
        $this->ip = $ip;
74
75 7
        $this->loop = \React\EventLoop\Factory::create();
76 7
        $factory = new \React\Datagram\Factory($this->loop);
77
        $factory->createServer($this->ip.':'.$this->port)->then(function (Socket $server) {
78
            $this->dispatch(Events::SERVER_START, new ServerStartEvent($server));
79
            $server->on('message', [$this, 'onMessage']);
80
        })->otherwise(function (\Exception $exception) {
81 7
            $this->dispatch(Events::SERVER_START_FAIL, new ServerExceptionEvent($exception));
82 7
        });
83 7
    }
84
85
    /**
86
     * Start the server.
87
     */
88
    public function start(): void
89
    {
90
        set_time_limit(0);
91
        $this->loop->run();
92
    }
93
94
    /**
95
     * This methods gets called each time a query is received.
96
     *
97
     * @param string          $message
98
     * @param string          $address
99
     * @param SocketInterface $socket
100
     */
101 4
    public function onMessage(string $message, string $address, SocketInterface $socket)
102
    {
103
        try {
104 4
            $this->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
105 4
            $socket->send($this->handleQueryFromStream($message), $address);
106
        } catch (\Exception $exception) {
107
            $this->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception));
108
        }
109 4
    }
110
111
    /**
112
     * Decode a message and return an encoded response.
113
     *
114
     * @param string $buffer
115
     *
116
     * @return string
117
     *
118
     * @throws UnsupportedTypeException
119
     */
120 7
    public function handleQueryFromStream(string $buffer): string
121
    {
122 7
        $message = Decoder::decodeMessage($buffer);
123 7
        $this->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message));
124
125 7
        $responseMessage = clone $message;
126 7
        $responseMessage->getHeader()
127 7
            ->setResponse(true)
128 7
            ->setRecursionAvailable($this->resolver->allowsRecursion())
129 7
            ->setAuthoritative($this->isAuthoritative($message->getQuestions()));
130
131
        try {
132 7
            $answers = $this->resolver->getAnswer($responseMessage->getQuestions());
133 7
            $responseMessage->setAnswers($answers);
134 7
            $this->needsAdditionalRecords($responseMessage);
135 7
            $this->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->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
    /**
243
     * @param string     $eventName
244
     * @param Event|null $event
245
     *
246
     * @return Event|null
247
     */
248 7
    private function dispatch($eventName, ?Event $event = null): ?Event
249
    {
250 7
        if (null === $this->dispatcher) {
251
            return null;
252
        }
253
254 7
        return $this->dispatcher->dispatch($eventName, $event);
255
    }
256
}
257