├── .gitignore ├── .gitmodules ├── .travis.yml ├── README.md ├── api.php ├── config.default.php ├── definitions.php ├── docs └── example_config.php.txt ├── examples └── ansible │ ├── README │ ├── create_record.yml │ ├── delete_record.yml │ └── vars.yml ├── favicon.ico ├── favicon.png ├── includes ├── audit.php ├── caching.php ├── caching_memcache.php ├── caching_memcached.php ├── common.php ├── common_ui.php ├── debug.php ├── fatal.php ├── fatal_api.php ├── logging.php ├── login.php ├── menu.php ├── modify.php ├── notifications.php ├── nsupdate.php ├── record_list.php ├── tab_batch.php ├── tab_edit.php ├── tab_manage.php ├── tab_overview.php ├── validator.php └── zones.php ├── index.php ├── style.css └── util ├── mktar ├── testdata ├── invalid.zone ├── valid.zone1 └── valid.zone2 ├── unit.php └── unit_api.php /.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "psf"] 2 | path = psf 3 | url = https://github.com/benapetr/psf 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | 6 | script: 7 | - bash psf/tools/lint.sh 8 | - php util/unit.php 9 | 10 | notifications: 11 | irc: 12 | channels: 13 | - "irc.tm-irc.org#petan" 14 | on_success: change 15 | on_failure: always 16 | template: 17 | - "%{repository}/%{branch}/%{commit} - %{author} %{message} %{build_url}" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dnsphpadmin 2 | DNS admin panel, designed to operate via nsupdate, for all kinds of RFC compliant DNS servers 3 | 4 | # Features 5 | * Database-less simple stupid setup 6 | * Communicates directly with DNS servers, no external DB, can be used in combination with other interfaces or tools 7 | * Different servers for querying zone info (transfer) and for update, useful for load balancing 8 | * Audit logs 9 | * Support LDAP / Active Directory authentication 10 | * Web API 11 | * Individual user and LDAP group permissions to edit zones via roles (read/write) 12 | 13 | # How does it work 14 | DNS PHP admin is a very simple GUI utility that helps sysadmins manage their DNS records and also provides easy to use interface for end users, which is more user friendly than low level command line tools that are typically used to manage BIND9 servers. 15 | 16 | It also makes it possible to centralize management of multiple separate DNS servers, so that you can edit multiple zones on multiple different DNS servers. 17 | 18 | This tool is only a wrapper for Linux commands `dig` and `nsupdate`, it will download all records in a zone via AXFR (zone transfer) and it will change the records via nsupdate commands. 19 | 20 | # How to install 21 | First of all make sure that dig and nsupdate are available on system. They should be in /usr/bin, if they are somewhere else, change the paths in config.php later 22 | 23 | Then, download release tarball into any folder which is configured a http root of some web server with PHP installed, (for example into /var/www/dns) and unpack it. 24 | 25 | ``` 26 | cd /tmp 27 | wget https://github.com/benapetr/dnsphpadmin/releases/download/1.10.0/dnsphpadmin_1.10.0.tar.gz 28 | cd /var/www/html 29 | tar -xf /tmp/dnsphpadmin_1.10.0.tar.gz 30 | mv dnsphpadmin_1.10.0 dnsphpadmin 31 | cd dnsphpadmin 32 | 33 | # Now copy the default config file 34 | cp config.default.php config.php 35 | # Edit in your favorite editor 36 | vi config.php 37 | ``` 38 | 39 | Now update `$g_domains` so that it contains information about zones you want to manage. Web server must have nsupdate and dig Linux commands installed in paths that are in config.php and it also needs to have firewall access to perform zone transfer and to perform nsupdate updates. 40 | 41 | ## Docker image 42 | There is also a docker image maintained by Eugene Taylashev 43 | 44 | * GitHub: https://github.com/eugene-taylashev/docker-dnsphpadmin 45 | * Docker Hub: https://hub.docker.com/repository/docker/etaylashev/dnsphpadmin 46 | 47 | **IMPORTANT:** DNS tool doesn't use any authentication by default, so everyone with access to web server will have access to DNS tool. If this is just a simple setup for 1 or 2 admins who should have unlimited access to everything, you should setup login via htaccess or similar see https://httpd.apache.org/docs/2.4/howto/auth.html for apache. If you have LDAP (active directory is also LDAP), you can configure this tool to use LDAP authentication as well. 48 | -------------------------------------------------------------------------------- /api.php: -------------------------------------------------------------------------------- 1 | $result ]; 41 | if (!empty($g_api_warnings)) 42 | $json['warnings'] = $g_api_warnings; 43 | if (!empty($g_api_errors)) 44 | $json['errors'] = $g_api_errors; 45 | $api->PrintObj($json); 46 | } 47 | 48 | function api_warning($text) 49 | { 50 | global $g_api_warnings; 51 | $g_api_warnings[] = $text; 52 | } 53 | 54 | function print_success() 55 | { 56 | print_result('success'); 57 | } 58 | 59 | function print_login_error($reason) 60 | { 61 | global $api; 62 | http_response_code(400); 63 | $api->PrintObj([ 64 | 'result' => 'failure', 65 | 'error' => 'Login failed', 66 | 'message' => $reason, 67 | 'code' => G_API_ELOGIN 68 | ]); 69 | die(G_API_ELOGIN); 70 | } 71 | 72 | function api_call_login($source) 73 | { 74 | global $api, $g_login_failed, $g_login_failure_reason; 75 | ProcessLogin(); 76 | if ($g_login_failed) 77 | { 78 | print_login_error($g_login_failure_reason); 79 | return true; 80 | } 81 | print_success(); 82 | return true; 83 | } 84 | 85 | function api_call_logout($source) 86 | { 87 | session_unset(); 88 | print_success(); 89 | return true; 90 | } 91 | 92 | function api_call_login_token($source) 93 | { 94 | global $api, $g_login_failed, $g_login_failure_reason; 95 | if (!isset($_POST['token'])) 96 | { 97 | $api->ThrowError('No token', 'You need to provide a token'); 98 | return true; 99 | } 100 | ProcessTokenLogin(); 101 | if ($g_login_failed) 102 | { 103 | print_login_error($g_login_failure_reason); 104 | return true; 105 | } 106 | print_success(); 107 | return true; 108 | } 109 | 110 | function api_call_list($source) 111 | { 112 | global $api; 113 | $api->PrintObj(Zones::GetZoneList()); 114 | return true; 115 | } 116 | 117 | function api_call_list_records($source) 118 | { 119 | global $api, $g_domains; 120 | $zone = NULL; 121 | if (isset($_GET['zone'])) 122 | $zone = $_GET['zone']; 123 | else if (isset($_POST['zone'])) 124 | $zone = $_POST['zone']; 125 | else 126 | $api->ThrowError('No zone', 'You provided no zone name to list records for'); 127 | 128 | if (!array_key_exists($zone, $g_domains)) 129 | $api->ThrowError('No such zone', 'This zone is not in configuration file'); 130 | 131 | $api->PrintObj(GetRecordList($zone)); 132 | return true; 133 | } 134 | 135 | function api_call_is_logged($source) 136 | { 137 | global $api, $g_auth_roles_map; 138 | $logged = is_authenticated($api->AuthenticationBackend); 139 | $result = [ 'is_logged' => $logged ]; 140 | if ($logged && isset($_SESSION['user'])) 141 | { 142 | $result['user'] = $_SESSION['user']; 143 | if ($g_auth_roles_map !== NULL && array_key_exists($_SESSION['user'], $g_auth_roles_map)) 144 | $result['role'] = implode (',', $g_auth_roles_map[$_SESSION['user']]); 145 | } 146 | $api->PrintObj($result); 147 | return true; 148 | } 149 | 150 | function check_zone_access($zone) 151 | { 152 | global $api, $g_domains; 153 | if (!array_key_exists($zone, $g_domains)) 154 | { 155 | $api->ThrowError('No such zone', "No such zone: $zone"); 156 | return false; 157 | } 158 | 159 | if (!Zones::IsEditable($zone)) 160 | { 161 | $api->ThrowError('Unable to write: Read-only zone', "Domain $zone is not writeable"); 162 | return false; 163 | } 164 | 165 | if (!IsAuthorizedToWrite($zone)) 166 | { 167 | $api->ThrowError('Permission denied', "You are not authorized to edit $zone"); 168 | return false; 169 | } 170 | 171 | return true; 172 | } 173 | 174 | function get_required_post_get_parameter($name) 175 | { 176 | global $api; 177 | $result = NULL; 178 | if (isset($_GET[$name])) 179 | $result = $_GET[$name]; 180 | else if (isset($_POST[$name])) 181 | $result = $_POST[$name]; 182 | else 183 | $api->ThrowError('Missing parameter: ' . $name, 'This parameter is required' ); 184 | 185 | if ($result === NULL || strlen($result) == 0) 186 | $api->ThrowError('Missing parameter: ' . $name, 'This parameter is required' ); 187 | 188 | if (psf_string_contains($result, "\n")) 189 | $api->ThrowError('Newline not allowed', 'Parameter values must not contain newlines for safety reasons'); 190 | 191 | return $result; 192 | } 193 | 194 | function get_optional_post_get_parameter($name) 195 | { 196 | global $api; 197 | $result = NULL; 198 | if (isset($_GET[$name])) 199 | $result = $_GET[$name]; 200 | else if (isset($_POST[$name])) 201 | $result = $_POST[$name]; 202 | 203 | if ($result !== NULL && psf_string_contains($result, "\n")) 204 | $api->ThrowError('Newline not allowed', 'Parameter values must not contain newlines for safety reasons'); 205 | 206 | return $result; 207 | } 208 | 209 | function get_zone_for_fqdn_or_throw($fqdn) 210 | { 211 | global $api; 212 | $zone = Zones::GetZoneForFQDN($fqdn); 213 | 214 | if ($zone === NULL) 215 | $api->ThrowError('No such zone', 'Zone for given fqdn was not found'); 216 | 217 | return $zone; 218 | } 219 | 220 | function validate_type_or_throw($type) 221 | { 222 | global $api; 223 | 224 | if (!IsValidRecordType($type)) 225 | { 226 | $api->ThrowError('Invalid type', "Type $type is not a valid DNS record type"); 227 | return false; 228 | } 229 | 230 | return true; 231 | } 232 | 233 | function api_call_create_record($source) 234 | { 235 | global $api, $g_domains; 236 | $zone = get_optional_post_get_parameter('zone'); 237 | $record = get_required_post_get_parameter('record'); 238 | $ttl = get_required_post_get_parameter('ttl'); 239 | $type = get_required_post_get_parameter('type'); 240 | $value = get_required_post_get_parameter('value'); 241 | $comment = get_optional_post_get_parameter('comment'); 242 | $ptr = IsTrue(get_optional_post_get_parameter('ptr')); 243 | $merge_record = true; 244 | 245 | if ($zone === NULL) 246 | { 247 | $merge_record = false; 248 | $zone = get_zone_for_fqdn_or_throw($record); 249 | } 250 | 251 | if (!check_zone_access($zone)) 252 | return false; 253 | 254 | if (!validate_type_or_throw($type)) 255 | return false; 256 | 257 | if (!is_numeric($ttl)) 258 | { 259 | $api->ThrowError('Invalid ttl', "TTL must be a number"); 260 | return false; 261 | } 262 | 263 | $record = SanitizeHostname($record); 264 | if (!IsValidHostName($record)) 265 | { 266 | $api->ThrowError('Invalid hostname', "Hostname is containing invalid characters"); 267 | return false; 268 | } 269 | 270 | $n = "server " . $g_domains[$zone]['update_server'] . "\n"; 271 | $merged_record = NULL; 272 | if ($merge_record) 273 | { 274 | $n .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl); 275 | $merged_record = $record . "." . $zone; 276 | } else 277 | { 278 | $n .= ProcessInsertFromPOST("" , $record, $value, $type, $ttl); 279 | $merged_record = $record; 280 | } 281 | $n .= "send\nquit\n"; 282 | 283 | ProcessNSUpdateForDomain($n, $zone); 284 | WriteToAuditFile("create", $merged_record . " " . $ttl . " " . $type . " " . $value, $comment); 285 | 286 | if ($ptr == true) 287 | { 288 | Debug('PTR record was requested for ' . $merged_record . ' creating one'); 289 | if ($type != 'A') 290 | { 291 | api_warning('Requested PTR record was not created: PTR record can be only created when you are inserting A record, you created ' . $type . ' record instead'); 292 | } else 293 | { 294 | DNS_InsertPTRForARecord($value, $merged_record, $ttl, $comment); 295 | } 296 | } 297 | 298 | print_success(); 299 | return true; 300 | } 301 | 302 | function api_call_replace_record($source) 303 | { 304 | global $api, $g_domains; 305 | $zone = get_optional_post_get_parameter('zone'); 306 | $record = get_required_post_get_parameter('record'); 307 | $ttl = get_required_post_get_parameter('ttl'); 308 | $type = get_required_post_get_parameter('type'); 309 | $new_value = get_required_post_get_parameter('new_value'); 310 | $value = get_optional_post_get_parameter('value'); 311 | $comment = get_optional_post_get_parameter('comment'); 312 | $new_record = get_optional_post_get_parameter('new_record'); 313 | $new_type = get_optional_post_get_parameter('new_type'); 314 | $ptr = IsTrue(get_optional_post_get_parameter('ptr')); 315 | $merge_record = true; 316 | 317 | // Auto-fill optional 318 | if ($new_type === NULL) 319 | $new_type = $type; 320 | 321 | if ($new_record === NULL) 322 | $new_record = $record; 323 | 324 | if ($zone === NULL) 325 | { 326 | $merge_record = false; 327 | $zone = get_zone_for_fqdn_or_throw($record); 328 | } 329 | 330 | if (!check_zone_access($zone)) 331 | return false; 332 | 333 | if (!validate_type_or_throw($type)) 334 | return false; 335 | 336 | if (!validate_type_or_throw($new_type)) 337 | return false; 338 | 339 | if (!is_numeric($ttl)) 340 | { 341 | $api->ThrowError('Invalid ttl', "TTL must be a number"); 342 | return false; 343 | } 344 | 345 | $old = NULL; 346 | $old_record = NULL; 347 | $merged_record = NULL; 348 | if (!$merge_record) 349 | { 350 | $old = $record . ' 0 ' . $type; 351 | $old_record = $record; 352 | $merged_record = $new_record; 353 | } else 354 | { 355 | $old = $record . '.' . $zone . ' 0 ' . $type; 356 | $old_record = $record . '.' . $zone; 357 | $merged_record = $new_record . '.' . $zone; 358 | } 359 | 360 | if ($value !== NULL) 361 | $old .= ' ' . $value; 362 | 363 | DNS_ModifyRecord($zone, $new_record, $new_value, $new_type, $ttl, $comment, $old, !$merge_record); 364 | 365 | if ($ptr) 366 | { 367 | if ($type != 'A' && $new_type != 'A') 368 | { 369 | api_warning("You requested to modify underlying PTR record, but neither new or old record type is A record, ignoring PTR update request"); 370 | } else 371 | { 372 | // PTR update was requested, if old type was A, delete it. If new type is A, create it 373 | if ($type == 'A') 374 | { 375 | if ($value === NULL) 376 | api_warning("Old PTR record was not deleted, because parameter value was not provided - so we don't know what to delete"); 377 | else 378 | DNS_DeletePTRForARecord($value, $old_record, $comment); 379 | } 380 | if (($new_type === NULL && $type == 'A') || $new_type == 'A') 381 | { 382 | DNS_InsertPTRForARecord($new_value, $merged_record, $ttl, $comment); 383 | } 384 | } 385 | } 386 | 387 | print_success(); 388 | return true; 389 | } 390 | 391 | function api_call_delete_record($source) 392 | { 393 | global $api, $g_domains; 394 | $zone = get_optional_post_get_parameter('zone'); 395 | $record = get_required_post_get_parameter('record'); 396 | $ttl = 0; 397 | $type = get_required_post_get_parameter('type'); 398 | $value = get_optional_post_get_parameter('value'); 399 | $comment = get_optional_post_get_parameter('comment'); 400 | $ptr = get_optional_post_get_parameter('ptr'); 401 | $merge_record = true; 402 | 403 | if ($zone === NULL) 404 | { 405 | $merge_record = false; 406 | $zone = get_zone_for_fqdn_or_throw($record); 407 | } 408 | 409 | if (!check_zone_access($zone)) 410 | return false; 411 | 412 | if (!validate_type_or_throw($type)) 413 | return false; 414 | 415 | $record = SanitizeHostname($record); 416 | if (!IsValidHostName($record)) 417 | { 418 | $api->ThrowError('Invalid hostname', "Hostname is containing invalid characters"); 419 | return false; 420 | } 421 | 422 | // Value is optional, so in order to make nsupdate call more simple, we prefix it with space 423 | $original_value = $value; 424 | if (!psf_string_is_null_or_empty($value)) 425 | $value = " " . $value; 426 | else 427 | $value = ""; 428 | 429 | $n = "server " . $g_domains[$zone]['update_server'] . "\n"; 430 | 431 | $merged_record = ""; 432 | if ($merge_record) 433 | { 434 | $n .= "update delete " . $record . "." . $zone . " 0 " . $type . $value . "\n"; 435 | $merged_record = $record . "." . $zone; 436 | } else 437 | { 438 | $n .= "update delete " . $record . " 0 " . $type . $value . "\n"; 439 | $merged_record = $record; 440 | } 441 | $n .= "send\nquit\n"; 442 | 443 | ProcessNSUpdateForDomain($n, $zone); 444 | WriteToAuditFile("delete", $merged_record . " 0 " . $type . $value, $comment); 445 | 446 | if ($ptr == true) 447 | { 448 | Debug('PTR record deletion was requested for ' . $merged_record); 449 | if ($type != 'A') 450 | { 451 | api_warning('Requested PTR record was not deleted: PTR record can be only deleted when you are changing A record, you deleted ' . $type . ' record instead'); 452 | } else 453 | { 454 | DNS_DeletePTRForARecord($original_value, $merged_record, $comment); 455 | } 456 | } 457 | 458 | print_success(); 459 | return true; 460 | } 461 | 462 | function api_call_get_zone_for_fqdn($source) 463 | { 464 | global $api; 465 | $fqdn = get_required_post_get_parameter('fqdn'); 466 | $zone = Zones::GetZoneForFQDN($fqdn); 467 | if ($zone === NULL) 468 | $api->ThrowError('No such zone', 'Zone for given fqdn was not found'); 469 | $api->PrintObj(['zone' => $zone]); 470 | return true; 471 | } 472 | 473 | function api_call_get_record($source) 474 | { 475 | global $api; 476 | $record = get_required_post_get_parameter('record'); 477 | $record = SanitizeHostname($record); 478 | if (!IsValidHostName($record)) 479 | { 480 | $api->ThrowError('Invalid hostname', "Hostname $record is not a valid hostname"); 481 | return false; 482 | } 483 | $type = get_optional_post_get_parameter('type'); 484 | if ($type === NULL) 485 | $type = 'A'; 486 | 487 | if (!IsValidRecordType($type)) 488 | { 489 | $api->ThrowError('Invalid type', "Type $type is not a valid DNS record type"); 490 | return false; 491 | } 492 | 493 | $zone = get_optional_post_get_parameter('zone'); 494 | if ($zone === NULL) 495 | { 496 | $zone = get_zone_for_fqdn_or_throw($record); 497 | } else 498 | { 499 | $record .= '.' . $zone; 500 | } 501 | if (!IsAuthorizedToRead($zone)) 502 | $api->ThrowError('Permission denied', "You don't have access to read data from this zone"); 503 | 504 | WriteToAuditFile("get_record", $record . ' ('. $zone .')'); 505 | $api->PrintObj(get_records_from_zone($record, $type, $zone)); 506 | return true; 507 | } 508 | 509 | function api_call_get_version($source) 510 | { 511 | global $api; 512 | $api->PrintObj([ 'version' => G_DNSTOOL_VERSION ]); 513 | return true; 514 | } 515 | 516 | function register_api($name, $short_desc, $long_desc, $callback, $auth = true, $required_params = [], $optional_params = [], $example = NULL, $post_only = false) 517 | { 518 | global $api; 519 | $call = new PsfApi($name, $callback, $short_desc, $long_desc, $required_params, $optional_params); 520 | $call->Example = $example; 521 | $call->RequiresAuthentication = $auth; 522 | $call->POSTOnly = $post_only; 523 | $api->RegisterAPI_Action($call); 524 | return $call; 525 | } 526 | 527 | function is_authenticated($backend) 528 | { 529 | global $api, $g_login_failed, $g_login_failure_reason; 530 | $require_login = RequireLogin(); 531 | 532 | if (!$require_login) 533 | return true; 534 | if ($require_login && !isset($_POST['token'])) 535 | return false; 536 | 537 | // User is not logged in, but provided a token, let's validate it 538 | ProcessTokenLogin(); 539 | if ($g_login_failed) 540 | { 541 | $api->ThrowError('Login failed', $g_login_failure_reason); 542 | return false; 543 | } 544 | return true; 545 | } 546 | 547 | function is_privileged($backend, $privilege) 548 | { 549 | return true; 550 | } 551 | 552 | // Start up the program, initialize all sorts of resources, syslog, session data etc. 553 | Initialize(); 554 | 555 | $api = new PsfApiBase_JSON(); 556 | $api->ShowHelpOnNoAction = false; 557 | $api->ExamplePrefix = "/api.php"; 558 | $api->AuthenticationBackend = new PsfCallbackAuth($api); 559 | $api->AuthenticationBackend->callback_IsAuthenticated = "is_authenticated"; 560 | $api->AuthenticationBackend->callback_IsPrivileged = "is_privileged"; 561 | 562 | register_api("is_logged", "Returns information whether you are currently logged in, or not", "Returns information whether you are currently logged in or not.", 563 | "api_call_is_logged", false, [], [], '?action=is_logged'); 564 | register_api("login", "Logins via username and password", "Login into API via username and password using exactly same login method as index.php. This API can be only accessed via POST method", 565 | "api_call_login", false, 566 | [ new PsfApiParameter("loginUsername", PsfApiParameterType::String, "Username to login"), new PsfApiParameter("loginPassword", PsfApiParameterType::String, "Password") ], 567 | [], '?action=login', true); 568 | register_api("logout", "Logs you out", "Logs you out and clear your session data", "api_call_logout", true, [], [], '?action=logout'); 569 | register_api("login_token", "Logins via token", "Login into API via application token", "api_call_login_token", false, 570 | [ new PsfApiParameter("token", PsfApiParameterType::String, "Token that is used to login with") ], 571 | [], '?action=login_token&token=123ngfshegkernker5', true); 572 | register_api("list_zones", "List all existing zones that you have access to", "List all existing zones that you have access to.", 573 | "api_call_list", true, [], [], '?action=list_zones'); 574 | register_api('list_records', "List all existing records for a specified zone", "List all existing records for a specified zone", "api_call_list_records", true, 575 | [ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to list records for") ], 576 | [], '?action=list_records&zone=domain.org'); 577 | register_api('create_record', 'Creates a new DNS record in specified zone', 'Creates a new DNS record in specific zone. Please mind that domain name / zone is appended to record name automatically, ' . 578 | 'so if you want to add test.domain.org, name of key is only test.', 'api_call_create_record', true, 579 | // Required parameters 580 | [ new PsfApiParameter("record", PsfApiParameterType::String, "Record name, if you don't provide zone name explicitly, this should be FQDN"), 581 | new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds)"), new PsfApiParameter("type", PsfApiParameterType::String, "Record type"), 582 | new PsfApiParameter("value", PsfApiParameterType::String, "Value of record") ], 583 | // Optional parameters 584 | [ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"), 585 | new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally create PTR record, works only when you are adding A records"), 586 | new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ], 587 | // Example call 588 | '?action=create_record&zone=domain.org&record=test&ttl=3600&type=A&value=0.0.0.0'); 589 | register_api('delete_record', 'Deletes DNS record(s) in specified zone', 'Deletes DNS record(s) in specific zone. If you don\'t provide value, all records of given type will be deleted.', 'api_call_delete_record', true, 590 | // Required parameters 591 | [ new PsfApiParameter("record", PsfApiParameterType::String, "Record name, if you don't provide zone name explicitly, this should be FQDN"), 592 | new PsfApiParameter("type", PsfApiParameterType::String, "Record type") ], 593 | // Optional parameters 594 | [ new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds). Please note that nsupdate ignores TTL in delete requests. This parameter exists only for compatiblity reasons and is silently ignored."), 595 | new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"), 596 | new PsfApiParameter("value", PsfApiParameterType::String, "Value of record. If not provided, all records with given type will be removed."), 597 | new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally delete PTR record, works only when you are deleting A records"), 598 | new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ], 599 | // Example call 600 | '?action=delete_record&zone=domain.org&record=test&ttl=3600&type=A&value=0.0.0.0'); 601 | register_api('replace_record', 'Removes old and create a new DNS record in single nsupdate transaction', 'Replaces specific record. Both records must be within same zone, but may be of different type. Note that due to nature of nsupdate, if record you want to replace ' . 602 | 'doesn\'t exist, it will not fail. So replace_record on non-existent record will still create a new record.', 'api_call_replace_record', true, 603 | // Required parameters 604 | [ new PsfApiParameter("record", PsfApiParameterType::String, "Name of existing record you want to replace, if you don't provide zone name explicitly, this should be FQDN"), 605 | new PsfApiParameter("type", PsfApiParameterType::String, "Type of current record that you want to replace"), 606 | new PsfApiParameter("ttl", PsfApiParameterType::Number, "Time to live (seconds)"), 607 | new PsfApiParameter("new_value", PsfApiParameterType::String, "Value of new record")], 608 | // Optional parameters 609 | [ new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file"), 610 | new PsfApiParameter("value", PsfApiParameterType::String, "Value of record. If not provided, all records with given type will be removed and replaced with a single new record"), 611 | new PsfApiParameter("new_record", PsfApiParameterType::String, "New record name, if you are not changing name of key, this can be omitted. If you don't provide zone name explicitly, this should be FQDN"), 612 | new PsfApiParameter("new_type", PsfApiParameterType::String, "Type of record, if you are not changing type, this can be omitted."), 613 | new PsfApiParameter("ptr", PsfApiParameterType::Boolean, "Optionally replace associated PTR record, works only when either new, old or both records are A records"), 614 | new PsfApiParameter("comment", PsfApiParameterType::String, "Optional comment for audit logs") ], 615 | // Example call 616 | '?action=replace_record&record=test.zone.org&ttl=3600&type=A&value=0.0.0.0&new_value=2.2.2.2&ptr=true'); 617 | register_api('get_zone_for_fqdn', 'Returns zone name for given FQDN', 'Attempts to look up zone name for given FQDN using configuration file of php dns admin using auto-lookup function', 618 | 'api_call_get_zone_for_fqdn', false, [ new PsfApiParameter("fqdn", PsfApiParameterType::String, "FQDN") ], [], '?action=get_zone_for_fqdn&fqdn=test.example.org'); 619 | register_api('get_record', 'Return single record with specified FQDN', 'Lookup single record from master server responsible for zone that hosts this record', 'api_call_get_record', true, 620 | [ new PsfApiParameter("record", PsfApiParameterType::String, "Record name, if you don't provide zone name explicitly, this should be FQDN") ], 621 | [ new PsfApiParameter("type", PsfApiParameterType::String, "Record type (if not specified, will be A)"), 622 | new PsfApiParameter("zone", PsfApiParameterType::String, "Zone to modify, if not specified and record is fully qualified, it's automatically looked up from config file") ], 623 | '?action=get_record&record=test.example.org'); 624 | register_api('get_version', 'Returns version', 'Returns version of this tool.', 'api_call_get_version', false, [], [], '?action=get_version'); 625 | if (!$api->Process()) 626 | { 627 | if (isset($_GET['action']) || isset($_POST['action'])) 628 | { 629 | $api->ThrowError('Unknown action', 'This action is unknown. Please refer to help. Open api.php with no parameters to see help in HTML form.'); 630 | } else 631 | { 632 | $api->PrintHelpAsHtml(); 633 | } 634 | } else 635 | { 636 | IncrementStat('api'); 637 | } 638 | 639 | ResourceCleanup(); 640 | -------------------------------------------------------------------------------- /config.default.php: -------------------------------------------------------------------------------- 1 | [ 'transfer_server' => 'localhost', 'update_server' => 'localhost' ] ]; 24 | 25 | // You can specify multiple custom options per domain, this example here contains all available options with documentation: 26 | // You can also specify custom TSIG override 27 | // $g_domains = [ 'example.domain' => [ 'transfer_server' => 'localhost', 28 | // 'update_server' => 'localhost', 29 | // 'explicit' => true, // by default true, will explicitly tell nsupdate to perform updates to this zone, if set to false nsupdate will automatically try to figure out the zone name 30 | // 'read_only' => false, // by default false, if true domain will be read only 31 | // 'in_transfer' => false, // if true domain will be marked as "in transfer" which means it's being transfered from one DNS master to another, so the records may not reflect truth 32 | // 'maintenance_note' => 'This domain is being configured now', // maintenance note to display for this domain 33 | // 'note' => 'This zone is very important', // generic note to display for this domain 34 | // 'tsig' => true, 35 | // 'tsig_key' => 'some_key', 36 | // 'ttl' => 3600 ] ]; // Overrides default global TTL for new records 37 | 38 | // List of record types that can be edited 39 | // https://en.wikipedia.org/wiki/List_of_DNS_record_types 40 | $g_editable = [ 'A', 'AAAA', 'CNAME', 'DNAME', 'DS', 'NS', 'PTR', 'SRV', 'SSHFP', 'TXT', 'SPF', 'MX' ]; 41 | 42 | // List of record types that are hidden from UI by default, this is not a security feature, it's for user comfort and can be easily disabled in UI 43 | // API is not affected by this 44 | $g_hidden_record_types = [ 'NSEC', 'RRSIG' ]; 45 | 46 | // Default TTL for new DNS records (can be also specified per zone using ttl key) 47 | $g_default_ttl = 3600; 48 | 49 | // Path to executable of dig, you can also use this to specify some dig options for example: 50 | // $g_dig = '/usr/bin/dig +tcp +time=10'; 51 | $g_dig = '/usr/bin/dig'; 52 | 53 | // Path to executable of nsupdate 54 | $g_nsupdate = '/usr/bin/nsupdate'; 55 | 56 | // If enabled, it will not be possible to work with garbage hostnames not conforming to standards 57 | $g_strict_hostname_checks = true; 58 | 59 | // If set to value higher than 0, dig will be retried for N times, this is useful on broken networks with heavy packet loss 60 | $g_retry_on_error = 2; 61 | 62 | // Error log, keep NULL to disable error logging to external file, or set to absolute path to writeable error log file 63 | $g_error_log = NULL; 64 | 65 | // Whether audit subsystem should be enabled 66 | $g_audit = false; 67 | 68 | // Define which events are logged into audit log 69 | $g_audit_events = [ 70 | 'login_success' => true, 71 | 'login_fail' => true, 72 | 'batch' => true, 73 | 'create' => true, 74 | 'replace_delete' => true, 75 | 'replace_create' => true, 76 | 'delete' => true, 77 | 'display' => false, 78 | 'get_record' => false 79 | ]; 80 | 81 | // Destination file to which the audit events are written to 82 | $g_audit_log = '/var/log/dns_audit.log'; 83 | 84 | // Folder where the batch operations should be logged, each batch operation will be stored in separate file 85 | // Keep this null to log batch operations into single line to $g_audit_log 86 | $g_audit_batch_location = null; 87 | 88 | // TSIG authentication for nsupdate - global config 89 | // you can specify individual TSIG settings per each domain, if you don't this is default value 90 | $g_tsig = false; 91 | $g_tsig_key = ''; 92 | 93 | // Will print debug statements into html output 94 | $g_debug = false; 95 | 96 | // Will print debug messages into specified file (lot of text) 97 | $g_debug_log = NULL; 98 | 99 | // Log to syslog 100 | $g_syslog = false; 101 | 102 | $g_syslog_targets = [ 103 | 'error' => true, 104 | 'audit' => true, 105 | 'debug' => false 106 | ]; 107 | 108 | // Syslog facility 109 | $g_syslog_facility = LOG_LOCAL0; 110 | 111 | // Syslog ident (program name) 112 | $g_syslog_ident = 'dnsphpadmin'; 113 | 114 | // Optional execution ID used to identify separate executions in logs (debug / audit / error) 115 | $g_eid = bin2hex(openssl_random_pseudo_bytes(8)); 116 | 117 | // How long do sessions last in seconds 118 | $g_session_timeout = 3600; 119 | 120 | // Authentication setup - by default, don't provide any authentication mechanism, leave it up to sysadmin 121 | // Only supported authentication backend right now is LDAP ($g_auth = "ldap";) 122 | $g_auth = NULL; 123 | 124 | // Application ID for sessions, if you have multiple separate installations of dns php admin, you should create unique strings for each of them 125 | // to prevent sharing session information between them, this string is also used as prefix for caching keys and cookies 126 | $g_auth_session_name = 'dnsphpadmin'; 127 | 128 | // Few words about LDAP integration within dns php admin: 129 | // This tool was written in a very large corporation world with extreme edge use-cases in mind. Therefore it's very flexible and it has 130 | // large amount of options that may look quite hard to understand on first sight. While it supports generic LDAP protocol it was written 131 | // with Active Directory in mind. This tool supports multiple authentication schemes such as: 132 | // * anyone who has access to LDAP / AD can use it without limits (keep g_auth_roles and g_auth_allowed_users NULL) 133 | // * selected users can login only (g_auth_allowed_users) 134 | // * RBAC access - there are roles defined with fine-grained permissions where each user is bound to one or more of these roles (groups) 135 | // Many of the options present in this config may be left as default value unless you are aiming for one of these edge cases that I unfortunatelly 136 | // had to prepare this tool for. 137 | 138 | // Example auth 139 | // $g_auth = "ldap"; 140 | // URL of LDAP server, prefix with ldaps:// to get SSL, if you need to ignore invalid certificate, follow this: 141 | // https://stackoverflow.com/questions/3866406/need-help-ignoring-server-certificate-while-binding-to-ldap-server-using-php 142 | // $g_auth_ldap_url = "ldap.example.com"; 143 | $g_auth_ldap_url = NULL; 144 | 145 | // Custom login information 146 | // Example: 147 | // $g_auth_login_banner = "You can login with your domain name"; 148 | $g_auth_login_banner = NULL; 149 | 150 | // Set up optional filter for usernames that are allowed to login 151 | // Example: 152 | // $g_auth_allowed_users = array( "domain\\bob", "joe" ); 153 | $g_auth_allowed_users = NULL; 154 | 155 | // Optional prefix for users - this prefix is automatically appended in front of every username unless it's already present 156 | // this is useful for AD domain logins where domain has to be specified in front of username 157 | // Example: 158 | // $g_auth_domain_prefix = "CORP\\"; 159 | // will result in joe being changed to CORP\joe while authenticating to LDAP, but when retrieving a list of groups, only joe will be used 160 | $g_auth_domain_prefix = NULL; 161 | 162 | // If true, following string will be used to fetch group membership for each user. These groups will be added to list of roles that user is member of. 163 | // If you want to grant some privileges to an LDAP group, you should create a special role with exactly same name as LDAP group, that way each member 164 | // of this group will have these privileges 165 | $g_auth_fetch_domain_groups = false; 166 | 167 | // This is only used if g_auth_fetch_domain_groups option is set to true to fetch list of groups user is in 168 | $g_auth_ldap_dn = "CN=Users,DC=ad,DC=domain"; 169 | 170 | // You can also setup authentication roles and their privileges here, there is special built-in role "root" which has unlimited privileges 171 | // Privileges are one of 'rw', 'r' or '' for nothing 172 | // Examples: 173 | // $g_auth_roles = [ 'users' => [ 'example.domain' => 'rw' ] ]; 174 | // $g_auth_roles = [ 'DOMAIN.GROUP.WITH.FANCY.NAME' => [ 'example.domain' => 'rw' ] ]; // in combination with g_auth_fetch_domain_groups 175 | // $g_auth_roles = [ 'admins' => [ 'example.domain' => 'rw' ], 'users' => [ 'example.domain' => 'r' ] ]; 176 | // IMPORTANT: if you are using LDAP groups, you will still need to define some authentication roles here and later you can bind these roles to 177 | // individual groups 178 | $g_auth_roles = NULL; 179 | 180 | // Each user can be member of multiple roles, in case no role is specified for user, this is default role 181 | $g_auth_default_role = NULL; 182 | 183 | // Don't allow users who don't belong to any role to login to this tool - this is only enforced in case that g_auth_roles is not nullptr 184 | $g_auth_disallow_users_with_no_roles = true; 185 | 186 | // You can assign roles to users or LDAP groups here 187 | // Example: 188 | // $g_auth_roles_map = [ 'joe' => [ 'admins', 'users' ] ]; 189 | $g_auth_roles_map = []; 190 | 191 | // Use local bootstrap instead of CDN (useful for clients behind firewall) 192 | // In order for this to work, you need to download bootstrap 3.3.7 so that it's in root folder of htdocs (same level as index.php) example: 193 | // /bootstrap-3.3.7 194 | // /index.php 195 | $g_use_local_bootstrap = false; 196 | 197 | // Use local jquery instead of CDN (useful for clients behind firewall) 198 | // For this to work download compressed jquery 3.3.1 to root folder for example: 199 | // /jquery-3.3.1.min.js 200 | $g_use_local_jquery = false; 201 | 202 | // Whether API interface is available or not 203 | $g_api_enabled = false; 204 | 205 | // List of access tokens that can be used with API calls (together with classic login) 206 | // This is a simple list of secrets. Each secret is a string that is used to authenticate for API subsystem. 207 | // It's recommended to optionally prefix each secret with a memorable string (user name) and underscore, for example: 208 | // my_favorite_tool_secretstring123345 209 | // In this case if masking is enabled, audit logs will not contain the last part after last underscore to prevent secret from leaking 210 | // into the audit logs 211 | $g_api_tokens = [ ]; 212 | 213 | // If enabled text after last underscore of each api token will be removed from audit logs 214 | $g_api_token_mask = true; 215 | 216 | // Transfer cache is optional and used to cache the results of zone transfer in order to prevent unnecessary transfers, that might put heavy load 217 | // on both DNS server as well as network. Caching will store a whole zone and instead of performing full zone transfer, DNS tool will just query SOA record and it will 218 | // check if record serial is matching serial in our cache. If it doesn't, full zone transfer will be executed. 219 | // You can check whether caching is functioning in debug logs - see $g_debug. Following caching engines are provided: 220 | // NULL - no caching 221 | // 'memcache' - Memcache daemon (using memcache class, not memcached class - PHP has two classes for same purpose) https://www.php.net/manual/en/book.memcache.php 222 | // 'memcached' - Memcache daemon (using memcached class) 223 | $g_caching_engine = NULL; 224 | 225 | // In case you decide to use memcached as caching engine, you can adjust some parameters with these variables 226 | // NOTE: memcached engine uses $g_auth_session_name as key prefixes 227 | $g_caching_memcached_host = 'localhost'; 228 | $g_caching_memcached_port = 11211; 229 | $g_caching_memcached_expiry = 0; 230 | 231 | // You can optionally enable in-cache statistics that can be exported for use with monitoring, such as prometheus, to obtain usage metrics 232 | $g_caching_stats_enabled = false; 233 | 234 | // Per-user configuration location 235 | $g_user_config_prefix = null; 236 | -------------------------------------------------------------------------------- /definitions.php: -------------------------------------------------------------------------------- 1 | [ 'transfer_server' => 'localhost', 'update_server' => 'localhost' ], 5 | 'subzone.example.domain' => [ 'transfer_server' => 'localhost', 'update_server' => 'localhost' ], 6 | 'example.org' => [ 'transfer_server' => 'ns-prod1.lan.example.org', 'update_server' => 'ns-prod1.lan.example.org' ], 7 | 'prod.example.org' => [ 'transfer_server' => 'ns-prod1.lan.example.org', 'update_server' => 'ns-prod1.lan.example.org' ], 8 | 'nonprod.example.org' => [ 'transfer_server' => 'ns-prod1.lan.example.org', 'update_server' => 'ns-prod1.lan.example.org' ], 9 | 'ad.example.org' => [ 'transfer_server' => 'windows.example.org', 'update_server' => '', read_only => true ] 10 | ]; 11 | 12 | // Audit 13 | $g_audit = true; 14 | $g_audit_log = '/var/log/dns/int_audit.log'; 15 | 16 | $g_audit_events['display'] = true; 17 | $g_audit_events['get_record'] = true; 18 | 19 | $g_auth = "ldap"; 20 | $g_auth_domain_prefix = "CORP\\"; 21 | $g_auth_fetch_domain_groups = true; 22 | $g_auth_ldap_dn = "OU=CORP,DC=evilcorp,DC=net"; 23 | $g_auth_login_banner = "You can login using your CORP account, for example: michael.smith"; 24 | $g_auth_ldap_url = "ldaps://ldap.evilcorp.net"; 25 | $g_session_timeout = 3600; 26 | 27 | // API enable 28 | $g_api_enabled = true; 29 | 30 | // Role based permissions matrix 31 | 32 | $g_auth_roles = [ 'devops' => [ 33 | 'nonprod.example.org' => 'rw', 34 | 'prod.example.org' => 'r', 35 | 'example.domain' => 'r' 36 | ], 37 | // these are just a placeholder, this role is filled up using code later 38 | 'readonly' => [ ], 39 | 'reverse_rw_all' => [], 40 | 'reverse_ro_all' => [] 41 | ]; 42 | 43 | 44 | /////////////////////////////////////////////// 45 | // hacks 46 | /////////////////////////////////////////////// 47 | foreach ($g_domains as $key => $value) 48 | { 49 | // hack to load every single zone into 'readonly' and pseudo-root roles 50 | $g_auth_roles['readonly'][$key] = 'r'; 51 | $g_auth_roles['admin'][$key] = 'rw'; 52 | 53 | // reverse 54 | if (psf_string_endsWith($key, 'in-addr.arpa')) 55 | { 56 | $g_auth_roles['reverse_rw_all'][$key] = 'rw'; 57 | $g_auth_roles['reverse_ro_all'][$key] = 'r'; 58 | } 59 | } 60 | 61 | // Grant access to AD groups 62 | $g_auth_roles['Security'] = array_merge($g_auth_roles['reverse_ro_in'], $g_auth_roles['readonly']); 63 | $g_auth_roles['Developers'] = array_merge($g_auth_roles['reverse_rw_in'], $g_auth_roles['readonly'], $g_auth_roles['devops']); 64 | $g_auth_roles['Operations'] = $g_auth_roles['admin']; 65 | -------------------------------------------------------------------------------- /examples/ansible/README: -------------------------------------------------------------------------------- 1 | This folder contains some example ansible playbooks that modify DNS via API of dns tool using URI module 2 | 3 | create_record.yml - very simple implementation of login, record creation and logout 4 | delete_record.yml - removes the same record, contains extra checks that result is success 5 | -------------------------------------------------------------------------------- /examples/ansible/create_record.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - include_vars: vars.yml 5 | - uri: 6 | url: "{{ dns_tool }}/api.php" 7 | validate_certs: no 8 | method: POST 9 | body_format: form-urlencoded 10 | body: 11 | action: login 12 | loginUsername: "{{ dns_tool_username }}" 13 | loginPassword: "{{ dns_tool_password }}" 14 | name: 'Login to DNS tool' 15 | no_log: True 16 | register: login 17 | delegate_to: localhost 18 | failed_when: "login.json.result != 'success'" 19 | 20 | - uri: 21 | url: "{{ dns_tool }}/api.php" 22 | validate_certs: no 23 | method: POST 24 | body_format: form-urlencoded 25 | body: 26 | action: create_record 27 | record: "petr.bena.cz.preprod" 28 | ttl: 3600 29 | type: A 30 | value: 1.1.2.2 31 | headers: 32 | Cookie: "{{ login.set_cookie }}" 33 | delegate_to: localhost 34 | name: 'Create a new record' 35 | 36 | - uri: 37 | url: "{{ dns_tool }}/api.php" 38 | validate_certs: no 39 | method: POST 40 | body_format: form-urlencoded 41 | body: 42 | action: logout 43 | headers: 44 | Cookie: "{{ login.set_cookie }}" 45 | delegate_to: localhost 46 | name: 'Logout from DNS tool' 47 | -------------------------------------------------------------------------------- /examples/ansible/delete_record.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | tasks: 4 | - include_vars: vars.yml 5 | - uri: 6 | url: "{{ dns_tool }}/api.php" 7 | validate_certs: no 8 | method: POST 9 | body_format: form-urlencoded 10 | body: 11 | action: login 12 | loginUsername: "{{ dns_tool_username }}" 13 | loginPassword: "{{ dns_tool_password }}" 14 | name: 'Login to DNS tool' 15 | register: login 16 | nolog: True 17 | delegate_to: localhost 18 | failed_when: "login.json.result != 'success'" 19 | 20 | - uri: 21 | url: "{{ dns_tool }}/api.php" 22 | validate_certs: no 23 | method: POST 24 | body_format: form-urlencoded 25 | body: 26 | action: delete_record 27 | record: "petr.bena.cz.preprod" 28 | type: A 29 | value: 1.1.2.2 30 | headers: 31 | Cookie: "{{ login.set_cookie }}" 32 | name: 'Delete record' 33 | delegate_to: localhost 34 | register: this 35 | failed_when: "this.json.result != 'success'" 36 | 37 | - uri: 38 | url: "{{ dns_tool }}/api.php" 39 | validate_certs: no 40 | method: POST 41 | body_format: form-urlencoded 42 | body: 43 | action: logout 44 | headers: 45 | Cookie: "{{ login.set_cookie }}" 46 | name: 'Logout from DNS tool' 47 | delegate_to: localhost 48 | register: this 49 | failed_when: "this.json.result != 'success'" 50 | -------------------------------------------------------------------------------- /examples/ansible/vars.yml: -------------------------------------------------------------------------------- 1 | dns_tool: "https://dnstool.org/dns" 2 | dns_tool_username: "" 3 | dns_tool_password: "" 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benapetr/dnsphpadmin/79f283bdcbdf1ceb00021a544b74a24c5b3444e8/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benapetr/dnsphpadmin/79f283bdcbdf1ceb00021a544b74a24c5b3444e8/favicon.png -------------------------------------------------------------------------------- /includes/audit.php: -------------------------------------------------------------------------------- 1 | memcache = new Memcache(); 27 | $this->memcache->connect($g_caching_memcached_host, $g_caching_memcached_port) or die ('Unable to connect to memcached server at ' . $g_caching_memcached_host . ':' . $g_caching_memcached_port); 28 | if ($g_debug) 29 | { 30 | Debug('memcache version: ' . $this->memcache->getVersion()); 31 | } 32 | } 33 | 34 | function GetEngineName() 35 | { 36 | return 'memcache'; 37 | } 38 | 39 | function IsCached($zone) 40 | { 41 | return $this->memcache->get($this->getPrefix() . 'soa_' . $zone) !== false; 42 | } 43 | 44 | function GetSOA($zone) 45 | { 46 | return $this->memcache->get($this->getPrefix() . 'soa_' . $zone); 47 | } 48 | 49 | function CacheZone($zone, $soa, $data) 50 | { 51 | global $g_caching_memcached_expiry; 52 | Debug('Storing zone ' . $zone . " (SOA $soa) to memcache"); 53 | if (!$this->memcache->set($this->getPrefix() . 'soa_' . $zone, $soa, $g_caching_memcached_expiry) || 54 | !$this->memcache->set($this->getPrefix() . 'data_' . $zone, $data, $g_caching_memcached_expiry)) 55 | { 56 | die('Unable to store data in memcache'); 57 | } 58 | } 59 | 60 | function GetData($zone) 61 | { 62 | return $this->memcache->get($this->getPrefix() . 'data_' . $zone); 63 | } 64 | 65 | function Drop($zone) 66 | { 67 | $this->memcache->delete($this->getPrefix() . 'data_' . $zone); 68 | $this->memcache->delete($this->getPrefix() . 'soa_' . $zone); 69 | } 70 | 71 | function IncrementStat($stat) 72 | { 73 | if ($this->memcache->increment($this->getPrefix() . 'stat_' . $stat) === false) 74 | { 75 | // This statistic is not in memcache yet 76 | $this->memcache->set($this->getPrefix() . 'stat_' . $stat, 1); 77 | } 78 | } 79 | 80 | private function getPrefix() 81 | { 82 | global $g_auth_session_name; 83 | return $g_auth_session_name . "_"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /includes/caching_memcached.php: -------------------------------------------------------------------------------- 1 | memcached = new Memcached(); 27 | $this->memcached->addServer($g_caching_memcached_host, $g_caching_memcached_port); 28 | if ($g_debug) 29 | { 30 | $memcached_version = $this->memcached->getVersion(); 31 | Debug('memcached version: ' . reset($memcached_version)); 32 | } 33 | } 34 | 35 | function GetEngineName() 36 | { 37 | return 'memcached'; 38 | } 39 | 40 | function IsCached($zone) 41 | { 42 | return $this->memcached->get($this->getPrefix() . 'soa_' . $zone) !== false; 43 | } 44 | 45 | function GetSOA($zone) 46 | { 47 | return $this->memcached->get($this->getPrefix() . 'soa_' . $zone); 48 | } 49 | 50 | function CacheZone($zone, $soa, $data) 51 | { 52 | global $g_caching_memcached_expiry; 53 | Debug('Storing zone ' . $zone . " (SOA $soa) to memcache"); 54 | if (!$this->memcached->set($this->getPrefix() . 'soa_' . $zone, $soa, $g_caching_memcached_expiry) || 55 | !$this->memcached->set($this->getPrefix() . 'data_' . $zone, $data, $g_caching_memcached_expiry)) 56 | { 57 | die('Unable to store data in memcached: ' . $this->memcached->getResultMessage()); 58 | } 59 | } 60 | 61 | function GetData($zone) 62 | { 63 | return $this->memcached->get($this->getPrefix() . 'data_' . $zone); 64 | } 65 | 66 | function Drop($zone) 67 | { 68 | $this->memcached->delete($this->getPrefix() . 'data_' . $zone); 69 | $this->memcached->delete($this->getPrefix() . 'soa_' . $zone); 70 | } 71 | 72 | function IncrementStat($stat) 73 | { 74 | // increment doesn't seem to be able to work if key doesn't exist, so let's first check it does 75 | if ($this->memcached->get($this->getPrefix() . 'stat_' . $stat) === false) 76 | $this->memcached->set($this->getPrefix() . 'stat_' . $stat, 1); 77 | else 78 | $this->memcached->increment($this->getPrefix() . 'stat_' . $stat); 79 | } 80 | 81 | private function getPrefix() 82 | { 83 | global $g_auth_session_name; 84 | return $g_auth_session_name . "_"; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /includes/common.php: -------------------------------------------------------------------------------- 1 | GetEngineName()); 53 | $g_caching_engine_instance->Initialize(); 54 | } 55 | 56 | function IncrementStat($stat) 57 | { 58 | global $g_caching_stats_enabled, $g_caching_engine_instance; 59 | if ($g_caching_stats_enabled !== true) 60 | return; 61 | 62 | if ($g_caching_engine_instance === NULL) 63 | return; 64 | 65 | $g_caching_engine_instance->IncrementStat($stat); 66 | } 67 | 68 | //! Display warning message 69 | function Warning($text) 70 | { 71 | if (G_DNSTOOL_ENTRY_POINT === "api.php") 72 | return; 73 | Notifications::DisplayWarning($text); 74 | } 75 | 76 | function IsValidRecordType($type) 77 | { 78 | global $g_editable; 79 | return in_array($type, $g_editable); 80 | } 81 | 82 | //! Return true if application supports and require user to login, no matter if current user 83 | //! is logged in or not. Don't confuse with login.php's RequireLogin() which returns false 84 | //! even when login is enabled in case user is already logged in 85 | function LoginRequired() 86 | { 87 | global $g_auth; 88 | if ($g_auth === NULL || $g_auth !== 'ldap') 89 | return false; 90 | return true; 91 | } 92 | 93 | function IsAuthorized($domain, $privilege) 94 | { 95 | global $g_auth_roles, $g_auth_default_role, $g_auth_roles_map; 96 | 97 | if ($g_auth_roles === NULL || !LoginRequired()) 98 | return true; 99 | 100 | $roles = [ $g_auth_default_role ]; 101 | $user = $_SESSION['user']; 102 | if ($user === NULL || $user === '') 103 | Error('Invalid username in session'); 104 | 105 | if (array_key_exists($user, $g_auth_roles_map)) 106 | $roles = $g_auth_roles_map[$user]; 107 | 108 | if (in_array('root', $roles)) 109 | return true; 110 | 111 | foreach ($roles as $role) 112 | { 113 | if (!array_key_exists($role, $g_auth_roles)) 114 | continue; 115 | $role_info = $g_auth_roles[$role]; 116 | if (!array_key_exists($domain, $role_info)) 117 | continue; 118 | $permissions = $role_info[$domain]; 119 | if ($privilege == 'rw' && $permissions == 'rw') 120 | return true; 121 | if ($privilege == 'r' && ($permissions == 'rw' || $permissions == 'r')) 122 | return true; 123 | } 124 | 125 | return false; 126 | } 127 | 128 | function IsAuthorizedToRead($domain) 129 | { 130 | return IsAuthorized($domain, 'r'); 131 | } 132 | 133 | function IsAuthorizedToWrite($domain) 134 | { 135 | return IsAuthorized($domain, 'rw'); 136 | } 137 | 138 | function GetCurrentUserName() 139 | { 140 | global $g_auth, $g_api_token_mask; 141 | if ($g_api_token_mask && isset($_SESSION["logged_in"]) && $_SESSION["logged_in"] === true && isset($_SESSION["token"]) && $_SESSION["token"] === true) 142 | { 143 | $trimmed_name = $_SESSION["user"]; 144 | if (psf_string_contains($trimmed_name, '_')) 145 | $trimmed_name = substr($trimmed_name, 0, strrpos($trimmed_name, '_')); 146 | return $trimmed_name; 147 | } 148 | if ($g_auth === "ldap" && isset($_SESSION["user"])) 149 | return $_SESSION["user"]; 150 | if (!isset($_SERVER['REMOTE_USER'])) 151 | return "unknown user"; 152 | return $_SERVER['REMOTE_USER']; 153 | } 154 | 155 | //! Required to handle various non-standard boolean interpretations, mostly for strings from API requests 156 | function IsTrue($bool) 157 | { 158 | if ($bool === true) 159 | return true; 160 | 161 | // Check string version 162 | if ($bool == "true" || $bool == "t") 163 | return true; 164 | 165 | // Check int version 166 | if (is_numeric($bool) && $bool != 0) 167 | return true; 168 | 169 | return false; 170 | } 171 | -------------------------------------------------------------------------------- /includes/common_ui.php: -------------------------------------------------------------------------------- 1 | AppendHtmlLine("Zone:"); 44 | $c = new ComboBox("switcher", $switcher); 45 | $c->OnChangeCallback = "reload()"; 46 | foreach ($g_domains as $domain => $properties) 47 | { 48 | if (!IsAuthorizedToRead($domain)) 49 | continue; 50 | if ($g_selected_domain == $domain) 51 | $c->AddDefaultValue($domain); 52 | else 53 | $c->AddValue($domain); 54 | } 55 | $js = new Script("", $parent); 56 | $js->Source = "function reload()\n" . 57 | "{" . 58 | 'var switcher = document.getElementsByName("switcher");' . 59 | 'window.open("index.php?action=manage&domain=" + switcher[0].value, "_self");' . 60 | "}\n"; 61 | return $switcher; 62 | } 63 | -------------------------------------------------------------------------------- /includes/debug.php: -------------------------------------------------------------------------------- 1 | AppendHeader(G_HEADER); 40 | $fc->AppendObject(new BS_Alert("FATAL ERROR: " . $msg, "danger")); 41 | $fc->AppendHtmlLine('Go Back'); 42 | $web->AppendObject($fc); 43 | $web->PrintHtml(); 44 | if ($g_debug) 45 | psf_print_debug_as_html(); 46 | die(1); 47 | } else 48 | { 49 | Notifications::DisplayError($msg); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /includes/fatal_api.php: -------------------------------------------------------------------------------- 1 | ThrowError('ERROR: ' . $msg, $msg); 26 | die(1); 27 | } 28 | -------------------------------------------------------------------------------- /includes/logging.php: -------------------------------------------------------------------------------- 1 | $g_session_timeout) 36 | { 37 | // This session timed out 38 | session_unset(); 39 | } 40 | } 41 | $_SESSION["time"] = time(); 42 | if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] && isset($_SESSION['user']) && isset($_SESSION['groups'])) 43 | { 44 | // This user is logged in - we cached group list within session so that we don't need to query LDAP every single time 45 | $g_auth_roles_map[$_SESSION['user']] = $_SESSION['groups']; 46 | } 47 | } 48 | 49 | function GetLoginInfo() 50 | { 51 | global $g_auth_roles_map; 52 | $role_info = ''; 53 | if ($g_auth_roles_map !== NULL && array_key_exists($_SESSION['user'], $g_auth_roles_map)) 54 | { 55 | $role_info = ' (' . psf_string_auto_trim(implode (', ', $g_auth_roles_map[$_SESSION['user']]), 80, '...') . ')'; 56 | } 57 | return '
' . $_SESSION["user"] . $role_info . '
'; 58 | } 59 | 60 | function ProcessLogin_Error($reason) 61 | { 62 | global $g_login_failure_reason, $g_login_failed; 63 | $extra = ''; 64 | if (isset($_POST["loginUsername"])) 65 | $extra = 'username=' . $_POST["loginUsername"] . ' '; 66 | $g_login_failed = true; 67 | $g_login_failure_reason = $reason; 68 | $_SESSION['logged_in'] = false; 69 | WriteToAuditFile('login_fail', $extra . 'reason=' . $reason); 70 | IncrementStat('login_error'); 71 | } 72 | 73 | function ProcessTokenLogin() 74 | { 75 | global $g_auth, $g_login_failed, $g_api_tokens; 76 | if (!isset($_POST['token'])) 77 | { 78 | ProcessLogin_Error("No token"); 79 | return; 80 | } 81 | $token = $_POST['token']; 82 | if (in_array($token, $g_api_tokens)) 83 | { 84 | $_SESSION["user"] = $token; 85 | $_SESSION["logged_in"] = true; 86 | $_SESSION["token"] = true; 87 | $g_logged_in = true; 88 | WriteToAuditFile('login_success'); 89 | IncrementStat('token_login_success'); 90 | return; 91 | } 92 | // Invalid token 93 | $g_login_failed = true; 94 | $_SESSION["logged_in"] = false; 95 | WriteToAuditFile('login_fail', 'token=' . $token . ' reason=invalid token'); 96 | IncrementStat('token_login_error'); 97 | } 98 | 99 | function LDAP_GroupNameFromCN($name) 100 | { 101 | if (!psf_string_startsWith($name, 'CN=')) 102 | return $name; 103 | 104 | $name = substr($name, 3); 105 | if (!psf_string_contains($name, ',')) 106 | return $name; 107 | 108 | return substr($name, 0, strpos($name, ',')); 109 | } 110 | 111 | function FetchDomainGroups($ldap, $login_name) 112 | { 113 | global $g_auth_domain_prefix, $g_auth_ldap_dn, $g_auth_roles_map, $g_auth_roles; 114 | $ldap_user_search_string = $_POST["loginUsername"]; 115 | // Automatically correct user name 116 | if ($g_auth_domain_prefix !== NULL && psf_string_startsWith($ldap_user_search_string, $g_auth_domain_prefix)) 117 | $ldap_user_search_string = substr($ldap_user_search_string, strlen($g_auth_domain_prefix)); 118 | 119 | // Read groups and insert them to list of roles this user is member of 120 | $ldap_groups = ldap_search($ldap, $g_auth_ldap_dn, "(samaccountname=$ldap_user_search_string)", array("memberof", "primarygroupid")); 121 | if ($ldap_groups === false) 122 | { 123 | DisplayWarning("Unable to retrieve list of groups for this user from LDAP (ldap_search() returned false) - is your ldap_dn correct?"); 124 | return; 125 | } else 126 | { 127 | $entries = ldap_get_entries($ldap, $ldap_groups); 128 | if ($entries === false) 129 | { 130 | DisplayWarning("Unable to retrieve list of groups for this user from LDAP (ldap_get_entries() returned false) - is your ldap_dn correct?"); 131 | return; 132 | } 133 | if ($entries['count'] == 0) 134 | { 135 | DisplayWarning('Unable to retrieve list of groups for this user from LDAP ($entries[\'count\'] == 0) - is your ldap_dn correct?'); 136 | return; 137 | } 138 | if (!array_key_exists($login_name, $g_auth_roles_map)) 139 | { 140 | // Create an empty array to fill up with groups this user is member of 141 | $g_auth_roles_map[$login_name] = []; 142 | } 143 | // Convert these insane LDAP strings to human readable format that it's far easier to work with 144 | $ldap_group_entries = []; 145 | foreach ($entries[0]['memberof'] as $ldap_group_entry) 146 | { 147 | $ldap_group_name = LDAP_GroupNameFromCN($ldap_group_entry); 148 | // Only store relevant groups, users are typically members of many groups, but we only care about these which also exist as roles 149 | if (array_key_exists($ldap_group_name, $g_auth_roles)) 150 | $ldap_group_entries[] = $ldap_group_name; 151 | } 152 | $g_auth_roles_map[$login_name] = array_merge($g_auth_roles_map[$login_name], $ldap_group_entries); 153 | // Preserve the list of groups this user is in 154 | $_SESSION['groups'] = $g_auth_roles_map[$login_name]; 155 | } 156 | } 157 | 158 | function ProcessLogin() 159 | { 160 | global $g_auth, $g_auth_ldap_url, $g_login_failed, $g_auth_allowed_users, $g_auth_fetch_domain_groups, $g_auth_roles_map, 161 | $g_auth_ldap_dn, $g_auth_domain_prefix, $g_auth_roles, $g_auth_disallow_users_with_no_roles; 162 | 163 | // We support LDAP at this moment only 164 | if ($g_auth != "ldap") 165 | { 166 | ProcessLogin_Error("Unsupported authentication mechanism"); 167 | return; 168 | } 169 | 170 | // If user is already logged in, do nothing (probably just hit refresh in browser and re-sent POST data) 171 | if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) 172 | { 173 | DisplayWarning('You are already logged in, if you want to login again as someone else, logout first'); 174 | return; 175 | } 176 | 177 | // Check if we have the credentials 178 | if (!isset($_POST["loginUsername"]) || !isset($_POST["loginPassword"])) 179 | { 180 | ProcessLogin_Error("No credentials provided (loginUsername or loginPassword missing)"); 181 | return; 182 | } 183 | 184 | // Security hole - some LDAP servers will allow anonymous bind so empty password = access granted 185 | // PHP also kind of suck with strlen, so we need to check for multiple return values 186 | 187 | // This probably could be replaced with empty() which however has weird behaviour depending on PHP versions 188 | // so let's be safe here since this is a security thing and implement our own "is_really_empty_string" 189 | $pwl = strlen($_POST["loginPassword"]); 190 | if ($pwl === NULL || $pwl === 0) 191 | { 192 | ProcessLogin_Error('Empty password is not allowed'); 193 | return; 194 | } 195 | 196 | $ldap = ldap_connect($g_auth_ldap_url); 197 | ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); 198 | ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); 199 | 200 | $login_name = $_POST["loginUsername"]; 201 | // Check if we need to tweak the username 202 | if ($g_auth_domain_prefix !== NULL) 203 | { 204 | if (!psf_string_startsWith($login_name, $g_auth_domain_prefix)) 205 | $login_name = $g_auth_domain_prefix . $login_name; 206 | } 207 | 208 | if ($bind = ldap_bind($ldap, $login_name, $_POST["loginPassword"])) 209 | { 210 | // Login OK 211 | if ($g_auth_allowed_users !== NULL) 212 | { 213 | // Check if this user is allowed to login 214 | if (!in_array($login_name, $g_auth_allowed_users)) 215 | { 216 | ProcessLogin_Error("This user is not allowed to login to this tool (username not present in config.php)"); 217 | return; 218 | } 219 | } 220 | 221 | // If it's enabled get a list of LDAP groups for this user 222 | if ($g_auth_fetch_domain_groups) 223 | FetchDomainGroups($ldap, $login_name); 224 | 225 | // Check if only users with some groups are allowed to login 226 | if ($g_auth_roles !== NULL && $g_auth_disallow_users_with_no_roles) 227 | { 228 | if (empty($g_auth_roles_map[$login_name])) 229 | { 230 | ProcessLogin_Error('You are not a member of any group with access to this tool'); 231 | return; 232 | } 233 | } 234 | $_SESSION['user'] = $login_name; 235 | $_SESSION['logged_in'] = true; 236 | $g_logged_in = true; 237 | WriteToAuditFile('login_success'); 238 | IncrementStat('login_success'); 239 | } else 240 | { 241 | // Invalid user / pw 242 | WriteToAuditFile('login_fail', 'username=' . $login_name . ' reason=invalid username or password'); 243 | $g_login_failed = true; 244 | $_SESSION["logged_in"] = false; 245 | IncrementStat('login_fail'); 246 | } 247 | } 248 | 249 | function RequireLogin() 250 | { 251 | global $g_auth, $g_logged_in; 252 | if ($g_logged_in) 253 | return false; 254 | 255 | if ($g_auth === NULL) 256 | return false; 257 | 258 | // We support LDAP at this moment only 259 | if ($g_auth != "ldap") 260 | Error("Unsupported authentication mechanism"); 261 | 262 | // Check if we have the credentials 263 | if (!isset( $_SESSION["user"] ) || !isset( $_SESSION["logged_in"] )) 264 | return true; 265 | 266 | if ($_SESSION["logged_in"]) 267 | { 268 | $g_logged_in = true; 269 | return false; 270 | } 271 | 272 | return true; 273 | } 274 | 275 | function GetLogin() 276 | { 277 | $login_form = new LoginForm(); 278 | return $login_form; 279 | } 280 | -------------------------------------------------------------------------------- /includes/menu.php: -------------------------------------------------------------------------------- 1 | Zone overview", 25 | "Manage zone", 26 | "New / edit record", 27 | "Batch operations" 28 | ]; 29 | $menu = new BS_Tabs($menu_items, $parent); 30 | switch ($g_action) 31 | { 32 | case "manage": 33 | $menu->SelectedTab = 1; 34 | break; 35 | case "new": 36 | case "edit": 37 | $menu->SelectedTab = 2; 38 | break; 39 | case "batch": 40 | $menu->SelectedTab = 3; 41 | break; 42 | } 43 | return $menu; 44 | } 45 | -------------------------------------------------------------------------------- /includes/modify.php: -------------------------------------------------------------------------------- 1 | 0) 74 | Debug("result: " . $result); 75 | WriteToAuditFile('create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment); 76 | IncrementStat('create'); 77 | return true; 78 | } 79 | 80 | //! Replace record - atomic, returns true on success 81 | function DNS_ModifyRecord($zone, $record, $value, $type, $ttl, $comment, $old, $is_fqdn = false) 82 | { 83 | global $g_domains; 84 | if (!NSupdateEscapeCheck($old)) 85 | Error('Invalid data for old record: ' . $old); 86 | $input = "server " . $g_domains[$zone]['update_server'] . "\n"; 87 | // First delete the existing record 88 | $input .= "update delete " . $old . "\n"; 89 | if ($is_fqdn == false) 90 | $input .= ProcessInsertFromPOST($zone, $record, $value, $type, $ttl); 91 | else 92 | $input .= ProcessInsertFromPOST(NULL, $record, $value, $type, $ttl); 93 | $input .= "send\nquit\n"; 94 | $result = ProcessNSUpdateForDomain($input, $zone); 95 | if (strlen($result) > 0) 96 | Debug("result: " . $result); 97 | WriteToAuditFile('replace_delete', $old, $comment); 98 | IncrementStat('replace_delete'); 99 | WriteToAuditFile('replace_create', $record . "." . $zone . " " . $ttl . " " . $type . " " . $value, $comment); 100 | IncrementStat('replace_create'); 101 | return true; 102 | } 103 | 104 | function DNS_DeleteRecord($zone, $record) 105 | { 106 | global $g_domains; 107 | 108 | if (strlen($zone) == 0) 109 | Error("No domain"); 110 | 111 | if (!Zones::IsEditable($zone)) 112 | Error("Domain $zone is not writeable"); 113 | 114 | if (!IsAuthorizedToWrite($zone)) 115 | Error("You are not authorized to edit $zone"); 116 | 117 | if (!NSupdateEscapeCheck($record)) 118 | Error("Invalid delete string: " . $record); 119 | 120 | $input = "server " . $g_domains[$zone]['update_server'] . "\n"; 121 | $input .= "update delete " . $record . "\nsend\nquit\n"; 122 | ProcessNSUpdateForDomain($input, $zone); 123 | WriteToAuditFile("delete", $record); 124 | IncrementStat('delete'); 125 | return true; 126 | } 127 | 128 | //! Try to insert a PTR record for given IP, on failure, warning is emitted and false returned, true returned on success 129 | //! this function is designed as a helper function that is used together with creation of A record, so it's never fatal 130 | function DNS_InsertPTRForARecord($ip, $fqdn, $ttl, $comment) 131 | { 132 | global $g_domains; 133 | Debug('PTR record was requested, checking zone name'); 134 | $ip_parts = explode('.', $ip); 135 | if (count($ip_parts) != 4) 136 | { 137 | DisplayWarning('PTR record was not created: record '. $ip .' is not a valid IPv4 quad'); 138 | return false; 139 | } 140 | $arpa = $ip_parts[3] . '.' . $ip_parts[2] . '.' . $ip_parts[1] . '.' . $ip_parts[0] . '.in-addr.arpa'; 141 | $arpa_zone = Zones::GetZoneForFQDN($arpa); 142 | if ($arpa_zone === NULL) 143 | { 144 | DisplayWarning('PTR record was not created: there is no PTR zone for record '. $ip); 145 | return false; 146 | } 147 | if (!Zones::IsEditable($arpa_zone)) 148 | { 149 | DisplayWarning("PTR record was not created for $ip: zone " . $arpa_zone . ' is read only'); 150 | return false; 151 | } 152 | if (!IsAuthorizedToWrite($arpa_zone)) 153 | { 154 | DisplayWarning("PTR record was not created: you don't have write access to zone " . $arpa_zone); 155 | return false; 156 | } 157 | 158 | Debug('Found PTR useable zone: ' . $arpa_zone); 159 | 160 | if (!psf_string_endsWith($fqdn, '.')) 161 | $fqdn = $fqdn . '.'; 162 | 163 | // Let's insert this record 164 | $input = "server " . $g_domains[$arpa_zone]['update_server'] . "\n"; 165 | $input .= ProcessInsertFromPOST(NULL, $arpa, $fqdn, 'PTR', $ttl); 166 | $input .= "send\nquit\n"; 167 | $result = ProcessNSUpdateForDomain($input, $arpa_zone); 168 | if (strlen($result) > 0) 169 | Debug("result: " . $result); 170 | WriteToAuditFile('create', $arpa . " " . $ttl . " PTR " . $fqdn, $comment); 171 | IncrementStat('create'); 172 | return true; 173 | } 174 | 175 | //! Try to delete a PTR record for a given IP, on failure, warning is emitted and false returned, true returned on success 176 | //! this function is designed as a helper function that is used together with modifications of A record, so it's never fatal 177 | function DNS_DeletePTRForARecord($ip, $fqdn, $comment) 178 | { 179 | global $g_domains; 180 | Debug('PTR record removal was requested, checking zone name'); 181 | $ip_parts = explode('.', $ip); 182 | if (count($ip_parts) != 4) 183 | { 184 | DisplayWarning('PTR record was not deleted: record '. $ip .' is not a valid IPv4 quad'); 185 | return false; 186 | } 187 | $arpa = $ip_parts[3] . '.' . $ip_parts[2] . '.' . $ip_parts[1] . '.' . $ip_parts[0] . '.in-addr.arpa'; 188 | $arpa_zone = Zones::GetZoneForFQDN($arpa); 189 | if ($arpa_zone === NULL) 190 | { 191 | DisplayWarning('PTR record was not deleted: there is no PTR zone for record '. $ip); 192 | return false; 193 | } 194 | if (!Zones::IsEditable($arpa_zone)) 195 | { 196 | DisplayWarning("PTR record was not deleted for $ip: zone " . $arpa_zone . ' is read only'); 197 | return false; 198 | } 199 | if (!IsAuthorizedToWrite($arpa_zone)) 200 | { 201 | DisplayWarning("PTR record was not deleted: you don't have write access to zone " . $arpa_zone); 202 | return false; 203 | } 204 | 205 | Debug('Found PTR useable zone: ' . $arpa_zone); 206 | 207 | if (!psf_string_endsWith($fqdn, '.')) 208 | $fqdn = $fqdn . '.'; 209 | 210 | // Let's insert this record 211 | $input = "server " . $g_domains[$arpa_zone]['update_server'] . "\n"; 212 | $input .= "update delete " . $arpa . " 0 PTR " . $fqdn . "\n"; 213 | $input .= "send\nquit\n"; 214 | $result = ProcessNSUpdateForDomain($input, $arpa_zone); 215 | if (strlen($result) > 0) 216 | Debug("result: " . $result); 217 | WriteToAuditFile('delete', $arpa . " 0 PTR " . $fqdn, $comment); 218 | IncrementStat('delete'); 219 | return true; 220 | } -------------------------------------------------------------------------------- /includes/notifications.php: -------------------------------------------------------------------------------- 1 | WARNING: ' . htmlspecialchars($text), 'warning'); 38 | $warning_box->EscapeHTML = false; 39 | $g_warning_container->AppendObject($warning_box); 40 | } 41 | 42 | public static function DisplayError($text) 43 | { 44 | global $g_error_container, $g_api_errors; 45 | if (G_DNSTOOL_ENTRY_POINT === "api.php") 46 | { 47 | // API have separate container as we don't work with HTML there 48 | $g_api_errors[] = $text; 49 | return; 50 | } 51 | $fatal_box = new BS_Alert('ERROR: ' . $text, 'danger'); 52 | $fatal_box->EscapeHTML = false; 53 | $g_error_container->AppendObject($fatal_box); 54 | } 55 | } -------------------------------------------------------------------------------- /includes/nsupdate.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'r'), 42 | 1 => array('pipe', 'w'), 43 | 2 => array('pipe', 'w') 44 | ); 45 | $pipes = array(); 46 | $cwd = '/tmp'; 47 | $env = array(); 48 | $proc = proc_open($g_nsupdate, $desc, $pipes, $cwd, $env); 49 | if (!is_resource($proc)) 50 | { 51 | Error("Unable to execute " . $g_nsupdate); 52 | } 53 | Debug("proc_open(" . $g_nsupdate . ', $desc, $pipes, $cwd, $env)'); 54 | Debug("Sending this to nsupdate:\n" . $input); 55 | fwrite($pipes[0], $input); 56 | $output = stream_get_contents($pipes[1]); 57 | $errors = stream_get_contents($pipes[2]); 58 | fclose($pipes[0]); 59 | fclose($pipes[1]); 60 | fclose($pipes[2]); 61 | $ret = proc_close($proc); 62 | if ($ret > 0) 63 | { 64 | Error($g_nsupdate . " return code " . $ret . ": " . $errors); 65 | } 66 | return $output; 67 | } 68 | 69 | function dig($parameters) 70 | { 71 | global $g_dig; 72 | // Replace newlines for security reasons 73 | $parameters = str_replace("\n", "", $parameters); 74 | if (!ShellEscapeCheck($parameters)) 75 | die('FATAL: Invalid shell parameters: ' . $parameters); 76 | Debug("shell_exec: " . $g_dig . " " . $parameters); 77 | return shell_exec($g_dig . " " . $parameters); 78 | } 79 | 80 | // Convert standard DNS list of records as returned by transfer to PHP array 81 | function raw_zone_to_array($data) 82 | { 83 | $records = array(); 84 | $data = explode("\n", $data); 85 | foreach ($data as $line) 86 | { 87 | if (psf_string_startsWith($line, ";")) 88 | continue; 89 | // This is a little bit hard-core, we need to parse output from dig, which is terrible 90 | // In past we did some magic by simply replacing all tabs and spaces to split it, but that doesn't work 91 | // for some special TXT records 92 | // For example: 93 | // 2 tabs tab double space 94 | // v v v 95 | // example.org. 600 IN TXT "v=spf1 a mx include:_spf.example.org ip4:124.6.178.206 ~all" 96 | // 97 | // keep in mind that dig is randomly using tabs as separators and randomly spaces 98 | // 99 | // So there are two easy ways of this mess 100 | // 1) we use regular expressions and pray a lot (we use this one) 101 | // 2) we simply walk through out the whole string, that's the correct way, but this is actually CPU intensive, 102 | // so we might want to implement this into some kind of C library I guess 103 | 104 | // Get rid of empty lines 105 | if (strlen(str_replace(" ", "", $line)) == 0) 106 | continue; 107 | 108 | $records[] = preg_split('/[\t\s]/', $line, 5, PREG_SPLIT_NO_EMPTY); 109 | } 110 | return $records; 111 | } 112 | 113 | function get_zone_data($zone) 114 | { 115 | global $g_domains; 116 | $zone_servers = $g_domains[$zone]; 117 | $data = dig("axfr " . $zone . " @" . $zone_servers["transfer_server"]); 118 | return raw_zone_to_array($data); 119 | } 120 | 121 | function get_zone_soa($zone) 122 | { 123 | global $g_domains; 124 | $zone_servers = $g_domains[$zone]; 125 | $data = dig("SOA " . $zone . " @" . $zone_servers["transfer_server"]); 126 | return raw_zone_to_array($data); 127 | } 128 | 129 | function get_records_from_zone($fqdn, $type, $zone) 130 | { 131 | global $g_domains; 132 | $zone_servers = $g_domains[$zone]; 133 | $data = dig('+nocomments +noauthority +noadditional ' . $type . " '" . $fqdn . "' @" . $zone_servers["transfer_server"]); 134 | return raw_zone_to_array($data); 135 | } 136 | -------------------------------------------------------------------------------- /includes/record_list.php: -------------------------------------------------------------------------------- 1 | EscapeHTML = false; 45 | 46 | if (array_key_exists('in_transfer', $domain_info) && $domain_info['in_transfer'] === true) 47 | { 48 | $is_ok = false; 49 | $status->Text .= ' Warning: This domain is being transfered between different master servers
'; 50 | } 51 | if (array_key_exists('read_only', $domain_info) && $domain_info['read_only'] === true) 52 | { 53 | $is_ok = false; 54 | $status->Text .= ' Warning: This domain is read only
'; 55 | } 56 | if (!IsAuthorizedToRead($domain)) 57 | { 58 | $is_ok = false; 59 | $status->Text .= ' Can\'t read: you are not authorized to read this zone.
'; 60 | } 61 | if (!IsAuthorizedToWrite($domain)) 62 | { 63 | $is_ok = false; 64 | $status->Text .= ' Can\'t write: you are not authorized to write this zone.
'; 65 | } 66 | if (array_key_exists('maintenance_note', $domain_info)) 67 | { 68 | $is_ok = false; 69 | $status->Text .= ' Maintenance note: ' .$domain_info['maintenance_note']; 70 | } 71 | if (array_key_exists('note', $domain_info)) 72 | { 73 | $is_ok = false; 74 | $status->Text .= ' Note: ' .$domain_info['note']; 75 | } 76 | 77 | if ($is_ok) 78 | return NULL; 79 | 80 | return $status; 81 | } 82 | 83 | // This function will go through parsed zone data and will return SOA record if present, otherwise will return NULL 84 | function GetSOAFromData($data) 85 | { 86 | $soa = NULL; 87 | foreach ($data as $record) 88 | { 89 | if ($record[3] === 'SOA') 90 | { 91 | $soa = $record[4]; 92 | break; 93 | } 94 | } 95 | return $soa; 96 | } 97 | 98 | //! Go through whole zone and check if SOA is present on end 99 | function CheckIfZoneIsComplete($data) 100 | { 101 | if ($data[0][3] !== 'SOA') 102 | return false; 103 | if (end($data)[3] !== 'SOA') 104 | return false; 105 | return true; 106 | } 107 | 108 | function GetRecordList($zone) 109 | { 110 | global $g_caching_engine, $g_caching_engine_instance, $g_retry_on_error; 111 | if (!IsAuthorizedToRead($zone)) 112 | return array(); 113 | 114 | // Check if zone data exist in cache 115 | if ($g_caching_engine_instance->IsCached($zone)) 116 | { 117 | // There is something in the zone cache 118 | Debug('Zone ' . $zone . ' exist in cache, checking if SOA record is identical'); 119 | $current_soa = GetSOAFromData(get_zone_soa($zone)); 120 | $cached_soa = $g_caching_engine_instance->GetSOA($zone); 121 | if ($current_soa === NULL) 122 | { 123 | // Something is very wrong - there is no SOA record in our query 124 | // Check if retry on error is enabled, if yes, try N more times, if it doesn't help, show error 125 | if ($g_retry_on_error > 0) 126 | { 127 | $retry = $g_retry_on_error; 128 | $current_retry = 0; 129 | while ($retry-- > 0) 130 | { 131 | $current_retry++; 132 | Debug("Unable to retrieve SOA record for " . $zone . " (dig SOA returned no data), retrying dig ($current_retry/$g_retry_on_error)..."); 133 | $current_soa = GetSOAFromData(get_zone_soa($zone)); 134 | if ($current_soa !== NULL) 135 | { 136 | DisplayWarning("Transfer NS for " . $zone . " had troubles returning SOA record, had to retry $current_retry times, check your network"); 137 | break; 138 | } 139 | } 140 | } 141 | // if SOA is still NULL, show non-blocking error 142 | if ($current_soa === NULL) 143 | Error("Unable to retrieve SOA record for " . $zone . " - (dig SOA) transfer NS didn't return any data for it", false); 144 | } else if ($current_soa != $cached_soa) 145 | { 146 | Debug("Cache miss: '$current_soa' != '$cached_soa'"); 147 | } else if ($current_soa == $cached_soa) 148 | { 149 | Debug("Cache match! Not running a full zone transfer"); 150 | // Return data from cache instead of running full zone transfer 151 | $result = $g_caching_engine_instance->GetData($zone); 152 | if ($result === false) 153 | { 154 | Debug('SOA exist in cache, but data not (corrupted cache), dropping memory and falling back to full zone transfer'); 155 | $g_caching_engine_instance->Drop($zone); 156 | } else 157 | { 158 | WriteToAuditFile("display", $zone, "(cached)"); 159 | IncrementStat('display_zone_cached'); 160 | return $result; 161 | } 162 | } 163 | } else if ($g_caching_engine !== NULL) 164 | { 165 | Debug('Zone ' . $zone . ' does not exist in cache, running full zone transfer'); 166 | } 167 | 168 | WriteToAuditFile("display", $zone, "(full transfer)"); 169 | IncrementStat('display_zone'); 170 | Debug('Running full zone transfer for: ' . $zone); 171 | $data = get_zone_data($zone); 172 | $soa = GetSOAFromData($data); 173 | if ($soa === NULL) 174 | { 175 | // Again - server returned no SOA record, there is some network issue 176 | if ($g_retry_on_error > 0) 177 | { 178 | $retry = $g_retry_on_error; 179 | $current_retry = 0; 180 | while ($retry-- > 0) 181 | { 182 | $current_retry++; 183 | Debug("Unable to retrieve SOA record for " . $zone . " (dig AXFR returned no data), retrying dig ($current_retry/$g_retry_on_error)..."); 184 | $data = get_zone_data($zone); 185 | $soa = GetSOAFromData($data); 186 | if ($soa !== NULL) 187 | { 188 | DisplayWarning("Transfer NS for " . $zone . " had troubles returning SOA record during AXFR, had to retry $current_retry times, check your network"); 189 | break; 190 | } 191 | } 192 | } 193 | if ($soa === NULL) 194 | Error("Unable to retrieve SOA record for " . $zone . " - (dig AXFR) transfer NS didn't return any data for it", false); 195 | else 196 | $g_caching_engine_instance->CacheZone($zone, $soa, $data); 197 | } else 198 | { 199 | $g_caching_engine_instance->CacheZone($zone, $soa, $data); 200 | } 201 | 202 | if (count($data) > 0 && !CheckIfZoneIsComplete($data)) 203 | DisplayWarning("Transfer NS for " . $zone . " didn't return full zone, last SOA record is missing, zone data are incomplete"); 204 | 205 | return $data; 206 | } 207 | 208 | function GetRecordListTable($parent, $domain) 209 | { 210 | global $g_editable, $g_show_hidden_types, $g_hidden_record_types, $g_hidden_types_present, $g_total_records_count, $g_hidden_records_count; 211 | $table = new BS_Table($parent); 212 | $table->Condensed = true; 213 | $table->Headers = [ "Record", "TTL", "Scope", "Type", "Value", "Options" ]; 214 | $table->SetColumnWidth(2, '80px'); // Scope 215 | $table->SetColumnWidth(3, '80px'); // Type 216 | $table->SetColumnWidth(5, '80px'); // Options 217 | $records = GetRecordList($domain); 218 | $is_editable = Zones::IsEditable($domain) && IsAuthorizedToWrite($domain); 219 | $has_ptr = Zones::HasPTRZones(); 220 | foreach ($records as $record) 221 | { 222 | $g_total_records_count++; 223 | if (in_array($record[3], $g_hidden_record_types)) 224 | { 225 | $g_hidden_types_present = true; 226 | if (!$g_show_hidden_types) 227 | { 228 | $g_hidden_records_count++; 229 | continue; 230 | } 231 | } 232 | if (!$is_editable || !in_array($record[3], $g_editable)) 233 | { 234 | $record[] = ''; 235 | } else 236 | { 237 | $delete_record = ''; 239 | $delete_record_with_ptr = ''; 240 | if ($has_ptr && $record[3] == 'A') 241 | { 242 | // Optional button to delete record together with PTR record, show only if there are PTR zones in cfg 243 | $delete_record_with_ptr = ''; 246 | } 247 | $large_space = '  '; 248 | $record[] = $delete_record . $large_space . '' . $large_space . $delete_record_with_ptr; 252 | } 253 | $record[4] = '' . $record[4] . ''; 254 | $table->AppendRow($record); 255 | } 256 | if ($is_editable) { 257 | $add = ''; 258 | $table->AppendRow(['', '', '', '', '', $add]); 259 | } 260 | return $table; 261 | } 262 | 263 | //! This return similar results to GetRecordListTable but without option buttons in format friendly for exporting 264 | function GetRecordListTablePlainFormat($parent, $domain) 265 | { 266 | $table = new HtmlTable($parent); 267 | $table->Headers = [ "Record", "TTL", "Scope", "Type", "Value" ]; 268 | $records = GetRecordList($domain); 269 | foreach ($records as $record) 270 | { 271 | $table->AppendRow($record); 272 | } 273 | return $table; 274 | } 275 | -------------------------------------------------------------------------------- /includes/tab_batch.php: -------------------------------------------------------------------------------- 1 | AppendObject(new BS_Alert("Successfully executed batch operation on zone " . $zone)); 74 | } 75 | 76 | public static function GetForm($parent) 77 | { 78 | global $g_audit, $g_selected_domain, $g_domains, $g_editable; 79 | $form = new Form("index.php?action=batch", $parent); 80 | $form->Method = FormMethod::Post; 81 | $layout = new HtmlTable($form); 82 | $layout->BorderSize = 0; 83 | $dl = new BS_ComboBox("zone", $layout); 84 | foreach ($g_domains as $key => $info) 85 | { 86 | if (!IsAuthorizedToWrite($key)) 87 | continue; 88 | if ($g_selected_domain == $key) 89 | $dl->AddDefaultValue($key, $key); 90 | else 91 | $dl->AddValue($key, $key); 92 | } 93 | $layout->Width = "600px"; 94 | $layout->AppendRow( [ "This form allows execution of nsupdate commands, so that you can execute multiple nsupdate operations at once
" . 95 | "Example:
update add record.domain.org 3600 A 1.2.3.4\nupdate delete record2.domain.org
" ] ); 96 | $layout->AppendRow( [ "Note: only update statements are allowed, don't put send there, it will be there automatically" ] ); 97 | $layout->AppendRow( [ $dl ] ); 98 | $input = new BS_TextBox("record", NULL, NULL, $layout); 99 | $input->SetMultiline(); 100 | $layout->AppendRow( [ $input ] ); 101 | if ($g_audit) 102 | { 103 | $comment = new BS_TextBox("comment", NULL, NULL, $layout); 104 | $comment->Placeholder = 'Optional comment for audit log'; 105 | $layout->AppendRow( [ $comment ] ); 106 | } 107 | $form->AppendObject(new BS_Button("submit", "Submit")); 108 | return $form; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /includes/tab_edit.php: -------------------------------------------------------------------------------- 1 | AppendObject(new BS_Alert("Successfully inserted record " . $record . "." . $zone)); 75 | } else if ($_POST["submit"] == "Edit") 76 | { 77 | if (!isset($_POST["old"])) 78 | Error("Missing old record necessary for update"); 79 | if (DNS_ModifyRecord($zone, $record, $value, $type, $ttl, $comment, $_POST["old"])) 80 | { 81 | $form->AppendObject(new BS_Alert("Successfully replaced " . $_POST["old"] . " with " . $record . "." . $zone . " " . 82 | $ttl . " " . $type . " " . $value)); 83 | } 84 | // Delete PTR if wanted 85 | if (isset($_POST['ptr']) && $_POST['ptr'] === "true") 86 | { 87 | if (isset($_POST["old_type"]) && $_POST["old_type"] == "A") 88 | { 89 | // Check if all necessary values are present 90 | if (!isset($_POST["old_value"]) || !isset($_POST["old_record"])) 91 | { 92 | DisplayWarning("PTR record was not deleted, because old_record or old_value was missing"); 93 | } else 94 | { 95 | DNS_DeletePTRForARecord($_POST["old_value"], $_POST["old_record"], $comment); 96 | } 97 | } else 98 | { 99 | Debug("Not removing PTR, original type was " . $_POST["old_type"]); 100 | } 101 | } 102 | } else 103 | { 104 | Error("Unknown modify mode"); 105 | } 106 | 107 | // Create PTR if wanted 108 | if (isset($_POST['ptr']) && $_POST['ptr'] === "true") 109 | { 110 | if ($type !== "A") 111 | { 112 | DisplayWarning('PTR record was not created: PTR record can be only created when you are inserting A record, you created ' . $type . ' record instead'); 113 | return; 114 | } 115 | DNS_InsertPTRForARecord($value, $record . '.' . $zone, $ttl, $comment); 116 | } 117 | } 118 | 119 | public static function GetInsertForm($parent, $edit_mode = false, $default_key = "", $default_ttl = NULL, $default_type = "A", $default_value = "", $default_comment = "") 120 | { 121 | global $g_audit, $g_selected_domain, $g_domains, $g_editable; 122 | 123 | // In case we are returning to insert form from previous insert, make default type the one we used before 124 | if (isset($_POST['type'])) 125 | $default_type = $_POST['type']; 126 | else if (isset($_GET['type'])) 127 | $default_type = $_GET['type']; 128 | else if (psf_string_endsWith($g_selected_domain, ".in-addr.arpa")) 129 | $default_type = "PTR"; 130 | 131 | // Reuse some values from previous POST request 132 | if (isset($_POST['comment'])) 133 | $default_comment = $_POST['comment']; 134 | 135 | if (isset($_POST['ttl'])) 136 | $default_ttl = $_POST['ttl']; 137 | 138 | // If ttl is not specified use default one from config file 139 | if ($default_ttl === NULL) 140 | $default_ttl = strval(Zones::GetDefaultTTL($g_selected_domain)); 141 | 142 | $form = new Form("index.php?action=new", $parent); 143 | $form->Method = FormMethod::Post; 144 | $layout = new HtmlTable($form); 145 | $layout->BorderSize = 0; 146 | $layout->ColWidth[4] = '40%'; 147 | $layout->Headers = [ "Record", "Zone", "TTL", "Type", "Value" ]; 148 | if ($g_audit) 149 | $layout->Headers[] = 'Comment'; 150 | $form_items = []; 151 | $form_items[] = new BS_TextBox("record", $default_key, NULL, $layout); 152 | $dl = new BS_ComboBox("zone", $layout); 153 | if ($edit_mode) 154 | { 155 | if ($g_selected_domain === NULL) 156 | { 157 | Error("No domain selected"); 158 | } 159 | $dl->AddDefaultValue($g_selected_domain, "." . $g_selected_domain); 160 | $dl->Enabled = false; 161 | // we must add a hidden element that preserves the zone because disabled HTML elements do not submit form data 162 | $form->AppendObject(new Hidden("zone", $g_selected_domain)); 163 | } else 164 | { 165 | foreach ($g_domains as $key => $info) 166 | { 167 | if (!IsAuthorizedToWrite($key)) 168 | continue; 169 | if ($g_selected_domain == $key) 170 | $dl->AddDefaultValue($key, "." . $key); 171 | else 172 | $dl->AddValue($key, '.' . $key); 173 | } 174 | } 175 | $form_items[] = $dl; 176 | $form_items[] = new BS_TextBox("ttl", $default_ttl, NULL, $layout); 177 | $tl = new BS_ComboBox("type", $layout); 178 | $types = $g_editable; 179 | foreach ($types as $type) 180 | { 181 | if ($default_type == $type) 182 | $tl->AddDefaultValue($type); 183 | else 184 | $tl->AddValue($type); 185 | } 186 | $form_items[] = $tl; 187 | $value_box = new BS_TextBox("value", $default_value, NULL, $layout); 188 | $value_box->Size = 45; 189 | $form_items[] = $value_box; 190 | if ($g_audit) 191 | { 192 | $comment = new BS_TextBox("comment", $default_comment, NULL, $layout); 193 | $comment->Placeholder = 'Optional comment for audit log'; 194 | $comment->Size = 80; 195 | $form_items[] = $comment; 196 | } 197 | $layout->AppendRow($form_items); 198 | if (Zones::HasPTRZones()) 199 | { 200 | if (!$edit_mode) 201 | $form->AppendObject(new BS_CheckBox("ptr", "true", false, NULL, $form, "Create PTR record for this IP (works only with A records)")); 202 | else 203 | $form->AppendObject(new BS_CheckBox("ptr", "true", false, NULL, $form, "Modify underlying PTR records (works only if original, new or both values are A records)")); 204 | } 205 | if (isset($_GET["old"])) 206 | $form->AppendObject(new Hidden("old", htmlspecialchars($_GET["old"]))); 207 | 208 | if ($edit_mode) 209 | { 210 | // Preserve old values, we need to work with them when modifying PTR records 211 | $form->AppendObject(new Hidden("old_record", htmlspecialchars($_GET["key"]))); 212 | $form->AppendObject(new Hidden("old_ttl", htmlspecialchars($default_ttl))); 213 | $form->AppendObject(new Hidden("old_type", htmlspecialchars($default_type))); 214 | $form->AppendObject(new Hidden("old_value", htmlspecialchars($default_value))); 215 | 216 | $form->AppendObject(new BS_Button("submit", "Edit")); 217 | } else 218 | { 219 | $form->AppendObject(new BS_Button("submit", "Create")); 220 | } 221 | return $form; 222 | } 223 | 224 | public static function GetHelp() 225 | { 226 | $help = new DivContainer(); 227 | $help->AppendLine(); 228 | $help->AppendHtmlLine('Display help'); 229 | $c = new DivContainer($help); 230 | $c->ClassName = "collapse"; 231 | $c->ID = "collapseHelp"; 232 | $c->AppendHeader("Record", 3); 233 | $c->AppendHtmlLine('Name of the key you want to add. If you want to create DNS record test.domain.org in zone domain.org, then value of field record will be just test. Do not append zone name to record name, this is done automatically. Record can be also left blank if you want to add a record for zone apex (zone itself), such as MX records.'); 234 | $c->AppendHeader("Zone", 3); 235 | $c->AppendHtmlLine('Name of zone you want to create record in. In case that subzone exist (for example you want to add record subzone.test.domain.org but subzone test.domain.org exists in dropdown menu), you must create the record within the subzone, not in the parent zone, otherwise it will not be visible in domain name system. If no subzone exists, then you can create a record subzone.test inside of domain.org.'); 236 | $c->AppendHeader("TTL", 3); 237 | $c->AppendHtmlLine('Time to live tells caching name servers for how long can this record be cached for. Too low TTL may lead to performance issues as the request to resolve such record will be forwarded to authoritative name server most of the time. Too long TTL can make it complicated to change the value of record, because caching name servers will hold the cached value for too long. If you are not sure which value to pick, leave the default value.'); 238 | $c->AppendHeader("Type", 3); 239 | $c->AppendHtmlLine('Type of DNS record, following record types are most common:'); 240 | $record_types = new BS_Table($c); 241 | $record_types->Headers = [ 'Type', 'Description' ]; 242 | $record_types->AppendRow( [ 'A', 'IPv4 record, value of this record is IPv4 address, for example 1.2.3.4' ]); 243 | $record_types->AppendRow( [ 'AAAA', 'IPv6 record, value of this record is IPv6 address, for example ::1' ]); 244 | $record_types->AppendRow( [ 'TXT', 'Text record, must be max 255 characters in length, otherwise you need to split it to multiple parts within quotes ("), each part max. 255 characters in size' ]); 245 | $record_types->AppendRow( [ 'MX', 'Mail server record, value consist of two parts, priority and hostname of mail server, for example: 10 mail.domain.org']); 246 | $record_types->AppendRow( [ 'NS', 'Delegates a record to another name server. If used on zone apex it defines authoritative name servers for a zone.']); 247 | $record_types->AppendRow( [ 'SSHFP', 'SSH fingerprint, used by SSH client when verifying that target server has authentic fingerprint']); 248 | $record_types->AppendRow( [ 'CNAME', 'Redirect record to another domain name, this will redirect all record types for given record name and therefore can\'t be used on zone apex']); 249 | $record_types->AppendRow( [ 'SOA', 'Start of authority record - this record exists only for apex of zone and denotes existence of a zone, it includes administrative data for zone, this record is returned twice in zone transfer, as first and last record']); 250 | $c->AppendHtmlLine('See https://en.wikipedia.org/wiki/List_of_DNS_record_types for a more complete and detailed list'); 251 | $c->AppendHeader("Value", 3); 252 | $c->AppendHtmlLine('Value of record, format depends on record type'); 253 | $c->AppendHeader("Comment", 3); 254 | $c->AppendHtmlLine('Optional comment for audit log of DNS tool, this has no effect on DNS server itself. This field is available only if audit subsystem is enabled.'); 255 | //$c->AppendObject(new BS_List); 256 | return $help; 257 | } 258 | 259 | public static function GetEditForm($parent) 260 | { 261 | global $g_selected_domain; 262 | $k = $_GET["key"]; 263 | $suffix = $g_selected_domain; 264 | if (psf_string_endsWith($k, $suffix)) 265 | $k = substr($k, 0, strlen($k) - strlen($suffix)); 266 | if (psf_string_endsWith($k, $suffix . ".")) 267 | $k = substr($k, 0, strlen($k) - strlen($suffix) - 1); 268 | while (psf_string_endsWith($k, ".")) 269 | $k = substr($k, 0, strlen($k) - 1); 270 | 271 | return self::GetInsertForm($parent, true, $k, $_GET["ttl"], $_GET["type"], $_GET["value"]); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /includes/tab_manage.php: -------------------------------------------------------------------------------- 1 | AppendObject(new BS_Alert("Successfully deleted record " . $record)); 35 | 36 | if (isset($_GET["ptr"]) && $_GET["ptr"] == true) 37 | { 38 | Debug('PTR record deletion was requested for ' . $record); 39 | if (!isset($_GET['key']) || !isset($_GET['value']) || !isset($_GET['type'])) 40 | { 41 | Warning('PTR record was not removed because either key, value or type was not specified'); 42 | return; 43 | } 44 | $key = $_GET['key']; 45 | $type = $_GET['type']; 46 | $value = $_GET['value']; 47 | if ($type != 'A') 48 | { 49 | Warning('Requested PTR record was not deleted: PTR record can be only deleted when you are changing A record, you deleted ' . $type . ' record instead'); 50 | } else 51 | { 52 | DNS_DeletePTRForARecord($value, $key, ''); 53 | } 54 | } 55 | } 56 | 57 | public static function GetContents($fc) 58 | { 59 | global $g_auth_session_name, $g_domains, $g_selected_domain, $g_total_records_count, $g_hidden_records_count, $g_show_hidden_types, $g_hidden_types_present; 60 | 61 | // Check toggle for hidden 62 | if (isset($_GET['hidden_types'])) 63 | { 64 | if ($_GET['hidden_types'] == 'show') 65 | { 66 | setcookie($g_auth_session_name . '_show_hidden_types', true); 67 | $g_show_hidden_types = true; 68 | } else 69 | { 70 | setcookie($g_auth_session_name . '_show_hidden_types', false); 71 | $g_show_hidden_types = false; 72 | } 73 | } else 74 | { 75 | // Check if there is a cookie for hidden types 76 | if (isset($_COOKIE[$g_auth_session_name . '_show_hidden_types'])) 77 | $g_show_hidden_types = $_COOKIE[$g_auth_session_name . '_show_hidden_types']; 78 | } 79 | 80 | if ($g_selected_domain == null) 81 | { 82 | reset($g_domains); 83 | $g_selected_domain = key($g_domains); 84 | } 85 | // First get the record list - this function will fill up g_hidden_types_present variable as well as global counters 86 | $record_list = GetRecordListTable(NULL, $g_selected_domain); 87 | $record_count = ""; 88 | if ($g_total_records_count > 0) 89 | { 90 | if ($g_hidden_records_count == 0) 91 | $record_count = " ($g_total_records_count records)"; 92 | else 93 | $record_count = " ($g_total_records_count records, $g_hidden_records_count hidden)"; 94 | } 95 | $fc->AppendObject(GetSwitcher($fc)); 96 | $fc->AppendHeader($g_selected_domain . $record_count, 2); 97 | $fc->AppendHtml('
Export as CSV
'); 98 | $fc->AppendObject(GetStatusOfZoneAsNote($g_selected_domain)); 99 | if ($g_hidden_types_present === true) 100 | { 101 | // This zone contains some hidden record types, show toggle for user 102 | if (!$g_show_hidden_types) 103 | $fc->AppendHtml('
This zone contains record types that are hidden by default, click here to show them
'); 104 | else 105 | $fc->AppendHtml('
This zone contains record types that are hidden by default, click here to hide them
'); 106 | } 107 | $fc->AppendObject($record_list); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /includes/tab_overview.php: -------------------------------------------------------------------------------- 1 |  '; 37 | } 38 | if (!IsAuthorizedToWrite($domain) || (array_key_exists('read_only', $domain_info) && $domain_info['read_only'] === true)) 39 | { 40 | $is_ok = false; 41 | $status .= ' '; 42 | } 43 | if (array_key_exists('maintenance_note', $domain_info)) 44 | { 45 | $is_ok = false; 46 | $status .= ' '; 47 | } 48 | if (array_key_exists('note', $domain_info)) 49 | { 50 | $status .= '  '; 51 | } 52 | 53 | if ($is_ok) 54 | return '' . $status; 55 | return $status; 56 | } 57 | 58 | //! Generates a PSF table object with all zones with links to manage each zone, including their status 59 | public static function GetSelectForm($parent) 60 | { 61 | global $g_domains; 62 | $table = new BS_Table($parent); 63 | $table->Headers = [ "Domain name", "Status", "Update server", "Transfer server" ]; 64 | $table->SetColumnWidth(1, '80px'); 65 | foreach ($g_domains as $domain => $properties) 66 | { 67 | if (!IsAuthorizedToRead($domain)) 68 | continue; 69 | $table->AppendRow([ '' . $domain . '', self::getStatusOfZone($domain), $properties["update_server"], $properties["transfer_server"] ]); 70 | } 71 | return $table; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /includes/validator.php: -------------------------------------------------------------------------------- 1 | $properties) 25 | { 26 | if (!IsAuthorizedToRead($domain)) 27 | continue; 28 | $result[$domain] = [ 'domain' => $domain, 'update_server' => $properties['update_server'], 'transfer_server' => $properties['transfer_server'] ]; 29 | 30 | if (isset($properties['in_transfer'])) 31 | $result[$domain]['in_transfer'] = $properties['in_transfer']; 32 | 33 | if (isset($properties['note'])) 34 | $result[$domain]['note'] = $properties['note']; 35 | 36 | if (isset($properties['maintenance_note'])) 37 | $result[$domain]['maintenance_note'] = $properties['maintenance_note']; 38 | 39 | if (isset($properties['read_only'])) 40 | $result[$domain]['read_only'] = $properties['read_only']; 41 | } 42 | return $result; 43 | } 44 | 45 | public static function IsEditable($domain) 46 | { 47 | global $g_domains; 48 | if (!array_key_exists($domain, $g_domains)) 49 | die("No such zone: $domain"); 50 | 51 | $domain_info = $g_domains[$domain]; 52 | 53 | if (array_key_exists('read_only', $domain_info) && $domain_info['read_only'] === true) 54 | return false; 55 | 56 | return true; 57 | } 58 | 59 | public static function GetZoneForFQDN($fqdn) 60 | { 61 | global $g_domains; 62 | do 63 | { 64 | if (!array_key_exists($fqdn, $g_domains)) 65 | { 66 | $fqdn= substr($fqdn, strpos($fqdn, '.') + 1); 67 | continue; 68 | } 69 | return $fqdn; 70 | } while (psf_string_contains($fqdn, '.')); 71 | return NULL; 72 | } 73 | 74 | public static function HasPTRZones() 75 | { 76 | global $g_domains; 77 | foreach ($g_domains as $key => $info) 78 | { 79 | if (psf_string_endsWith($key, ".in-addr.arpa")) 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | public static function GetDefaultTTL($domain) 86 | { 87 | global $g_default_ttl, $g_domains; 88 | 89 | if ($domain === NULL) 90 | return $g_default_ttl; 91 | 92 | if (!array_key_exists($domain, $g_domains)) 93 | die("No such zone: $domain"); 94 | 95 | $domain_info = $g_domains[$domain]; 96 | 97 | if (array_key_exists('ttl', $domain_info)) 98 | return $domain_info['ttl']; 99 | 100 | return $g_default_ttl; 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | ExternalCss[] = 'style.css'; 57 | if (!$g_use_local_jquery) 58 | $website->ExternalJs[] = "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"; 59 | else 60 | $website->ExternalJs[] = "jquery-3.3.1.min.js"; 61 | $website->Style->items["td"]["word-wrap"] = "break-word"; 62 | $website->Style->items["td"]["max-width"] = "280px"; 63 | bootstrap_init($website); 64 | 65 | // Create a bootstrap fluid containers, one for whole website and one for errors, which are dynamically inserted to error container as they are generated 66 | $fc = new BS_FluidContainer($website); 67 | 68 | if (isset($_GET['login'])) 69 | ProcessLogin(); 70 | 71 | if (isset($_GET['logout'])) 72 | session_unset(); 73 | 74 | // Recover unfinished request 75 | // we must do this before checking POST and GET parameters - because they take precedence over preserved data 76 | if (isset($_SESSION['preserved_domain'])) 77 | { 78 | $g_selected_domain = $_SESSION['preserved_domain']; 79 | unset($_SESSION['preserved_domain']); 80 | } 81 | 82 | if (isset($_SESSION['preserved_action'])) 83 | { 84 | $g_action = $_SESSION['preserved_action']; 85 | unset($_SESSION['preserved_action']); 86 | } 87 | 88 | if (isset($_GET['action'])) 89 | $g_action = $_GET['action']; 90 | 91 | if (isset($_GET['domain'])) 92 | $g_selected_domain = $_GET['domain']; 93 | else if (isset($_POST['zone'])) 94 | $g_selected_domain = $_POST['zone']; 95 | 96 | // Check if login is needed 97 | if (RequireLogin()) 98 | { 99 | // If we were trying to run action=manage on some domain, preserve the link as much as we can, 100 | // so that user can resume the operation after login 101 | if ($g_action === 'manage' || $g_action === 'new' || $g_action === 'batch') 102 | $_SESSION['preserved_action'] = $g_action; 103 | if ($g_selected_domain !== null) 104 | $_SESSION['preserved_domain'] = $g_selected_domain; 105 | $fc->AppendHeader('Login to ' . G_HEADER); 106 | if ($g_auth_login_banner !== NULL) 107 | $fc->AppendObject(new BS_Alert($g_auth_login_banner, 'info')); 108 | 109 | // Display warnings and errors if there are any 110 | $fc->AppendObject($g_warning_container); 111 | $fc->AppendObject($g_error_container); 112 | 113 | if ($g_login_failed) 114 | $fc->AppendObject(new BS_Alert($g_login_failure_reason, 'danger')); 115 | $fc->AppendObject(GetLogin()); 116 | } else 117 | { 118 | $header = new DivContainer($fc); 119 | $header->ClassName = 'header'; 120 | $header->AppendObject(new Image("favicon.png", "DNS")); 121 | $header->AppendHeader(G_HEADER); 122 | if ($g_logged_in) 123 | $fc->AppendHtml(GetLoginInfo()); 124 | 125 | // Display warnings if there are any 126 | $fc->AppendObject($g_warning_container); 127 | $fc->AppendObject($g_error_container); 128 | 129 | $fc->AppendObject(GetMenu($fc)); 130 | 131 | if ($g_action === null) 132 | { 133 | $fc->AppendHeader("Select a zone to manage", 2); 134 | $fc->AppendObject(TabOverview::GetSelectForm($fc)); 135 | } else if ($g_action == "manage") 136 | { 137 | TabManage::ProcessDelete($fc); 138 | TabManage::GetContents($fc); 139 | } else if ($g_action == 'csv') 140 | { 141 | // Export the current zone as CSV (if user can actually read it) 142 | $table = GetRecordListTablePlainFormat($fc, $g_selected_domain); 143 | header('Content-Type: application/csv'); 144 | header('Content-Disposition: attachment; filename=' . $g_selected_domain . '.csv'); 145 | header('Pragma: no-cache'); 146 | print ($table->ToCSV(';', true)); 147 | exit(0); 148 | } else if ($g_action == 'new') 149 | { 150 | // Process previous inserting call (via submit) in case there was some 151 | TabEdit::Process($fc); 152 | $fc->AppendObject(TabEdit::GetInsertForm($fc)); 153 | $fc->AppendObject(TabEdit::GetHelp()); 154 | } else if ($g_action == 'edit') 155 | { 156 | // Process previous edit call (via submit) in case there was some 157 | TabEdit::Process($fc); 158 | $fc->AppendObject(TabEdit::GetEditForm($fc)); 159 | $fc->AppendObject(TabEdit::GetHelp()); 160 | } else if ($g_action == 'batch') 161 | { 162 | // Process any previous pending batch operation 163 | TabBatch::Process($fc); 164 | $fc->AppendObject(TabBatch::GetForm($fc)); 165 | } 166 | } 167 | 168 | // Bug workaround - the footer seems to take up some space 169 | $website->AppendHtml("


"); 170 | 171 | $website->AppendHtmlLine(""); 173 | 174 | $website->PrintHtml(); 175 | 176 | Debug('Generated in ' . psf_get_execution_time() . 's'); 177 | if ($g_debug) 178 | { 179 | psf_print_debug_as_html(); 180 | } 181 | 182 | // Close open FD's etc 183 | ResourceCleanup(); 184 | 185 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | 6 | body { 7 | margin-bottom: 60px; /* Margin bottom by footer height */ 8 | } 9 | 10 | .header img { 11 | float: left; 12 | width: 32px; 13 | } 14 | 15 | .header h1 { 16 | position: relative; 17 | left: 10px; 18 | } 19 | 20 | .footer { 21 | position: absolute; 22 | bottom: 0; 23 | width: 100%; 24 | height: 60px; /* Set the fixed height of the footer here */ 25 | line-height: 60px; /* Vertically center the text there */ 26 | background-color: #f5f5f5; 27 | } 28 | 29 | .login_info { 30 | position: absolute; 31 | top: 20px; 32 | right: 20px; 33 | } 34 | 35 | .export_csv { 36 | position: absolute; 37 | top: 120px; 38 | right: 20px; 39 | } 40 | 41 | .value { 42 | white-space:pre-wrap; 43 | } 44 | -------------------------------------------------------------------------------- /util/mktar: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #version=`git tag | tail -1` 4 | version=`git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | tail -1 | sed 's/ .*//' | sed 's/..........//'` 5 | 6 | if [ ! -f 'index.php' ]; then 7 | echo "You have to run this from root folder of project" 8 | exit 1 9 | fi 10 | 11 | if [ -f "/tmp/dnsphpadmin_$version.tar.gz" ];then 12 | echo "/tmp/dnsphpadmin_$version.tar.gz already exist" 13 | exit 1 14 | fi 15 | if [ -d "/tmp/dnsphpadmin_$version" ];then 16 | echo "/tmp/dnsphpadmin_$version already exists" 17 | exit 1 18 | fi 19 | cp -rp . "/tmp/dnsphpadmin_$version" || exit 1 20 | 21 | # Remove stuff we don't need to package 22 | rm -rf "/tmp/dnsphpadmin_$version/.git" 23 | rm -f "/tmp/dnsphpadmin_$version/.gitignore" 24 | rm -f "/tmp/dnsphpadmin_$version/.gitmodules" 25 | rm -rf "/tmp/dnsphpadmin_$version/util" 26 | rm -rf "/tmp/dnsphpadmin_$version/examples" 27 | rm -f "/tmp/dnsphpadmin_$version/.travis.yml" 28 | 29 | cd /tmp || exit 1 30 | tar -zcf "/tmp/dnsphpadmin_$version.tar.gz" "dnsphpadmin_$version" 31 | rm -rf "/tmp/dnsphpadmin_$version" 32 | echo "/tmp/dnsphpadmin_$version.tar.gz created" 33 | -------------------------------------------------------------------------------- /util/testdata/valid.zone2: -------------------------------------------------------------------------------- 1 | 2 | ; <<>> DiG 9.10.3-P4-Debian <<>> axfr bena.rocks @ns.insw.cz 3 | ;; global options: +cmd 4 | bena.rocks. 3600 IN SOA ns.bena.rocks. ops.bena.rocks. 90 1800 800 604800 43200 5 | bena.rocks. 3600 IN RRSIG AAAA 7 2 3600 20200516155619 20200416153140 6335 bena.rocks. X/DFezTZthpws8Kpgvmp4Y6k7e24daGD3279rGNFQELWBbEIm/CU6awr 07MtrF/HqkbHZmxxM1iKIM785dOogLmoingfCTcHhsnG4D0272ioBJQK 4T4C30Pn24v9SRYqoMslvW3LbRTmZXfcyfS13tgPDhSPyi9pjZoVEBBg Y9IGyljOKMwl696sPFj4UU5SfOwI47Asr0zv2G3ZQkSxmFRp/ig/I5dI fVXmorEdQa/yPMlMLLpCvU5JttkjnV2aoW+H54l1+s0nPEwAEJoeU95C 8xdHYZUJgpV4zFoXwiZyCqeWr0aQap2xRi6oD98QXcmjwVP4hD8kFXNs XyMJ4g== 6 | bena.rocks. 3600 IN RRSIG TXT 7 2 3600 20200516155619 20200416153140 6335 bena.rocks. gOuTMsc/oN/JzPXiV88Up4y/X5XyCQZXphhmEM1HuxeVQObZstF3j6GA gHAuOkKvrYztmTQxF6zTmPSQ7d8oF8zblUiSAZWsn5tLVK4QzHaZdYj6 w2nOYRJXHvshDh1Oukv00pI7XdfECRi1YVnQ3gxzMLEaLa7NSqDllT4w pr0xE3xyO1cuUquNtHq89cUEyCUsW07TRm0ZSMsFyVzny0sa1wxDdzFE lD+GqCpFElkx6lkR8Q9soYmcygd4tpxC6U4uGxk6CWcTpY3RyCImfDPM KevUYJ6aUb8eTuxO46dvTOm4TZuRPzX69Zqyu30rOY1KQT5YSvuvY7tp ia3mLA== 7 | bena.rocks. 3600 IN RRSIG MX 7 2 3600 20200516155619 20200416153140 6335 bena.rocks. QXT2qMYfVWvsgq8gPJOT0sggg1yYPQZo9iFYG2E/80P2/Xu9DHvIhLqA 8fGgsgUTOap0MllEmNEqQX7Do/4acT9xpppzcP9p8TVUtyH/fc+vzck5 S8xPDVH/+g1kpzKva81AuElgWYTzwXxQTvHo4KSXWZzeBpbsgidj/qSG 6QSLTn4XIgBNDTHoCyq+wZTxZLN75IJ+RotUGouV/6/S3BO1S3jd3O/a ItVlmCPKZ/O0N1sxorrnPzIalAS0fKBvLqlkeqP5DFJkAVYH9I8XH8dn 2DWb/3qR8p3x688eUj7xyTyb8dTkj6U8Kc11dzqIR2MXbBc46s/Utv84 SeN93g== 8 | bena.rocks. 3600 IN RRSIG A 7 2 3600 20200516155619 20200416153140 6335 bena.rocks. I84r5/2sJMYQ7DDXS8DyYlGQ60dFSnVWHYu5jR3mJAnzSgpHhHmLEHQt tEWZvjK8fxv1OECg23gtBzuXjWXCGGrv8kE4WdE6LpedK3zkg+6hc/IN TBar1ENiZwyKYzaWgl0Y9P12f17vGtBm8XdqizlqNddARBglvCrrd1PC x9qbWTKwLAdvI8mJiIQzCs3Z12uHS/qb/Lu8Bo9FS9JufFuqCgZkEsWw Xd35j7BSP/ENFsPUd5s57y7AKtOwkPcl6hiDF11OTTN56YP6j9u9B5u3 EOOyjUXcTdxYhL6MDXO4ttdr7HJjmk3UEQbvIZNYUDpgwxT+AC/Lyv6m 3H9T9Q== 9 | bena.rocks. 3600 IN RRSIG NS 7 2 3600 20200516155619 20200416153140 6335 bena.rocks. NLWLffMv5GTC0wQoNdeYjJYST0DTv14XLJehS5mN6kZHEL34lTn/VYZD XKnoGidU2YulnDToGi9syKLSibGuN09rFD+u5ph+Baa5nEJzXDQ27xat ya3cP3PMCkvoAO4OxcCGjE4HsovS9CrTfG7SdERfB5IgZlwneoRjJORQ 56ZzIJ0zqtJcFArFvk8JtjzhE0ZdXgyiPt+NAw+aiYXlenYQ7Cslw6rf qn2lDsMluW5C7PklhB3i7bc1ezJNwel+fEEkORW9Jtoats4FdY/+EolV 7xlA2XLhTJKljLVCnCVmmmS1qhuknHWWD+B+2N8mlPiQHSmzXyy8Ykuq tD03Ag== 10 | bena.rocks. 43200 IN RRSIG NSEC 7 2 43200 20200516155619 20200416153140 6335 bena.rocks. lcFVdKaNaaCor6OfLCGnpDRpOJbh3J0CQqnjFD/nNFnxc1eYggOSYtCP Pd1bSgIv7ATPQM0evQQhx13e6yw6pz1XPD38D2f4/Iy+F4+CDcy0c6CU uMh70kYkrDyGTMATsWUgzjgg3ybbHGwVfrbRVkey5qY9rB8nmfLI7cpk p16wAOBR85Hm08tVlwKedojCc2JnppaFr3rSMzDqPprV/Lxoob2XspRj K/C0BF0zq7Bu/P2bLLxq7L5T+x0VWMYWTBZg7DeFSUFaE/MInu+MnbrM EYj5obQSfitrkXZZCpQu3KH65dvyMnRwdb+7d4ft952JT9TKaPT4DYWw GQsqww== 11 | bena.rocks. 43200 IN NSEC _dmarc.bena.rocks. A NS SOA MX TXT AAAA RRSIG NSEC DNSKEY TYPE65534 12 | bena.rocks. 3600 IN RRSIG SOA 7 2 3600 20200516174320 20200416164320 6335 bena.rocks. RXGotOFtjjUFJUb394ny6sgJm2meCNHsiYdNRDedIyBd4LCAQXfX1pVG Qn2pUdeUh3bEIzpHucXIo5A26fL/jBEZFRT5ZI2NyXgdeUJBHz4QUDMP uWZcZL7YfdwCDzwZycxjDhWHIaLlLmvkV4vm3Tkun/bjIIfKz9hTBPXC 6HDWWwjmILN6MqWrgGKL+zV60bB3iaVz8mQn7WoY0hTQE5mV8gFCm0HI hD4cO+AwKyblJUDdrOoEQFqtpEASXJqJcNg5uuffndu5sisbeKQDJiBi YjQN9M++veF1Hn5dL4P4Mjo2H1YokpMZmpubxzsiTAj8GygNZy/DALus FDr8gQ== 13 | bena.rocks. 0 IN RRSIG TYPE65534 7 2 0 20200516171732 20200416162339 6335 bena.rocks. IW4KdpvZgxboWXpcsACIHktwd3sONMrzcaEoLPj8MxtOxxy06BKcfYLA pWT5nCp0tB/N3S/m4iN0g2LHoHGa7/ig+rTzx6QM4NLJBZwY+LYenlvO BPayKxA/iHEHD5WP1TZENhcONeQGgNfFql/LGo8Wh+9vAM8YRtU8sADD k2JKt2qLkEusMiuA/1To6zjmirQy0Vcm8cCknNQGfPE4y+p4n4SeAJ0x ZkqHUosNEGU8BrU9F3g0wSqmJMjTzstFmlTSNtsDO/zSkfZ2Dli3Njz3 yC0dDI4WuHOj+a473ozFrGL0pVANnjKQBLBxEyExm50uZs/6cLt/LH7G jjN7fA== 14 | bena.rocks. 3600 IN RRSIG DNSKEY 7 2 3600 20200516164947 20200416164320 6335 bena.rocks. ViQlXWB2Re1wHjgduBjEVf0T2qR3F+OisENg83Jbk3UG4YvH9O2cmbx6 de/KZyKc7JyaKth8q14/h0bldYrXsKYleWyZOOdDGYs259Jh4ci2cFBF j1FpfrqdAyF47QX3wF+9ZntNzT/qPrXsRmW5a8WcZ1Od5i/t7A9r1mFf c1CAG/+YN8xoXk8sa5Jn7eY9yseGW9shlrO2iE8osdgYlQkQhTTYRdBU QSJ11PeCRbWYM09nD3uXseMkaNySMfxs7VJaa78OS8D31YpYKFxCanmK qzC5KXjgSk1aGmBVG0qyQvjP4OydE4zfofd4ikMw8GelD12ibK5Fh93o p0Hs8Q== 15 | bena.rocks. 3600 IN RRSIG DNSKEY 7 2 3600 20200516164947 20200416164320 8151 bena.rocks. EDh4Gcy9N4kVcx71T58PFis+kXRHz5SJtXNq7MPRRISRH57sjVifbYcf 0I7ayFDxpnv8eWk215hiW61bdMLJF5sNCn91Lqakq3yVngPde3t9uCLd UBuu9hr1jQZfz5af2IM5Vwki1+ewQMoB6qiAzNlQVCmv+crfBcuv5sws r0jTYCjZpH7676P1KcWy/6a3re5lUOWYj04NEcusOt12x7o1qpAMNXpA 2Gmj8vmd2AOXyLstw76emXpXzxrIFqjqzYlFF877iAqLZU2IFvm9KowH ZpU9XwljOe9oPMdqbYRIEL9a4xjo7fLgfJrfbw++ySgJevP9oFs6wOQt yi1ir8H2JcNeBKBmLY08L1li+F3tGtrj09xWf+siELuk3nzzbGngrLl9 /R6ww46pZWV7z7DGRKpBkjWSQ6H83+K9ouKwRThCW1LdHt5XWEuj7qrA OOIUdO6MColvMKtQnUVKpFTXlqMysxdGrbUKC7jYX4MO5/L1a5greadd Kl0UpL0Y3ZwJnBTRfF53/qCfE+S469NP4EStrVcFbhKbFlAsFoqwMmK0 peuhqknJUXBLYVkG1SfRRWBpTtdqEg4svvJAgS8C/mfDuK1OpddqDkcB +Z2Ypfi2+BiDCXolH9ilLSaNQlEIjA0z/B8DoZY+zNVHr73hHgoaRVgS fKE3ILFb5Gk= 16 | bena.rocks. 0 IN TYPE65534 \# 5 0718BF0001 17 | bena.rocks. 0 IN TYPE65534 \# 5 071FD70001 18 | bena.rocks. 3600 IN DNSKEY 256 3 7 AwEAAbTW1jHFZK5Ovc7j139qgarE7yAu8LP1L7Nf8Ec6JlKVGbEMqR9t sB7/2KgyR/d6q46y7wGFjPg87vu6w88SaLKSsHxFPijqzqE3M9IBuuCq 088dMUWTXHbli5QyWHkWMDVe/lwzxdX7z5FsFDmmR8p0jF+mv8n9Nl87 16CyQxoZR/KHZvu9fLxhWRTw+Dpan2u0dYqVpqEkY3KUpv5Whp9VqZs3 pUYk+ia2FGWMl1Nri4o7bHp9lrI22GxqLq92at29lJMPvIEv3twe+uDK qmWXfygeAWwFYz6DRXXvYV2CKL6DGJIeVyVzwBVjokIVQBpAgqVcg2w+ 0qSt/n7PQjc= 19 | bena.rocks. 3600 IN DNSKEY 257 3 7 AwEAAcV5zegEEufr5KyNU/RhrCck8wwI5NJ0rOZWkhW8Kqt+iy4Bu6SO JZi3Md6kQgPzy/TxxfAFA5ZNqk/NWE9TLvifvnsKq1wbFfDH6ds6odSQ 3A7/lWbf0nG21O/nwC6wjYLn6TKmx4lkqwJu8WikV71sYZYaxzOpKfEF /T20/hcvy7mKRSTh7LgWzakvN8Yao+KQc6Dq2YDEtSTVskbGXXiJn4tr RTWK43pkGPYt1tTWwtk/UkN3EyYSMvgoNxrEZS+55Ute1B4Xlgtcweod d1Jd2JRdINSEeq9KMZj5Sa+Lv055wr5lyGKBtFZOTP/J2DGzt/mY8Oz+ NN92Gh3pim3WaW4FTxutCzMVND4SYp0S7/tuaVjPf9iwd4iW+u6ao4du zcn3Cw+TBhslRlKxLi/FDJs7YO+q6NBgJax2KNe8/y3Rq47E7ZisQklW YAg1VfYlNAAtitKk+HXCfWVbxnCUcvZ9PcjTyp9S+aJSugY9YSVJI2jk oMYa+naS0p/+5tvOOvLgZog1WWxGXpMND6aixxyp+e/vOY3Eg7+s8ZFh M4PXmtfbKjE6dvItVuPVzeAWgmGTnfsXJ5wuvGrwvq5vGJ74dix6/6Gz aqnFi74GECxnjElRvLG8ijuiHfTkhX2c8o6AOR9Iiq1LOg0dHlfE03NF yRSxZXHakTUJTEbd 20 | bena.rocks. 3600 IN NS ns.bena.rocks. 21 | bena.rocks. 3600 IN NS ns2.bena.rocks. 22 | bena.rocks. 3600 IN NS ns3.bena.rocks. 23 | bena.rocks. 3600 IN A 83.167.247.8 24 | bena.rocks. 3600 IN MX 1 mail.bena.rocks. 25 | bena.rocks. 3600 IN TXT "v=spf1 mx a ip6:2A01:430:224::81 ip4:83.167.247.81/32 a:mail.insw.cz a:insw.cz a:mail.bena.rocks a:bena.rocks ~all" 26 | bena.rocks. 3600 IN AAAA 2a01:430:224::8 27 | _dmarc.bena.rocks. 3600 IN RRSIG TXT 7 3 3600 20200516155619 20200416153140 6335 bena.rocks. AVrfUTAaNWTtnuDSWJz+oMOAtmZfUiQ6qrF2K28dPEO4ww9azYjsFdOy mxYwyBU9fNay7jFLmDz92voWqutYHktyV/ywDpChP8F89DS4+dwy5I5A dTBknwfKK0WWFtIkuc/g8vjiaQg/Bcun/d5xuVvR586YztnBG8HyavsU iwTcomBoceNqn6XOcHljDaJYG9dAFW4HMLde4Uqc1ZPD6CIlWKyl9YoH jf9qZ4jn7/2U4CBO/M1RElWGUpGwlRNFgt/qUW51rPrY8NTVVCJU01gl 3KjDEmGfwLMilSPRErTX/N4kaaddCOknkLtxnGqrh24WKKf5NyTpIO38 VK9MNA== 28 | _dmarc.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516155619 20200416153140 6335 bena.rocks. Da2kiZUeyYulhTihUsOqI7B8vr1rIVGv5ncAfE+OiA8f3QG2dd1PH1kE bJIXOxUDC4/sMArsdMx7KzR6cVVlecpOPrpDmy7mQITsp4CiwcAZoYk9 2GHsDW1AJFQ3bIMTcnV3Lu2c/jyolDPlz2zEdai2glxo1r4OHlNXXdE+ hx0f2f3yIolNsE4nLJ4nJ2CIeOtKWf0DlhdmsEqggO4VgnpNr72E7FnH uf4T9PKI6xcuRugyoxJNVIeQSIoGRfXuIdybauJ4zyekKYxO4FDQElkq 9zHa60ox+E037wpgl/Yi9MtFVAQYkmRIq0TREsrUvKche3/jwEAPih6d SuN08g== 29 | _dmarc.bena.rocks. 43200 IN NSEC mail._domainkey.bena.rocks. TXT RRSIG NSEC 30 | _dmarc.bena.rocks. 3600 IN TXT "v=DMARC1; p=none; adkim=r; aspf=s" 31 | mail._domainkey.bena.rocks. 3600 IN RRSIG TXT 7 4 3600 20200516163347 20200416153915 6335 bena.rocks. dxLPV7vpgFFEA3TkQtpKxpF/4qwXI5ila8xv2HrMUsM0WNACX6pzWsou M5EqEd3Yx8lGOQwnl1AvnBPS8a2JfZCr0eJtShWqbrge35KkV/13TUGU O2dwkwFM8/YZrYbeUAarAPYQiJPlYpqDVZ80li3mCQn0DiCThFynUQ8E FZ7QNQvq6AR+fHsB/pEdiKbvagQOePSiIFfqqQcnunqpWa+x7FZJuvzR VNnjjQOkHQncYEKM3AWcgVJklJ01VPg1SZfSp0QPSQyc+NM46CFxzq/a PANJGNyVciPUOYjL1WTkKifNYbN0LzcklKhZyDyr/SXoBdaP89PjGmki X5WJrw== 32 | mail._domainkey.bena.rocks. 43200 IN RRSIG NSEC 7 4 43200 20200516163347 20200416153915 6335 bena.rocks. DBfYLB+MgN6TU/GwaK26CN5Gd1Le/dsz0NKfY/a8jByCPpdberNHNtsP ahLHlbbz0iKWuHyb0MDMU4v/+G0EuqwMA5fkrFBKZV2hb3txuamY+KTD tp1FLa+4WZZmHhLq4hNFy9qU6xlpMYp3Mdks89mtRo7QS9chcqsJeuR+ DYh655ragTPHgl0VuaXHZ1ydpPE5FC+asxvab7XvPylorSQguIHchLir 6Rk2KCpXLGvK7ZijVpflIzR+EGSGmdRgKhBvm/HZhOCV3aTdeK/W6ndQ Dp3dmTFrsCVMmpUi8s0bdreg6dNfXoidoGO5YE0a6G4nWhcRBpGCSN+A G3AReA== 33 | mail._domainkey.bena.rocks. 43200 IN NSEC _xmpp-server._tcp.bena.rocks. TXT RRSIG NSEC 34 | mail._domainkey.bena.rocks. 3600 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhRvURShStu/bAB/otkEw+TA2cQ3Trn/a2Vg9ZBtnJ6rlws5QjyugzElwtSpCD8Wn20rykgl+2Akw7/Wgr1lXIDAKdaZKf4eVawlmHSLEVw5IrWR9/AdMCVKTFOImpKC44lfJkxZxJPmOwa9Aat0IFBqbbKK/ZPx5Ny2071ScShwIDAQAB" 35 | _xmpp-server._tcp.bena.rocks. 3600 IN RRSIG SRV 7 4 3600 20200516163347 20200416153915 6335 bena.rocks. LXS+KUv4hw5KqCK08PkkVF0SnucPkz220zB9NM/0eUuZcgIKNt5FMWMp jBZ4eiS0lTeUg5wU+JqaKv2KppBj/1+8eSXDCctaYPcHWpVTKHAh+VY8 ixpj1B7Otg7Hu7NeqUwSVOUr8IfnHNIKvTgETZwyZZGn63tqDHdGm8R7 tW0pt6ddku9r+ku+rvk3LJLxMjJdtiM/3fE5BiCDJSL+4R/FgyGB1HBn u5rO8evPuy4IrSxB1m53U6ni3zia1q5fcrrmccaI53HLVc2X165iYKaa Wgadzuq4UrXeDdmBwZ4EmLXALcerXHFABaaNqvbr65RDuRC0wdXfreOz kcSAeA== 36 | _xmpp-server._tcp.bena.rocks. 43200 IN RRSIG NSEC 7 4 43200 20200516163347 20200416153915 6335 bena.rocks. lwCgC+QqtkuAp38ttfIy5isQZwecrrv3tmvsi9JWJX2LX/LSKfSbqTmO FFjT2owwWA20lzORWlXyTzdnJyw5JmkVz8nEJDQxWWP+U8sazvdCEBWK s/3NZMAOg9Q7CsZn/+QaJ2HTITqR/OwZQOwV2y3erR08GqoJNNbHnsmX tV9O4rvwzGqyg00pAcfBvRmDxqIldPLdGo/1NmPimGV38D6W+FXS9Nkf KNH84Ui7RKfvZPLA5kfbygyTzU+PHsRhNYvc03/UTotYnIBetiIT7uEW JYzWDVOsWnTmO1JCbqFIPWCUpcT25HNzhweE6op+n8R7I2FKBUBxXeJf fKmkmA== 37 | _xmpp-server._tcp.bena.rocks. 43200 IN NSEC cloud.bena.rocks. SRV RRSIG NSEC 38 | _xmpp-server._tcp.bena.rocks. 3600 IN SRV 0 5 5269 jabber.bena.rocks. 39 | cloud.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516163347 20200416153915 6335 bena.rocks. dG8wHnldayEKHmNXqD0p7gb0Lv0lngCT0NXOKWk5LFzP7Ikc/9Vc+dMv AEHYMiR0bU9UvmBk75L59bajixOTIVuXmpcRoMyrpptZc58ctsUHNbQZ WgLa9PJh0XJ5eFeNo3aVORvGYrOE9b3gw8WFSleSdYf8AOWTyV1BXoHJ F0Xf7q2jE/kvWmnfvwRliI79Ybwib0sg3dkv915EpdFZo4shAtBdLf6E xOQWXlwrOsnXuLcF6Vikk9WwvmTREYLParLwdEQbGUbZ2llgqO094sAj cuMfNtZ6bLpf3ExV2AU0NWZmZtTE2BO8spX5R2ar2pAZf4Y0HitcQ2Ak nLSWRA== 40 | cloud.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516163347 20200416153915 6335 bena.rocks. XO6HzXTKdB5g6L5SLkaXWGIcB2UYq9FZK4o/IYcMhs8sMC+vG5GRREcL tMrYC/grWeBdrYxYtJYEx0BZyqlzjSsm36sPSL/BApGW5ll+cm4VAixR /XCl+ciyp0MDdQgPxm5bvSEWU1ZEm2uuHEC4NIor/qTyuHHeP20Ot9Tc yx2SaPqTvPKgxfjCCdHJysnkGdC5nQduQssoncQBguEGl5VScpgrwWXf me8jzqlOZ9HccaWU0i08bkUKvkKlFYE842wVkerAcO+5Fe9R9DCe8vQg 20JsDy9iLaOyNGEVmg5ioFllaOMu0za8KyIi9ktIhc28yP2eDeXX+fQv +YXfaQ== 41 | cloud.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516163347 20200416153915 6335 bena.rocks. dZCC95S1e06M9+ayc3MLNpE6OOb9G3QSj+ChyChQHTpgowMEVcJ2082Y qJy4v3CLlnhW17qxjC88QKfahg9ylzrYRreEFVvwWmtXzvURzcDUr/5p I2LMpqcXm8htBdw6wBN+H89EaKIPzxjB5ltQMorVT38dUiWecwC2iy1G D3uj7LhyxJRNgj1+uXDSzVwxCSj2iPe3SmuXDC2jj46aDcNYz7zuW/0E WMgIDXsK8RGxavGtwndlVPm7yUT08VH/+kKkogzZ+Luc1QUGw909zyFP R0md/m3LJ9woMtSgp502huAFLrMvR2Hjp/FN8OTE+m8A5aSsElhUjAtm rqWqmQ== 42 | cloud.bena.rocks. 43200 IN NSEC fotbalek.bena.rocks. A AAAA RRSIG NSEC 43 | cloud.bena.rocks. 3600 IN A 83.167.247.180 44 | cloud.bena.rocks. 3600 IN AAAA 2a01:430:224::180 45 | fotbalek.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516163347 20200416153915 6335 bena.rocks. ZCOCYIaDP53w2q+dMVKwGRDTcp/y/7HcTOhdd6RkOuaWt0hfbzPQH5E/ TW/lQ3lkMGrIJbUiHdVtP3fbr8lsAYaUTcLw1odE6maVh80pz5ZgRPji iWGpwslarJ2yIc1fdwSyHeeJbI9uVWQ3I4pjqaniB4SKtCgmM0DVJOov nXpP5jZ6acoMgaEgdIMFWeFAXhd3mbOpcIu8YN92w6NVWmsknzu4hN6v tmgYzaxTapn5P/SZDV/uib9wS7WBiUaN8xott9ktTQ3k8dbNOkU5SX/J Q2yM6BaSkv0MJ/+66LJUy+Lsx2wfYF3enzRJPVplCVyZ/w4w8XhMCj8t EVcDNQ== 46 | fotbalek.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516163347 20200416153915 6335 bena.rocks. gpabg1Ugf8rHEoEWi+ge7fe7GNQ5Oix0eo5l3xoKW+/PYZwxvlIyzDwv VZdRRWAjd01iafqMkn09nav6fW9iFJoIfkUlrkmnBzS+yw4GOlklhlVk jY6uisemZOScqeUdYVv1z5tXD2hKMYhiQWsPjXGJOi0yf1VAlknqlVlt sF+q6+bE6fkXp4mi+gxjxjpS0PMZP/fJN2Vbzhe2A1wVL596tB/nrkyL Mz2OoJviBE81EG5plAXedtUw6oj8+/BF86k9bXFK2vhYskLx5ReK9Acn swB+ufHs1rt6gEWsp+zfrdpONBl4i4q6Rio35nl2kMIgi1ZK66t/xWKm sdCIdw== 47 | fotbalek.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516163347 20200416153915 6335 bena.rocks. btfe8enHIHKAsOVMTowPYpYCEeZpcy7uEI2gkY//9//zwwPwnHmvCzD7 91yAX4/eT/bE25WZ8RSrYDJoLiHTAJCrqfpF49+dOu0kZkdiYhn54pvV Z7KhGsElNe3lh+F8mV8IoHPCQuC48DDG/TZmN+YequPKDQn7Nlq12bmY RXUyEZ/aAzOEpC9SRkRaGtU7Ey0O6+P5eZCQbemQ7lQl65sG0EnDgH5/ mBLfUQKPyh9tzN6boP8DYtQVy3HrwKFQQMZe9jDxWeMpmsjTvZW0AUCq /5rIVwn7R9XXKiLsaMs7s2CharzWMHn/sgwWGtxUa/LTsenmG+bviwol 7Xyazw== 48 | fotbalek.bena.rocks. 43200 IN NSEC ft.bena.rocks. A AAAA RRSIG NSEC 49 | fotbalek.bena.rocks. 3600 IN A 83.167.247.81 50 | fotbalek.bena.rocks. 3600 IN AAAA 2a01:430:224::81 51 | ft.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516153019 20200416150616 6335 bena.rocks. VPY8BovBT34pASQ3qP6AhE6eL15CHBfg/VvID/slWjTbxT73E6ThLxKi EBCCYhwdHQy5BRaU54hQGVqRuCjKW96xcbz4zAUNs2DBMQSn2uVdIL6c witj+1o/sauZCPdMaTcFk9FwnRoUKXMkel4XLRxGcXrpWL5B16YuMP6I guCqh6Y/hjPr5gAptKS9aawdRBb44KJbRUUmE/6yiGPCutBIhz7InvpU ygO2GlI4wVftBohtnGaqRz1OCuOA1B0Dlze4Dlp7YFtuYdF3AXTv5Ua5 5LQIlOZGQ/NC5Bfo8SedIQNrS5u653d5iql0IW5uJr4SwmmWWhPNoHUQ RODbtQ== 52 | ft.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. RpiscnCpmwz/rd51Om1QfChYg63nc4KcU4lTZUOCPR4ZkiheGMe2yfGb 1RK+UV41H3nalSClrvIyl2DSJc4DAyWuqQkdxWp+kFX0/sYAUOBywIMG mxKioF6/sITTrJRQsBYzKv763uNEI5q+2DAU5tAqU2G2x6MFcM3glJGT KKEo++veBdhf66n/2ES0wwPYSo2SgxdrkLCCFx4e2kV/zemT2G3jw3MM mdKRMswtWFk+mmemQs0dptLwc2PRc4h6pIr2OF6AiPE3gqogRKShPu6/ mpCZLNgj6ry8pdeCZFLOLeUpLx4jZ7O+WWhpIDwKeVpNzfuMjogrcgxn iEh6lA== 53 | ft.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516162744 20200416155500 6335 bena.rocks. XdLOCjERP4m7i6GTU3cQY4xJeUhoDn+AKXdwcwpT4/uns61kWiXryZNq zucn/1myhJ2rmqcm/4PbwDmzd87aST6jtffOq49xqoNu1cXwhJcWNn2T bK6wDewiHDs2yaZy0A7SjtqRVLmVJUHZl+ktcWXs8zGS3JTjRq9mn9jv ql8i5vWrVRVUuLJQZzRiHG1s5fqJeLYXLW0lf1etaAXey58OqrnhHwl6 H9XPX2VqfCBE2P3Ifk/466onscc2hQj83C/ycsZVzxmRTaNazGU7DAca FLfTbTbXdzNwrggQCB7UkVq4ytrOTgQqFpAxTKmEi/RLrur/8DNpuhW3 nre81w== 54 | ft.bena.rocks. 43200 IN NSEC jabber.bena.rocks. A AAAA RRSIG NSEC 55 | ft.bena.rocks. 3600 IN A 83.167.247.81 56 | ft.bena.rocks. 3600 IN AAAA 2a01:430:224::81 57 | jabber.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. Und1r2rexmviA843SqoZBX10LuF6D1zYMoDyu/q9aZlBDN9U4TASu9qI nGghc+fUkgLS8ibsPPVF8876/QYqUHepjV3dwgz5jOnPKm4FMBBjHhqb +ZgjZrBsjmiwQOFwq7k0tAEFnvmM3FJM+PLUh9UG1Hork6fPa8KbPc0C DND9YyuO3n7ujH3l5Z5NzP4xfd/kt6uZZFxGVzaHkpF9qHwpn8lQymQC sXuzhwa04jadnTpUVfm7eZ2fI0KKSOz2n3HxByazCaeQL1BIKTholyai lpYetIoCplX17mIb7tWWEPRRzdAl0y882E1yRRkHz6RjBfBCrXb4zAQB t5T8KA== 58 | jabber.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. jmYUsybwkIMYlvFzHFVVBLmsCeZQ7aDuLu3pyFy0tTQ3+hqCj0uy9wLp Z9HtiKlqSAwV02tzJie/dWDMmuUWkd2YiTrqH9bNyeGs3/o8AEOtwZqu AwN/7ETs4Tweif0utjvSfbns5yvL+UjczQNmt0UBXdrHpzu17Z9x7VV3 g/VS8Q1l2fK474zhGaYQ6H0B0MuXAbPvyxC5evZ4XWMRjiKeiBCF7j6z xvAjdpNR2fGuCQj+AluKJcUci33lvgQfhdylL7sjGKvn9uCyZNypfjxH 6WfLN7BQp06EUyLeKvdCOQWu1WATG/pdsWjtPLYM+021+9Mp+a0AVwXq oDb73w== 59 | jabber.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516162744 20200416155500 6335 bena.rocks. EUHix84syK07+F7htsQ9dTuAwtX0cMD8111IoODWLvE+Q91IE8q09DbH zjIXQIVL83TOnD0OqyCBNBXV0+18qxtqNyEwCl2hRibnK/vC65ZDh9D6 /Oi2GQI+U6VUYiq4aLCfoj3CHqgRLlCwrqrDiM4wFlXtkQbJ69tVuNy9 DV5jEFVrg4mUUziktCt2uIIBh7QsE35ESq+k+Jixi3iQwU5AD5dz/6jZ zrmq7ekoQlCWQ205jpIq+7z3H9WtrfIgytaxsmswo4RZipgw+5ibunSD iwNMs7Bmpv4oACMAy61ucOC543MsaZn5/wyDmX0WJzw2pP0ffTSWCEHB 8tXVhg== 60 | jabber.bena.rocks. 43200 IN NSEC mail.bena.rocks. A AAAA RRSIG NSEC 61 | jabber.bena.rocks. 3600 IN A 83.167.247.81 62 | jabber.bena.rocks. 3600 IN AAAA 2a01:430:224::81 63 | mail.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. BtFiWsMaiwDg9sK0cCK/NT4Tf9nUNiCbryrL7hUE1Qf4U/9aW8eX4ORB TxfVOJDdLzQJV+KsL7EsJi1f+D4fBA/7HQI/id1LY8f/sCMX/QaUa4+e zur6uJ0wK7rsCpdBTIzVOJB/y1u7K77wOamFxLlmINWwHtbp3/TTnlKL sU4F+wcAwEtwZ6wK/jjZfXeCTXGGo1zu+v3vYmGbsAwfNMeiXi15qXxy wSE+WMzkwBlXkxCT0h3D2FpD7wDYCyqElmbhGY3I6z68eMEQU3MSK66Z 2s+K3s2DO37HJpjEAUokUeobg+apDRoBcssu+PoMG4r1pyfx1Bw840wk 53BMnA== 64 | mail.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. iMqUAYaTntNYhlUWlKbrAgECqo5QgMYHneSzVuF6qBrB/OE2qYAdfLRV E5Zsm/f2ykm7g+mCekSqG9sy0oFjdPltgzVgdwIF0gnGsnOmK6L3lXWt TMPdwZgOZvqgMFj43OCGJ0AueAlWOIeW+zxDjstd20PntzmdM/f20ZOX Bw/s5iwS0Fv0X3zrHW+dLGPPZ0dnnG9otpq6j/JJ27QDDjfa9gOLYknZ BOW5yCouVVUJL0JTVUoHjZdGEKQ7rrDDM3IHGkzxfoBomIWrwctYm98S WZHsz6CyQCtCOoYiIO24MxszbUfCqBhVv0VwFWIeGSeEZn+cGJu4j4Av xacsyw== 65 | mail.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516162744 20200416155500 6335 bena.rocks. bI5fXhWztfqDw4QbKZexYzF42qUjklq81srumtrAa8L07XH0ha5Vx4dm GGNtfqqLhA4LLuamacivCVHvT6XsoAcGw7v5GXfxWTIUsLMN59lJvSJ1 cHUmPCDVmLJ24K/zv77xTOnMa1FzyescNVxYn3ymW4r03gdBgGMsyWoa OnGYh0QyktO608mr51q54T2xWE+OFlkpVO/pNJc82vajItZuy8wkaD9l 8yGT0eW3V7ZdEzPnvhe/ynAJAI0CFK8n+0fJywS9KeaJRJgLajH966/b 2eJc4A/IjmoUsq3RZ9qpTBSsSvE1FSWobwamLYxmImoZDH/Fia0WXRnx 8/qLmA== 66 | mail.bena.rocks. 43200 IN NSEC ns.bena.rocks. A AAAA RRSIG NSEC 67 | mail.bena.rocks. 3600 IN A 83.167.247.81 68 | mail.bena.rocks. 3600 IN AAAA 2a01:430:224::81 69 | ns.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. gfV+jH6L5gcalibtWwVtwb16kVxEFcfktIvyuWENNuAH3TAtGEBRSIVw WndXCLUlLQElO1RMSDNl1YFR4JuVjrr+0xzBHQbfKdSZqWAOEGzf9kH0 2BbtcAL3WhAHAbhhJtnOj8CKTvK2MwBDaFtw8S3Nuc1Q1E2InP3aQhMO 7YmKu2Z3+I2uGdg/JqHdlE9dDoUISiZJzoyopDkalj+cDFJrQD2Gz+W8 yXRo0JB6l009plthH7MoxcZBeXCZ3EGDYP25OAjJYxxnw9imLaRc6kU/ b2lBtU3mYiXCwmLCik5LwIktUNkONRPqhEuU+OPdAsXxHStwK/TnfQLZ AEqm2Q== 70 | ns.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516162744 20200416155500 6335 bena.rocks. acdPdIM1DyaUAx7swk+I6Ed24J0cHNuXm+TOJgAiuaynArwT1GNjTlRe lBvPIJl6UqP8XYmbrmkhUcrD9h3+XsBwq8rW0FeqVRnK5jYnYPhrZyyi WqmYSw3SM0H1oyWkRjQkCpFgAfh3Jr6NssljROhrxs7XAPeLi581mJ1R +KI7BiNslEnz5MMw03vhaQ0lP2mi8hVFtiR0q2A6MzHHYEEL5wFdazwd HYZ2d0L9dmYfBfScVSxqwO9zS6dDAMVZwd4jXZkNtbmZVOFyzgn5725k sf7/NYLKLWXdjSBqC/6PYdWGC/ShXjDXWoW6xv+36hINQo1vTRF/YQ+4 k6iEzw== 71 | ns.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516162744 20200416155500 6335 bena.rocks. cIR8Da+5keOWNKw60HzvNdmA4M5dJx64LPboNtGhu0s8GVgVrn3xNk0o aToDbF2K0mzJS8JZBM7Bts7Q9r7mfMN6e+r6EChymrl5KzvicufF+Eye o0hJZj3fCvCckHytc9Wa0vlTjSPZDJInIo8MeuB4tMQJY2zCgGx9RCNy FvHwMvdvC2BpV37vMI4bitWiIviArJep62WV/be7TtvbpWw0tRx/FYoy V2WufyowGOKx9/RDsOIJsxYpB36eFpjgwaPT/IrchAirdUNaGeX21nTS KMstMhr5IC+2yj0BxyyNX9Lc+RFAIEPxU0XYN9yF1XldULV2+2Pvpai8 Q9XaQg== 72 | ns.bena.rocks. 43200 IN NSEC ns2.bena.rocks. A AAAA RRSIG NSEC 73 | ns.bena.rocks. 3600 IN A 83.167.247.29 74 | ns.bena.rocks. 3600 IN AAAA 2a01:430:224::2 75 | ns2.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516151438 20200416144113 6335 bena.rocks. ma9W1DMZBDOUrTrfCcbAUac8K7VlGbF0WCOazIkjFw20tkCqwbdgaT6z AkTkeyTwLNyYKiyVBCogI6L2E2KH9VYL5L5mcGVbDSJGtAwJ7rx2/vq/ e2kaU+mQ6t5HRczkn2GzXqTYHAlMOZzTyTdBLNL8tU6DArm36doDNnX+ juUrkcbSedjXbUaZ//HgmAjQaYynRNvXdUXRJ6R/YHObahQeqVX2pVuq YTzumDLhRO68KyDeLv0tbP4YzNbEr96FbSbgImhw+E4tZpZeDoATX1my ZWo5WSKwdB56EBegypw2wUpW6XMB7jqz8U95vO0SFqE53LhoadjwmYNb FBRTqA== 76 | ns2.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. bzI5Ji5ez7P/QezGe74QMu4x9lV5QhRg2OZKZ/+V2dYOUymNpHI/ksVt x3TXo6NSDihCJUAQSi3hrGu1xgsa7x7wqY8uX6Ys6DTzcXvwx/t5UeWn wKJacXyHwdQUTWo9ND2t4dyVDrfe9ByBBHoRNQVZyNEUhKbLRCjT5vl0 9ZZuRg9cpP8aLHuBkprWjSShpnSTrgpRdEEco+aEZ6cUrhbb0zBttfkK F/E2djL4LXGUUSGJK3WjFxMH6GL4hZpAHkmcKfLn1PFHrETiyuiXDDVe UWk5Kfd4nktvmyVDyn0xwtjucjnSMy+atnEF9Y7nwhdZqZ2BAVsgyxkm tfEFYA== 77 | ns2.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516155611 20200416153225 6335 bena.rocks. Gj8U7QScxWb0ZdnSW7JnDv2xqtuEqbWc4g2PUOaswUkPjYYRMJebjkyi IXkQnXaShhXY8bkimOjotFYe/iSGWclbeAMumu08yXhDstPhDQ5nIE7c xXzvBvPHp5zRJyyHtubvNfhoHvW+ZsSdQYnHYbnBgfwPMrPsa2cCO9xk O1rOz8rNtCP5JxT526H2MVfr6t01kNqTO0yV6P8HXJ9iphJFrCemXoMt FA7Hde3wJAgXkMCQGOc6maeHEYd7uQP8wEaEevM2ZNP1NNXNDKNFb5ka 7xdkH5le1VwT3cP7Xh0srNas+YcX+YD99TBnKLgjjnyS9VpK58k/d1Ov rW7LKA== 78 | ns2.bena.rocks. 43200 IN NSEC ns3.bena.rocks. A AAAA RRSIG NSEC 79 | ns2.bena.rocks. 3600 IN A 83.167.247.81 80 | ns2.bena.rocks. 3600 IN AAAA 2a01:430:224::81 81 | ns3.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. q15wXwp/D3KdSpcgUV04GIs8AyIE8sC9qDwPIj8dtCl/NbfttAchqqa4 dWQotjuq2C13dwQ9ivR4thZcZKGF0sCo5vHzh5kn7E06MizRV/cEK9vM h5/+4QaDx7XGGA4WrGgYgSgSrjW1IHThd3NNejSOYBr2jglkRoIawjIS UdyNbMGOHXuQAzeN5F7HsI5HPSvRdhTr+FdNQKYaW/Eu6b/16vc8kwnf 48WmWLrsTXJzeAFT3MI7z9JFg4sl3FdF8BHCPCO1aKePjr3ajZ7YIsbG TR6iVLndp7hi4uYQIWw9TM14mEAyqg37vrwgXbj8WzZdkjT3w8DUU3uW Qiybmw== 82 | ns3.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. RLB78fDDrHsrH+cAYk5oQSnFfb6tlw7mIKOlHuBRMc2kYXu9rHPSoQFu G7M8/ijxwVrxnUp5GmRwpBSU1wWf21MdgRRMDtUNlRBvCB08S/zR1ly8 GBpYu5aBsJqOEpUv+jWxE42MqaeFtvlSorDsYU3m2EcOYQwrjBzpqWID B6I3thra5iLx/B98Unu6p8/DbuyG715Ar868o27O0gx9LXIVP/PhBA7A os0wqjdnZpe4GkSyRE4D8I03B86+KiEc/Gj0oAUvWFzRihYpsaTog0EL i7Tn9Pobam7i8WsTsWz6iMPTJ4RSiX1cGE4ZQ5u9XLJ/L7QBHeJOUsEb Q2Apjg== 83 | ns3.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516155611 20200416153225 6335 bena.rocks. bxQYj8HZncSXKlfTmGp79XuZKb3nmHELsqo+Xi1DzwKIjSjPA0N3/Wiu 45+9gtjmOOHHRfWpNFB7Kd3zIED9l/HwjIJco3CQrVLfmmU/jhELMJd0 axgSk6Ga5j7V/Tl/NU0HUeFoYU3pR9s5NiIldCIBVq9qiOO8MekaOcnU WgBVcWnWd78FXF394CnwLTwDlPaXrzoBz8KYdGgw1f18VfWMBncHa5gA +UzfiE0JdS7NWhhAAME7lYVdYGfOFMVra4vV254qnMJNmk/FvI9Wrtv3 2bGqOoT/G+bdcI9rRKhzQsn265s6ioFnPY/Ro2lEyf/H73lYJZZOPrb8 2dTQ0w== 84 | ns3.bena.rocks. 43200 IN NSEC petr.bena.rocks. A AAAA RRSIG NSEC 85 | ns3.bena.rocks. 3600 IN A 139.59.182.40 86 | ns3.bena.rocks. 3600 IN AAAA 2a03:b0c0:1:a1::4da:d001 87 | petr.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. mI4uH32av52yXQERLgl0LvlaKtEkYLHYNA7wA+URmQZTHKDjfFfx5MpX 55huZ7NRcLZ2Zxjhgowq2kmaQz+MjXVPqBaZgUlgPXwNn/MYmYTTn7I/ 3cbEIbzOB8A4nYEb0LjhlR0VVQPhQ3D+/VCkX5f+P8wGiUgj2rbM9DyN xRkeFQ0yHxTsKE/JuXKNFFZP8BeYS6e3Zt2QB7QF1M1Z8UDsnHjbONHq TD2M1QvlgYBYpjFSoLQBzUJ/l3AtrIiLlT86O4KC5Thnvjx2hFBegP1f wS8oJZk5Xt+F4AsVA818DfH81YljIdj0lVMqJlxn5aQjnKild3yO8wDG 8SVwyA== 88 | petr.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. VCVOWfr7AmEE4m0tEH6ADG6v8MiOh/S82ipasyDJ/TmqbOZBF1eNuFE2 /E1VV+knb0+fX0Y7sSP8otNsbeZnjMS9X+zWQyVtjMUvBcbmYFZI81Io cqGeGunAxWMtEjj6KKjcShnEa0GQsVMcBWW8EXgOuhVKuQGOS17TGv8r /96NZbqIDhw0rNVIiXvVp5oWG+CuM7M2A0rJAXYHE125IsTrTsZBwSZg 8bxjErj9Mt/wVsZsvVSBwJ7eiJ2UzsF3tUkaOTRZ1O43J+8TnYnlOH8d 0TMLtN6g5L5MPVz2bFI7Pr0qHGDlqS6+tAb7vD7/ZSdvJ0xVnDsp7AAz VXjNDg== 89 | petr.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516155611 20200416153225 6335 bena.rocks. PhQtDgs1SYbcSPgIb5U3hBKyv/7frseLTG32kIVjJo94jQdasuTg9Peu TrmC/2sk38/SYwqmQrdMb4icIfgcX9ltizAHJwVVC7Pd1UiWocMuJdTQ jSMOT5Thg5jOXS54sznwv0ixuPrPbJ1SJuVw/8B2+9wKhYFb//pyeUob MKryaL9TwY0J1vg4qylYHmXw3HFMuCLxwoc2a5APYARDafTsTse/7gSB DPEPk+gjv5ZSA/+WaeWXPtXSce4sGCEMEO61S3bNSqbDUAx9vhkRH5Oe IMSHS0sk66QzmQ+W9yIwM4/r4ex6O9qCAYu4iix3U4F/OClETNFW8dN5 PoQ1vg== 90 | petr.bena.rocks. 43200 IN NSEC wiki.bena.rocks. A AAAA RRSIG NSEC 91 | petr.bena.rocks. 3600 IN A 83.167.247.81 92 | petr.bena.rocks. 3600 IN AAAA 2a01:430:224::81 93 | wiki.bena.rocks. 3600 IN RRSIG AAAA 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. mi4NP47JZxtV0lTTrXM9qxMXUv8a41zBlmdhD3eZAykRwXK1YTPOMxta IWV9Q8EnXB/AEnhpMnM85hF8RmIzhO3L8ay7U3MElblHfXFkhNKWlhVx bxxXWTZ94csc84pQFgOGXlvxQT7sfBajmHR+nKzoSpV6TCcPlUGQHaSl xv/iZrl8UDO3E5bRZeGzVWNffBIZ4KsBLVBTsadKTmdow2vVLwYydVOB JZJy1KMd7zw2stymgT9smbBe0B9PBsiL9VP/F39WLyJn455+1aFd1llR YQxyXwvF/JB+0te3Y1B2hh9mBc7hQIKR3kMaPddV08DWJWYUhArpXZX+ m81rdQ== 94 | wiki.bena.rocks. 3600 IN RRSIG A 7 3 3600 20200516155611 20200416153225 6335 bena.rocks. oUh0qug8Y3cn8Ae+7p8fbtJrb6izTXTgM2TEKMu2Tr7am5WyJKm0Y7BK BYgjGHm7MCaltyNecTS1ZJAYZuAm53nbN/JVwFwC1wsoW3bJ8YDx7fJ6 BKJe63Kd/tbut9TawgsSv0bBCmYU062zDV48yps72Ft4llgmDdmuDPYC Ipm+0n0rgu5whZ5kRXKTHoGhS+eD2BLqn9qJ0r6W0n5oCaTYUWAa8wi8 CjJX3QLaFwqFxcJy0l8TCQ18yXuF8U4SnMoNqqe/0D1A9aw0DxgaZaui gyw86t7VVFikmBp45lC4D8wQSsIoi8p1GoyP1MzYnF4VwB4OuwlJGTYi 2DoX0g== 95 | wiki.bena.rocks. 43200 IN RRSIG NSEC 7 3 43200 20200516155611 20200416153225 6335 bena.rocks. gPYWHuD4/jgFzgbHXOU57kwXie1QwgRMQeu1LXTNJxyx327jEl+Om+Sm Tmldng/kXyt2Q0NRGpGmcU6Ce1I9wVgOLuaCHXm8lDcajHRGfuyvNwXS nWiLd4pvbYLmqs9wyhe0jaDyixZ6MKZG9pIIHsbHh0qFd0TatVrPaujb +IG+Y0MO+Ied2cJ2NTwVkn8X3K+T8YEMRp78WOB1iY8qbsrVzB5ruKhn wqrwHHdfViuW/seyaH9H8aHb3RsAOfUB6ANeuyCZDUNPChU2K0Jc+qWQ iZAtv390C4qDAp0AEYFy0pwcVAVwBk62DMiZEZ+h3noeOlQ9CDbJnhLo uqzq+Q== 96 | wiki.bena.rocks. 43200 IN NSEC bena.rocks. A AAAA RRSIG NSEC 97 | wiki.bena.rocks. 3600 IN A 83.167.247.81 98 | wiki.bena.rocks. 3600 IN AAAA 2a01:430:224::81 99 | bena.rocks. 3600 IN SOA ns.bena.rocks. ops.bena.rocks. 90 1800 800 604800 43200 100 | ;; Query time: 123 msec 101 | ;; SERVER: 2a01:430:224::2#53(2a01:430:224::2) 102 | ;; WHEN: Tue Apr 28 23:53:29 CEST 2020 103 | ;; XFR size: 96 records (messages 1, bytes 16511) 104 | 105 | -------------------------------------------------------------------------------- /util/unit.php: -------------------------------------------------------------------------------- 1 | Evaluate('Check for non-existence of PTR zones (none) in empty list', Zones::HasPTRZones() === false); 44 | $g_domains['168.192.in-addr.arpa'] = [ ]; 45 | $g_domains['192.in-addr.arpa'] = [ 'ttl' => 200 ]; 46 | $ut->Evaluate('Check for non-existence of PTR zones (none) in empty list', Zones::HasPTRZones() === true); 47 | $ut->Evaluate('Get zone for FQDN', Zones::GetZoneForFQDN('0.0.168.192.in-addr.arpa') == '168.192.in-addr.arpa'); 48 | $ut->Evaluate('Test GetDefaultTTL()', Zones::GetDefaultTTL('168.192.in-addr.arpa') == 3600); 49 | $ut->Evaluate('Test GetDefaultTTL()', Zones::GetDefaultTTL('192.in-addr.arpa') == 200); 50 | 51 | $dz1 = raw_zone_to_array(file_get_contents(dirname(__FILE__) . '/testdata/valid.zone1')); 52 | $dz2 = raw_zone_to_array(file_get_contents(dirname(__FILE__) . '/testdata/invalid.zone')); 53 | $dz3 = raw_zone_to_array(file_get_contents(dirname(__FILE__) . '/testdata/valid.zone2')); 54 | 55 | $ut->Evaluate('Check validness of valid zone testdata/valid.zone1', CheckIfZoneIsComplete($dz1) === true); 56 | $ut->Evaluate('Check validness of invalid zone testdata/invalid.zone', CheckIfZoneIsComplete($dz2) === false); 57 | $ut->Evaluate('Check validness of valid zone testdata/valid.zone2', CheckIfZoneIsComplete($dz3) === true); 58 | $ut->Evaluate('Check count of records in testdata/valid.zone1', count($dz1) === 389); 59 | $ut->Evaluate('Parser test - zone 1', CheckZone($dz1)); 60 | $ut->Evaluate('Parser test - zone 2', CheckZone($dz3)); 61 | 62 | $ut->Evaluate('Validator - valid #1', IsValidHostName('insw.cz') === true); 63 | $ut->Evaluate('Validator - valid #2', IsValidHostName('te-st1.petr.bena.rocks') === true); 64 | $ut->Evaluate('Validator - valid #3', IsValidHostName('*.petr.bena.rocks') === true); 65 | $ut->Evaluate('Validator - valid #4', IsValidHostName('_spf.petr.bena.rocks') === true); 66 | $ut->Evaluate('Validator - valid #5', IsValidHostName('wqdcsrv331') === true); 67 | $ut->Evaluate('Validator - valid #6', IsValidHostName('2.168.192.in-addr.arpa') === true); 68 | $ut->Evaluate('Validator - invalid #1', IsValidHostName('-invalid') === false); 69 | $ut->Evaluate('Validator - invalid #2', IsValidHostName('---') === false); 70 | $ut->Evaluate('Validator - invalid #3', IsValidHostName('google domain') === false); 71 | $ut->Evaluate('Validator - invalid #4', IsValidHostName('google.com;rm -rf /') === false); 72 | $ut->Evaluate('Validator - invalid #5', IsValidHostName("google.com\ntest") === false); 73 | $ut->Evaluate('Validator - invalid #6', IsValidHostName("google.com\ttest") === false); 74 | $ut->Evaluate('Validator - invalid #7', IsValidHostName("'google.com") === false); 75 | $ut->Evaluate('Validator - invalid #8', IsValidHostName("\"google.com") === false); 76 | $ut->Evaluate('Validator - invalid #9', IsValidHostName('$test.org') === false); 77 | $ut->Evaluate('Validator - invalid #10', IsValidHostName('/x.test.org') === false); 78 | 79 | echo ("\n"); 80 | $ut->PrintResults(); 81 | 82 | $ut->ExitTest(); 83 | -------------------------------------------------------------------------------- /util/unit_api.php: -------------------------------------------------------------------------------- 1 | Evaluate("get_version", $version['version'] == G_DNSTOOL_VERSION); 28 | 29 | $logged = api("action=is_logged"); 30 | $ut->Evaluate("is_logged", $logged['is_logged']); 31 | 32 | $list = api("action=list_zones"); 33 | //var_export($list); 34 | $ut->Evaluate("list_zones contains local test", array_key_exists('test.local', $list)); 35 | $ut->Evaluate("list_zones contains update server localhost", $list['test.local']['update_server'] == 'localhost'); 36 | $ut->Evaluate("list_zones contains transfer server localhost", $list['test.local']['transfer_server'] == 'localhost'); 37 | 38 | $zone_transfer = api("action=list_records&zone=test.local"); 39 | $ut->Evaluate("list_records contains SOA for local", $zone_transfer[0][3] == "SOA"); 40 | //var_export($zone_transfer); 41 | 42 | $output = api('action=create_record&record=test1.test.local&ttl=10&type=A&value=10.2.2.8'); 43 | $ut->Evaluate('create_record test A', $output['result'] == 'success'); 44 | 45 | $record = api('action=get_record&record=test1.test.local'); 46 | $ut->Evaluate('get_record TTL', $record[0][1] == '10'); 47 | $ut->Evaluate('get_record value', $record[0][4] == '10.2.2.8'); 48 | $ut->Evaluate('get_record name', $record[0][0] == 'test1.test.local.'); 49 | 50 | $output = api('action=delete_record&record=test1.test.local&ttl=10&type=A&value=10.2.2.8'); 51 | $ut->Evaluate('delete_record test A', $output['result'] == 'success'); 52 | 53 | $output = api('action=delete_record&record=test.local&ttl=10&type=SOA&value='); 54 | $ut->Evaluate("delete_record that isn't allowed", isset($output['error'])); 55 | 56 | $output = api('action=create_record&record=-.test.local&ttl=10&type=A&value=10.2.2.8'); 57 | $ut->Evaluate('create invalid record #1', isset($output['error'])); 58 | 59 | $output = api('action=create_record&record=$.test.local&ttl=10&type=A&value=10.2.2.8'); 60 | $ut->Evaluate('create invalid record #2', isset($output['error'])); 61 | 62 | $fqdn = api('action=get_zone_for_fqdn&fqdn=meep.test.local'); 63 | $ut->Evaluate('get zone for FQDN (test.local name)', $fqdn['zone'] == 'test.local'); 64 | 65 | $fqdn = api('action=get_zone_for_fqdn&fqdn=local'); 66 | $ut->Evaluate('get zone for nonexistent FQDN', $fqdn['error'] != ''); 67 | 68 | $login = api("action=logout"); 69 | $ut->Evaluate("logout", $login['result'] == 'success'); 70 | 71 | 72 | 73 | echo ("\n\n\n"); 74 | $ut->PrintResults(); 75 | 76 | $ut->ExitTest(); 77 | --------------------------------------------------------------------------------