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