samuelwilliams /
PHP-DNS-SERVER
| 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
Loading history...
|
|||
| 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); |
|
| 308 | } |
||
| 309 | } |
||
| 310 |