Completed
Push — master ( 282f78...6dce89 )
by Sam
05:20
created

Server::needsAdditionalRecords()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 37
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 26
nc 9
nop 1
dl 0
loc 37
rs 8.8817
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
    public function __construct(ResolverInterface $resolver, EventDispatcherInterface $dispatcher, string $ip = '0.0.0.0', int $port = 53)
63
    {
64
        if (!function_exists('socket_create') || !extension_loaded('sockets')) {
65
            throw new \Exception('Socket extension or socket_create() function not found.');
66
        }
67
68
        $this->dispatcher = $dispatcher;
69
        $this->resolver = $resolver;
70
        $this->port = $port;
71
        $this->ip = $ip;
72
73
        $this->loop = \React\EventLoop\Factory::create();
74
        $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
        });
80
    }
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
    public function onMessage(string $message, string $address, SocketInterface $socket)
99
    {
100
        try {
101
            $this->dispatcher->dispatch(Events::MESSAGE, new MessageEvent($socket, $address, $message));
102
            $socket->send($this->handleQueryFromStream($message), $address);
103
        } catch (\Exception $exception) {
104
            $this->dispatcher->dispatch(Events::SERVER_EXCEPTION, new ServerExceptionEvent($exception));
105
        }
106
    }
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
    public function handleQueryFromStream(string $buffer): string
118
    {
119
        $message = Decoder::decodeMessage($buffer);
120
        $this->dispatcher->dispatch(Events::QUERY_RECEIVE, new QueryReceiveEvent($message));
121
122
        $responseMessage = clone $message;
123
        $responseMessage->getHeader()
124
            ->setResponse(true)
125
            ->setRecursionAvailable($this->resolver->allowsRecursion())
126
            ->setAuthoritative($this->resolver->isAuthority($responseMessage->getQuestions()[0]->getName()));
127
128
        try {
129
            $answers = $this->resolver->getAnswer($responseMessage->getQuestions());
130
            $responseMessage->setAnswers($answers);
131
            $this->needsAdditionalRecords($responseMessage);
132
133
            $this->dispatcher->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
134
135
            return Encoder::encodeMessage($responseMessage);
136
        } catch (UnsupportedTypeException $e) {
137
            $responseMessage
138
                    ->setAnswers([])
139
                    ->getHeader()->setRcode(Header::RCODE_NOT_IMPLEMENTED);
140
            $this->dispatcher->dispatch(Events::QUERY_RESPONSE, new QueryResponseEvent($responseMessage));
141
142
            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
    private function needsAdditionalRecords(Message $message): void
184
    {
185
        $additionals = [];
186
        foreach ($message->getAnswers() as $answer) {
187
            $name = null;
188
            switch ($answer->getType()) {
189
                case RecordTypeEnum::TYPE_NS:
190
                    $name = $answer->getRdata();
191
                    break;
192
                case RecordTypeEnum::TYPE_MX:
193
                    $name = $answer->getRdata()['exchange'];
194
                    break;
195
                case RecordTypeEnum::TYPE_SRV:
196
                    $name = $answer->getRdata()['target'];
197
                    break;
198
            }
199
200
            if (null === $name) {
201
                continue;
202
            }
203
204
            $query = [
205
                (new ResourceRecord())
206
                    ->setQuestion(true)
207
                    ->setType(RecordTypeEnum::TYPE_A)
208
                    ->setName($name),
209
210
                (new ResourceRecord())
211
                    ->setQuestion(true)
212
                    ->setType(RecordTypeEnum::TYPE_AAAA)
213
                    ->setName($name),
214
            ];
215
216
            $additionals = array_merge($additionals, $this->resolver->getAnswer($query));
217
        }
218
219
        $message->setAdditionals($additionals);
220
    }
221
}
222