= 300) { jtable_respond(null, 'error', "API Error: $code"); } return $json; } function zones_api_request($opts = null, $type = 'POST') { global $apisid; return api_request("/servers/${apisid}/zones", $opts, $type); } function get_all_zones() { return zones_api_request(); } function _get_zone_by_key($key, $value) { if ($value !== '') { foreach (get_all_zones() as $zone) { if ($zone[$key] === $value) { $zone['owner'] = get_zone_owner($zone['name'], 'admin'); if (!check_owner($zone)) { jtable_respond(null, 'error', 'Access denied'); } return $zone; } } } header('Status: 404 Not found'); jtable_respond(null, 'error', "Zone not found"); } function get_zone_by_url($zoneurl) { return _get_zone_by_key('url', $zoneurl); } function get_zone_by_id($zoneid) { return _get_zone_by_key('id', $zoneid); } function get_zone_by_name($zonename) { return _get_zone_by_key('name', $zonename); } /* This function is taken from: http://pageconfig.com/post/how-to-validate-ascii-text-in-php and got fixed by #powerdns */ function is_ascii($string) { return ( bool ) ! preg_match( '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x80-\\xff]/' , $string ); } function _valid_label($name) { return is_ascii($name) && ( bool ) preg_match("/^([-.a-z0-9_\/\*]+)?$/i", $name ); } function make_record($zone, $input) { global $defaults; $name = isset($input['name']) ? $input['name'] : ''; if ('' == $name) { $name = $zone['name']; } elseif (string_ends_with($name, '.')) { # "absolute" name, shouldn't append zone[name] - but check. $name = substr($name, 0, -1); if (!string_ends_with($name, $zone['name'])) { jtable_respond(null, 'error', "Name $name not in zone ".$zone['name']); } } else if (!string_ends_with($name, $zone['name'])) { $name = $name . '.' . $zone['name']; } $ttl = (int) ((isset($input['ttl']) && $input['ttl']) ? $input['ttl'] : $defaults['ttl']); $priority = (int) ((isset($input['priority']) && $input['priority']) ? $input['priority'] : $defaults['priority']); $type = isset($input['type']) ? $input['type'] : ''; $disabled = (bool) (isset($input['disabled']) && $input['disabled']); $content = isset($input['content']) ? $input['content'] : ''; if ($type === 'TXT') { # empty TXT records are ok, otherwise require surrounding quotes: "..." if (strlen($content) == 1 || substr($content, 0, 1) !== '"' || substr($content, -1) !== '"') { # fix quoting: first escape all \, then all ", then surround with quotes. $content = '"'.str_replace('"', '\\"', str_replace('\\', '\\\\', $content)).'"'; } } if (!_valid_label($name)) { jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]"); } if (!$type) { jtable_respond(null, 'error', "Require a type"); } if (!is_ascii($content)) { jtable_respond(null, 'error', "Please only use ASCII-characters in your fields"); } return array( 'disabled' => $disabled, 'type' => $type, 'name' => $name, 'ttl' => $ttl, 'priority' => $priority, 'content' => $content); } function update_records($zone, $name_and_type, $inputs) { # need one "record" to extract name and type, in case we have no inputs # (deletion of all records) $name_and_type = make_record($zone, $name_and_type); $name = $name_and_type['name']; $type = $name_and_type['type']; $records = array(); foreach ($inputs as $input) { $record = make_record($zone, $input); if ($record['name'] !== $name || $record['type'] !== $type) { jtable_respond(null, 'error', "Records not matching"); } array_push($records, $record); } if (!_valid_label($name)) { jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]"); } $patch = array( 'rrsets' => array(array( 'name' => $name, 'type' => $type, 'changetype' => count($records) ? 'REPLACE' : 'DELETE', 'records' => $records))); api_request($zone['url'], $patch, 'PATCH'); } function create_record($zone, $input) { $record = make_record($zone, $input); $records = get_records_by_name_type($zone, $record['name'], $record['type']); array_push($records, $record); $patch = array( 'rrsets' => array(array( 'name' => $record['name'], 'type' => $record['type'], 'changetype' => 'REPLACE', 'records' => $records))); api_request($zone['url'], $patch, 'PATCH'); return $record; } function get_records_by_name_type($zone, $name, $type) { $zone = api_request($zone['url']); $records = array(); foreach ($zone['records'] as $record) { if ($record['name'] == $name and $record['type'] == $type) { array_push($records, $record); } } return $records; } function decode_record_id($id) { $record = json_decode($id, 1); if (!$record || !isset($record['name']) || !isset($record['type']) || !isset($record['ttl']) || !isset($record['priority']) || !isset($record['content']) || !isset($record['disabled'])) { jtable_respond(null, 'error', "Invalid record id"); } return $record; } # get all records with same name and type but different id (content) # requires records with id to be present # SOA records match always, regardless of content. function get_records_except($zone, $exclude) { $is_soa = ($exclude['type'] == 'SOA'); $found = false; $zone = api_request($zone['url']); $records = array(); foreach ($zone['records'] as $record) { if ($record['name'] == $exclude['name'] and $record['type'] == $exclude['type']) { if ($is_soa) { # SOA changes all the time (serial); we can't match it in a sane way. # OTOH we know it is unique anyway - just pretend we found a match. $found = true; } elseif ($record['content'] != $exclude['content'] or $record['ttl'] != $exclude['ttl'] or $record['priority'] != $exclude['priority'] or $record['disabled'] != $exclude['disabled']) { array_push($records, $record); } else { $found = true; } } } if (!$found) { header("Status: 404 Not Found"); jtable_respond(null, 'error', "Didn't find record with id"); } return $records; } function compareName($a, $b) { $a = array_reverse(explode('.', $a)); $b = array_reverse(explode('.', $b)); for ($i = 0; ; ++$i) { if (!isset($a[$i])) { return isset($b[$i]) ? -1 : 0; } else if (!isset($b[$i])) { return 1; } $cmp = strnatcasecmp($a[$i], $b[$i]); if ($cmp) { return $cmp; } } } function zone_compare($a, $b) { if ($cmp = compareName($a['name'], $b['name'])) return $cmp; return 0; } function rrtype_compare($a, $b) { # sort specials before everything else $specials = array('SOA', 'NS', 'MX'); $spa = array_search($a, $specials, true); $spb = array_search($b, $specials, true); if ($spa === false) { return ($spb === false) ? strcmp($a, $b) : 1; } else { return ($spb === false) ? -1 : $spa - $spb; } } function record_compare($a, $b) { if ($cmp = compareName($a['name'], $b['name'])) return $cmp; if ($cmp = rrtype_compare($a['type'], $b['type'])) return $cmp; if ($cmp = ($a['priority'] - $b['priority'])) return $cmp; if ($cmp = strnatcasecmp($a['content'], $b['content'])) return $cmp; return 0; } function add_db_zone($zonename, $ownername) { if (valid_user($ownername) === false) { jtable_respond(null, 'error', "$ownername is not a valid username"); } if (!_valid_label($zonename)) { jtable_respond(null, 'error', "$zonename is not a valid zonename"); } if (is_apiuser() && !user_exists($ownername)) { add_user($ownername); } $db = get_db(); $q = $db->prepare("INSERT OR REPLACE INTO zones (zone, owner) VALUES (?, (SELECT id FROM users WHERE emailaddress = ?))"); $q->bindValue(1, $zonename, SQLITE3_TEXT); $q->bindValue(2, $ownername, SQLITE3_TEXT); $q->execute(); $db->close(); } function delete_db_zone($zonename) { if (!_valid_label($zonename)) { jtable_respond(null, 'error', "$zonename is not a valid zonename"); } $db = get_db(); $q = $db->prepare("DELETE FROM zones WHERE zone = ?"); $q->bindValue(1, $zonename, SQLITE3_TEXT); $q->execute(); $db->close(); } function get_zone_owner($zonename, $default) { if (!_valid_label($zonename)) { jtable_respond(null, 'error', "$zonename is not a valid zonename"); } $db = get_db(); $q = $db->prepare("SELECT u.emailaddress FROM users u, zones z WHERE z.owner = u.id AND z.zone = ?"); $q->bindValue(1, $zonename, SQLITE3_TEXT); $result = $q->execute(); $zoneinfo = $result->fetchArray(SQLITE3_ASSOC); $db->close(); if (isset($zoneinfo['emailaddress']) && $zoneinfo['emailaddress'] != null ) { return $zoneinfo['emailaddress']; } return $default; } function get_zone_keys($zone) { $ret = array(); foreach (api_request($zone['url'] . "/cryptokeys") as $key) { if (!isset($key['active'])) continue; $key['dstxt'] = $zone['name'] . ' IN DNSKEY '.$key['dnskey']."\n\n"; if (isset($key['ds'])) { foreach ($key['ds'] as $ds) { $key['dstxt'] .= $zone['name'] . ' IN DS '.$ds."\n"; } unset($key['ds']); } $ret[] = $key; } return $ret; } function check_owner($zone) { return is_adminuser() or ($zone['owner'] === get_sess_user()); } if (isset($_GET['action'])) { $action = $_GET['action']; } else { jtable_respond(null, 'error', 'No action given'); } switch ($action) { case "list": case "listslaves": $return = array(); $q = isset($_POST['domsearch']) ? $_POST['domsearch'] : false; foreach (get_all_zones() as $zone) { $zone['owner'] = get_zone_owner($zone['name'], 'admin'); if (!check_owner($zone)) continue; if ($q && !preg_match("/$q/", $zone['name'])) { continue; } if ($action == "listslaves" and $zone['kind'] == "Slave") { array_push($return, $zone); } elseif ($action == "list" and $zone['kind'] != "Slave") { if ($zone['dnssec']) { $zone['keyinfo'] = get_zone_keys($zone); } array_push($return, $zone); } } usort($return, "zone_compare"); jtable_respond($return); break; case "create": $zonename = isset($_POST['name']) ? $_POST['name'] : ''; $zonekind = isset($_POST['kind']) ? $_POST['kind'] : ''; if (!is_adminuser() and $allowzoneadd !== true) { jtable_respond(null, 'error', "You are not allowed to add zones"); } if (!_valid_label($zonename)) { jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]"); } if (!$zonename || !$zonekind) { jtable_respond(null, 'error', "Not enough data"); } $createOptions = array( 'name' => $zonename, 'kind' => $zonekind, ); $nameservers = array(); if (isset($_POST['nameserver1']) && $_POST['nameserver1'] != null) { array_push($nameservers, $_POST['nameserver1']); } if (isset($_POST['nameserver2']) && $_POST['nameserver2'] != null) { array_push($nameservers, $_POST['nameserver2']); } if ($zonekind != "Slave") { if (!isset($_POST['zone'])) { if (0 == count($nameservers)) { jtable_respond(null, 'error', "Require nameservers"); } $createOptions['nameservers'] = $nameservers; } else { $createOptions['zone'] = $_POST['zone']; } if (isset($defaults['soa_edit_api'])) { $createOptions['soa_edit_api'] = $defaults['soa_edit_api']; } if (isset($defaults['soa_edit'])) { $createOptions['soa_edit'] = $defaults['soa_edit']; } } else { // Slave if (isset($_POST['masters'])) { $createOptions['masters'] = preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY); } if (0 == count($createOptions['masters'])) { jtable_respond(null, 'error', "Slave requires master servers"); } } // only admin user and original owner can "recreate" zones that are already // present in our own db but got lost in pdns. if (!is_adminuser() && get_sess_user() !== get_zone_owner($zonename, get_sess_user())) { jtable_respond(null, 'error', 'Zone already owned by someone else'); } $zone = zones_api_request($createOptions); $zonename = $zone['name']; if (is_adminuser() && isset($_POST['owner'])) { add_db_zone($zonename, $_POST['owner']); } else { add_db_zone($zonename, get_sess_user()); } if (isset($_POST['template']) && $_POST['template'] != 'None') { foreach (user_template_list() as $template) { if ($template['name'] !== $_POST['template']) continue; foreach ($template['records'] as $record) { if (isset($record['label'])) { $record['name'] = $record['label']; unset($record['label']); } create_record($zone, $record); } break; } } if (isset($_POST['zone']) && isset($_POST['owns']) && $_POST['owns'] && count($nameservers)) { $records = array(); foreach ($nameservers as $ns) { array_push($records, array('type' => 'NS', 'content' => $ns)); } update_records($zone, $records[0], $records); } unset($zone['records']); unset($zone['comments']); jtable_respond($zone, 'single'); break; case "update": $zone = get_zone_by_id(isset($_POST['id']) ? $_POST['id'] : ''); $zoneowner = isset($_POST['owner']) ? $_POST['owner'] : $zone['owner']; if ($zone['owner'] !== $zoneowner) { if (!is_adminuser()) { header("Status: 403 Access denied"); jtable_respond(null, 'error', "Can't change owner"); } else { add_db_zone($zone['name'], $zoneowner); $zone['owner'] = $zoneowner; } } $update = false; if (isset($_POST['masters'])) { $zone['masters'] = preg_split('/[,;\s]+/', $_POST['masters'], null, PREG_SPLIT_NO_EMPTY); $update = true; } if ($update) { $zoneUpdate = $zone; unset($zoneUpdate['id']); unset($zoneUpdate['url']); unset($zoneUpdate['owner']); $newZone = api_request($zone['url'], $zoneUpdate, 'PUT'); $newZone['owner'] = $zone['owner']; } else { $newZone = $zone; } unset($newZone['records']); unset($newZone['comments']); jtable_respond($newZone, 'single'); break; case "delete": $zone = get_zone_by_id(isset($_POST['id']) ? $_POST['id'] : ''); api_request($zone['url'], array(), 'DELETE'); delete_db_zone($zone['name']); jtable_respond(null, 'delete'); break; case "listrecords": $zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : ''); $records = api_request($zone['url'])['records']; foreach ($records as &$record) { $record['id'] = json_encode($record); } unset($record); usort($records, "record_compare"); jtable_respond($records); break; case "createrecord": $zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : ''); $record = create_record($zone, $_POST); $record['id'] = json_encode($record); jtable_respond($record, 'single'); break; case "editrecord": $zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : ''); $old_record = decode_record_id(isset($_POST['id']) ? $_POST['id'] : ''); $records = get_records_except($zone, $old_record); $record = make_record($zone, $_POST); if ($record['name'] !== $old_record['name']) { # rename: $newRecords = get_records_by_name_type($zone, $record['name'], $record['type']); array_push($newRecords, $record); update_records($zone, $old_record, $records); # remove from old list update_records($zone, $record, $newRecords); # add to new list } else { array_push($records, $record); update_records($zone, $record, $records); } $record['id'] = json_encode($record); jtable_respond($record, 'single'); break; case "deleterecord": $zone = get_zone_by_url(isset($_GET['zoneurl']) ? $_GET['zoneurl'] : ''); $old_record = decode_record_id(isset($_POST['id']) ? $_POST['id'] : ''); $records = get_records_except($zone, $old_record); update_records($zone, $old_record, $records); jtable_respond(null, 'delete'); break; default: jtable_respond(null, 'error', 'No such action'); break; }