1<?php
2
3namespace React\Dns\Resolver;
4
5use React\Dns\Model\Message;
6use React\Dns\Query\ExecutorInterface;
7use React\Dns\Query\Query;
8use React\Dns\RecordNotFoundException;
9
10/**
11 * @see ResolverInterface for the base interface
12 */
13final class Resolver implements ResolverInterface
14{
15    private $executor;
16
17    public function __construct(ExecutorInterface $executor)
18    {
19        $this->executor = $executor;
20    }
21
22    public function resolve($domain)
23    {
24        return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
25            return $ips[array_rand($ips)];
26        });
27    }
28
29    public function resolveAll($domain, $type)
30    {
31        $query = new Query($domain, $type, Message::CLASS_IN);
32        $that = $this;
33
34        return $this->executor->query(
35            $query
36        )->then(function (Message $response) use ($query, $that) {
37            return $that->extractValues($query, $response);
38        });
39    }
40
41    /**
42     * [Internal] extract all resource record values from response for this query
43     *
44     * @param Query   $query
45     * @param Message $response
46     * @return array
47     * @throws RecordNotFoundException when response indicates an error or contains no data
48     * @internal
49     */
50    public function extractValues(Query $query, Message $response)
51    {
52        // reject if response code indicates this is an error response message
53        $code = $response->rcode;
54        if ($code !== Message::RCODE_OK) {
55            switch ($code) {
56                case Message::RCODE_FORMAT_ERROR:
57                    $message = 'Format Error';
58                    break;
59                case Message::RCODE_SERVER_FAILURE:
60                    $message = 'Server Failure';
61                    break;
62                case Message::RCODE_NAME_ERROR:
63                    $message = 'Non-Existent Domain / NXDOMAIN';
64                    break;
65                case Message::RCODE_NOT_IMPLEMENTED:
66                    $message = 'Not Implemented';
67                    break;
68                case Message::RCODE_REFUSED:
69                    $message = 'Refused';
70                    break;
71                default:
72                    $message = 'Unknown error response code ' . $code;
73            }
74            throw new RecordNotFoundException(
75                'DNS query for ' . $query->name . ' returned an error response (' . $message . ')',
76                $code
77            );
78        }
79
80        $answers = $response->answers;
81        $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
82
83        // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
84        if (0 === count($addresses)) {
85            throw new RecordNotFoundException(
86                'DNS query for ' . $query->name . ' did not return a valid answer (NOERROR / NODATA)'
87            );
88        }
89
90        return array_values($addresses);
91    }
92
93    /**
94     * @param \React\Dns\Model\Record[] $answers
95     * @param string                    $name
96     * @param int                       $type
97     * @return array
98     */
99    private function valuesByNameAndType(array $answers, $name, $type)
100    {
101        // return all record values for this name and type (if any)
102        $named = $this->filterByName($answers, $name);
103        $records = $this->filterByType($named, $type);
104        if ($records) {
105            return $this->mapRecordData($records);
106        }
107
108        // no matching records found? check if there are any matching CNAMEs instead
109        $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
110        if ($cnameRecords) {
111            $cnames = $this->mapRecordData($cnameRecords);
112            foreach ($cnames as $cname) {
113                $records = array_merge(
114                    $records,
115                    $this->valuesByNameAndType($answers, $cname, $type)
116                );
117            }
118        }
119
120        return $records;
121    }
122
123    private function filterByName(array $answers, $name)
124    {
125        return $this->filterByField($answers, 'name', $name);
126    }
127
128    private function filterByType(array $answers, $type)
129    {
130        return $this->filterByField($answers, 'type', $type);
131    }
132
133    private function filterByField(array $answers, $field, $value)
134    {
135        $value = strtolower($value);
136        return array_filter($answers, function ($answer) use ($field, $value) {
137            return $value === strtolower($answer->$field);
138        });
139    }
140
141    private function mapRecordData(array $records)
142    {
143        return array_map(function ($record) {
144            return $record->data;
145        }, $records);
146    }
147}
148