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\Event; |
||||
18 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
||||
19 | use yswery\DNS\Config\FileConfig; |
||||
20 | use yswery\DNS\Event\Events; |
||||
21 | use yswery\DNS\Event\MessageEvent; |
||||
22 | use yswery\DNS\Event\QueryReceiveEvent; |
||||
23 | use yswery\DNS\Event\QueryResponseEvent; |
||||
24 | use yswery\DNS\Event\ServerExceptionEvent; |
||||
25 | use yswery\DNS\Event\ServerStartEvent; |
||||
26 | use yswery\DNS\Filesystem\FilesystemManager; |
||||
27 | use yswery\DNS\Resolver\JsonFileSystemResolver; |
||||
28 | use yswery\DNS\Resolver\ResolverInterface; |
||||
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 | case RecordTypeEnum::TYPE_NS: |
||||
244 | 1 | $name = $answer->getRdata(); |
|||
245 | 1 | break; |
|||
246 | case RecordTypeEnum::TYPE_MX: |
||||
247 | 1 | $name = $answer->getRdata()['exchange']; |
|||
248 | 1 | break; |
|||
249 | 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
![]() |
|||||
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
$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
![]() |
|||||
308 | } |
||||
309 | } |
||||
310 |