├── .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 '
'; 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 serversupdate 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('');
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('