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); |
|||
|
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
Loading history...
|
|||||
| 308 | } |
||||
| 309 | } |
||||
| 310 |