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
![]() The call to
Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with $event .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||||
308 | } |
||||||
309 | } |
||||||
310 |