dns_record_validator.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. <?php
  2. declare(strict_types=1);
  3. if (!isset($argv[1], $argv[2], $argv[3])) {
  4. $error_json = [
  5. "valid" => false,
  6. "error_message" => "record, rtype, and priority arguments are required",
  7. ];
  8. echo json_encode(
  9. $error_json,
  10. JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
  11. );
  12. exit(1);
  13. }
  14. $record = $argv[1];
  15. $rtype = $argv[2];
  16. $priority = $argv[3];
  17. $known_types = [
  18. "A",
  19. "AAAA",
  20. "NS",
  21. "CNAME",
  22. "MX",
  23. "TXT",
  24. "SRV",
  25. "DNSKEY",
  26. "KEY",
  27. "IPSECKEY",
  28. "PTR",
  29. "SPF",
  30. "TLSA",
  31. "CAA",
  32. "DS",
  33. ];
  34. $valid = true;
  35. $error_message = null;
  36. $cleaned_record = null;
  37. $new_priority = null;
  38. $validateInt = static function ($value, $min, $max) {
  39. return filter_var($value, FILTER_VALIDATE_INT, [
  40. "options" => ["min_range" => $min, "max_range" => $max],
  41. ]);
  42. };
  43. $validateDomain = static function ($value) {
  44. return filter_var(rtrim($value, "."), FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
  45. };
  46. $validateHex = static function ($value) {
  47. return preg_match('/^[A-Fa-f0-9]+$/', $value) === 1;
  48. };
  49. $validateBase64 = static function ($value) {
  50. if ($value === "" || preg_match("/[^A-Za-z0-9+\/=]/", $value)) {
  51. return false;
  52. }
  53. return base64_decode($value, true) !== false;
  54. };
  55. $validatePrintableAscii = static function ($value) {
  56. return !preg_match('/[^\x20-\x7E]/', $value);
  57. };
  58. $validateSrvTarget = static function ($value) use ($validateDomain) {
  59. return $value === "." || $validateDomain($value);
  60. };
  61. if (!in_array($rtype, $known_types, true)) {
  62. $valid = false;
  63. $error_message = "unknown record type for validation: $rtype";
  64. } elseif ($rtype === "A") {
  65. $valid = filter_var($record, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
  66. if (!$valid) {
  67. $error_message = "invalid A record format";
  68. }
  69. } elseif ($rtype === "AAAA") {
  70. $valid = filter_var($record, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
  71. if (!$valid) {
  72. $error_message = "invalid AAAA record format";
  73. }
  74. } elseif ($rtype === "NS" || $rtype === "CNAME" || $rtype === "PTR") {
  75. $valid = $validateDomain($record);
  76. if (!$valid) {
  77. $error_message = "invalid $rtype record format";
  78. }
  79. } elseif ($rtype === "MX") {
  80. $valid = $validateDomain($record);
  81. if (!$valid) {
  82. $error_message = "invalid MX record format";
  83. } else {
  84. if ($priority === "") {
  85. $valid = false;
  86. $error_message = "MX record priority is required";
  87. } else {
  88. $valid = $validateInt($priority, 0, 65535);
  89. if ($valid === false) {
  90. $error_message = "invalid MX record priority format (must be between 0 and 65535)";
  91. }
  92. }
  93. }
  94. } elseif ($rtype === "SRV") {
  95. $parts = preg_split("/\s+/", trim($record));
  96. $parts_count = count($parts);
  97. $target = null;
  98. $port = null;
  99. $weight = null;
  100. $priority_to_use = $priority;
  101. if ($parts_count === 4) {
  102. [$priority_from_value, $weight, $port, $target] = $parts;
  103. $priority_to_use = $priority_from_value;
  104. } elseif ($parts_count === 3) {
  105. if ($validateSrvTarget($parts[0])) {
  106. [$target, $port, $weight] = $parts;
  107. } elseif ($validateSrvTarget($parts[2])) {
  108. [$weight, $port, $target] = $parts;
  109. } else {
  110. $valid = false;
  111. $error_message = "invalid SRV record format (expected target with port and weight)";
  112. }
  113. } else {
  114. $valid = false;
  115. $error_message =
  116. "invalid SRV record format (must contain priority weight port target or target port weight)";
  117. }
  118. if ($valid !== false) {
  119. $priority_validated = $validateInt($priority_to_use, 0, 65535);
  120. $weight_validated = $validateInt($weight, 0, 65535);
  121. $port_validated = $validateInt($port, 0, 65535);
  122. $target_validated = $validateSrvTarget($target ?? "");
  123. if ($priority_validated === false) {
  124. $valid = false;
  125. $error_message = "invalid SRV record priority format (must be between 0 and 65535)";
  126. } elseif ($weight_validated === false) {
  127. $valid = false;
  128. $error_message = "invalid SRV record weight format (must be between 0 and 65535)";
  129. } elseif ($port_validated === false) {
  130. $valid = false;
  131. $error_message = "invalid SRV record port format (must be between 0 and 65535)";
  132. } elseif (!$target_validated) {
  133. $valid = false;
  134. $error_message = "invalid SRV record target format";
  135. } else {
  136. $new_priority = $priority_validated;
  137. $cleaned_record = $weight_validated . " " . $port_validated . " " . $target;
  138. }
  139. }
  140. } elseif ($rtype === "TXT" || $rtype === "SPF") {
  141. if ($record === "") {
  142. $valid = false;
  143. $error_message = "$rtype record cannot be empty";
  144. } elseif (strlen($record) > 65535) {
  145. $valid = false;
  146. $error_message = "$rtype record exceeds maximum length";
  147. } elseif (!$validatePrintableAscii($record)) {
  148. $valid = false;
  149. $error_message = "$rtype record contains non-ASCII characters";
  150. }
  151. } elseif ($rtype === "DNSKEY" || $rtype === "KEY") {
  152. $parts = preg_split("/\s+/", trim($record));
  153. if (count($parts) < 3) {
  154. $valid = false;
  155. $error_message = "invalid $rtype record format (expected flags protocol algorithm [public-key])";
  156. } else {
  157. [$flags, $protocol, $algorithm] = array_slice($parts, 0, 3);
  158. $public_key = implode(" ", array_slice($parts, 3));
  159. $flags_valid = $validateInt($flags, 0, 65535);
  160. $protocol_valid = $validateInt($protocol, 0, 255);
  161. $algorithm_valid = $validateInt($algorithm, 0, 255);
  162. if ($rtype === "DNSKEY" && $protocol !== "3") {
  163. $protocol_valid = false;
  164. }
  165. if ($flags_valid === false || $protocol_valid === false || $algorithm_valid === false) {
  166. $valid = false;
  167. $error_message = "invalid $rtype numeric fields";
  168. } elseif ($rtype === "KEY" && $algorithm === "0") {
  169. if ($public_key !== "") {
  170. $valid = false;
  171. $error_message = "invalid KEY public key for algorithm 0 (must be empty)";
  172. }
  173. } elseif ($public_key === "" || !$validateBase64($public_key)) {
  174. $valid = false;
  175. $error_message = "invalid $rtype public key (must be base64)";
  176. }
  177. }
  178. } elseif ($rtype === "IPSECKEY") {
  179. $parts = preg_split("/\s+/", trim($record));
  180. if (count($parts) < 4) {
  181. $valid = false;
  182. $error_message =
  183. "invalid IPSECKEY record format (expected precedence gateway-type algorithm gateway [public-key])";
  184. } else {
  185. [$precedence, $gateway_type, $algorithm, $gateway] = array_slice($parts, 0, 4);
  186. $public_key = implode(" ", array_slice($parts, 4));
  187. $precedence_valid = $validateInt($precedence, 0, 255);
  188. $gateway_type_valid = $validateInt($gateway_type, 0, 3);
  189. $algorithm_valid = $validateInt($algorithm, 0, 255);
  190. if (
  191. $precedence_valid === false ||
  192. $gateway_type_valid === false ||
  193. $algorithm_valid === false
  194. ) {
  195. $valid = false;
  196. $error_message = "invalid IPSECKEY numeric fields";
  197. } else {
  198. if ($gateway_type === "0") {
  199. if ($gateway !== "." && $gateway !== "") {
  200. $valid = false;
  201. $error_message = "invalid IPSECKEY gateway for type 0";
  202. }
  203. } elseif ($gateway_type === "1") {
  204. if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
  205. $valid = false;
  206. $error_message = "invalid IPSECKEY IPv4 gateway";
  207. }
  208. } elseif ($gateway_type === "2") {
  209. if (!filter_var($gateway, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
  210. $valid = false;
  211. $error_message = "invalid IPSECKEY IPv6 gateway";
  212. }
  213. } else {
  214. if (!$validateDomain($gateway)) {
  215. $valid = false;
  216. $error_message = "invalid IPSECKEY domain gateway";
  217. }
  218. }
  219. if ($valid !== false) {
  220. if ($algorithm === "0") {
  221. if ($public_key !== "") {
  222. $valid = false;
  223. $error_message =
  224. "invalid IPSECKEY public key for algorithm 0 (must be empty)";
  225. }
  226. } elseif ($public_key === "" || !$validateBase64($public_key)) {
  227. $valid = false;
  228. $error_message = "invalid IPSECKEY public key (must be base64)";
  229. }
  230. }
  231. }
  232. }
  233. } elseif ($rtype === "TLSA") {
  234. $parts = preg_split("/\s+/", trim($record));
  235. if (count($parts) < 4) {
  236. $valid = false;
  237. $error_message = "invalid TLSA record format (expected usage selector matching-type data)";
  238. } else {
  239. [$usage, $selector, $matching_type] = array_slice($parts, 0, 3);
  240. $data = implode(" ", array_slice($parts, 3));
  241. $usage_valid = $validateInt($usage, 0, 3);
  242. $selector_valid = $validateInt($selector, 0, 1);
  243. $matching_valid = $validateInt($matching_type, 0, 2);
  244. $data_length = strlen($data);
  245. if ($usage_valid === false || $selector_valid === false || $matching_valid === false) {
  246. $valid = false;
  247. $error_message = "invalid TLSA numeric fields";
  248. } elseif ($data === "" || !$validateHex($data) || $data_length % 2 !== 0) {
  249. $valid = false;
  250. $error_message = "invalid TLSA data";
  251. } elseif ($matching_valid === 1 && $data_length !== 64) {
  252. $valid = false;
  253. $error_message = "invalid TLSA data length for matching type 1";
  254. } elseif ($matching_valid === 2 && $data_length !== 128) {
  255. $valid = false;
  256. $error_message = "invalid TLSA data length for matching type 2";
  257. }
  258. }
  259. } elseif ($rtype === "CAA") {
  260. $parts = preg_split("/\s+/", trim($record), 3);
  261. if (count($parts) < 3) {
  262. $valid = false;
  263. $error_message = "invalid CAA record format (expected flag tag value)";
  264. } else {
  265. [$flag, $tag, $value] = $parts;
  266. $flag_valid = $validateInt($flag, 0, 255);
  267. $tag_valid = preg_match('/^[A-Za-z0-9-]{1,63}$/', $tag);
  268. if ($flag_valid === false || $tag_valid === 0) {
  269. $valid = false;
  270. $error_message = "invalid CAA flag or tag";
  271. } elseif ($value === "") {
  272. $valid = false;
  273. $error_message = "invalid CAA value";
  274. }
  275. }
  276. } elseif ($rtype === "DS") {
  277. $parts = preg_split("/\s+/", trim($record));
  278. if (count($parts) < 4) {
  279. $valid = false;
  280. $error_message = "invalid DS record format (expected keytag algorithm digest-type digest)";
  281. } else {
  282. [$key_tag, $algorithm, $digest_type] = array_slice($parts, 0, 3);
  283. $digest = implode(" ", array_slice($parts, 3));
  284. $key_tag_valid = $validateInt($key_tag, 0, 65535);
  285. $algorithm_valid = $validateInt($algorithm, 0, 255);
  286. $digest_type_valid = $validateInt($digest_type, 0, 255);
  287. $digest_lengths = [1 => 40, 2 => 64, 3 => 64, 4 => 96];
  288. $digest_length = strlen($digest);
  289. if (
  290. $key_tag_valid === false ||
  291. $algorithm_valid === false ||
  292. $digest_type_valid === false
  293. ) {
  294. $valid = false;
  295. $error_message = "invalid DS numeric fields";
  296. } elseif ($digest === "" || !$validateHex($digest) || $digest_length % 2 !== 0) {
  297. $valid = false;
  298. $error_message = "invalid DS digest";
  299. } elseif (
  300. array_key_exists((int) $digest_type, $digest_lengths) &&
  301. $digest_length !== $digest_lengths[(int) $digest_type]
  302. ) {
  303. $valid = false;
  304. $error_message = "invalid DS digest length for type $digest_type";
  305. }
  306. }
  307. } else {
  308. $valid = false;
  309. $error_message = "validation not implemented for record type: $rtype";
  310. }
  311. $json = ["valid" => $valid !== false];
  312. if ($error_message !== null) {
  313. $json["error_message"] = $error_message;
  314. }
  315. if ($cleaned_record !== null) {
  316. $json["cleaned_record"] = $cleaned_record;
  317. }
  318. if ($new_priority !== null) {
  319. $json["new_priority"] = $new_priority;
  320. }
  321. echo json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);