headers = Array(); $this->hostname = $apiip; $this->port = $apiport; $this->auth = $apipass; $this->proto = $apiproto; $this->sslverify = $apisslverify; $this->curlh = curl_init(); $this->method = FALSE; $this->content = FALSE; $this->apiurl = ''; } public function addheader($field, $content) { $this->headers[$field] = $content; } private function authheaders() { $this->addheader('X-API-Key', $this->auth); } private function apiurl() { $tmp = new ApiHandler(); $tmp->url = '/api'; $tmp->go(); if ($tmp->json[0]['version'] <= 1) { $this->apiurl = $tmp->json[0]['url']; } else { throw new Exception("Unsupported API version"); } } private function curlopts() { $this->authheaders(); $this->addheader('Accept', 'application/json'); curl_setopt($this->curlh, CURLOPT_HTTPHEADER, Array()); curl_setopt($this->curlh, CURLOPT_RETURNTRANSFER, 1); if (strcasecmp($this->proto, 'https')) { curl_setopt($this->curlh, CURLOPT_SSL_VERIFYPEER, $this->sslverify); } $setheaders = Array(); foreach ($this->headers as $k => $v) { array_push($setheaders, join(": ", Array($k, $v))); } curl_setopt($this->curlh, CURLOPT_HTTPHEADER, $setheaders); } private function baseurl() { return $this->proto.'://'.$this->hostname.':'.$this->port.$this->apiurl; } private function go() { if ($this->content) { $this->addheader('Content-Type', 'application/json'); curl_setopt($this->curlh, CURLOPT_POST, 1); curl_setopt($this->curlh, CURLOPT_POSTFIELDS, $this->content); } switch ($this->method) { case 'DELETE': case 'PATCH': case 'PUT': curl_setopt($this->curlh, CURLOPT_CUSTOMREQUEST, $this->method); break; case 'POST': break; } curl_setopt($this->curlh, CURLOPT_URL, $this->baseurl().$this->url); $this->curlopts(); $return = curl_exec($this->curlh); $code = curl_getinfo($this->curlh, CURLINFO_HTTP_CODE); $json = json_decode($return, 1); if (isset($json['error'])) { throw new Exception("API Error $code: ".$json['error']); } elseif ($code < 200 || $code >= 300) { if ($code == 401) { throw new Exception("Authentication failed. Have you configured your authmethod correct?"); } throw new Exception("Curl Error: $code ".curl_error($this->curlh)); } $this->json = $json; } public function call() { if (empty($this->apiurl) and substr($this->url, 0, 1) == '/') { $this->apiurl(); } else { $this->apiurl = '/'; } $this->go(); } } function api_request($path, $opts = null, $type = null) { try { $myapi = new ApiHandler(); if ($type) { $myapi->method = $type; }; $myapi->url = $path; if ($opts) { $myapi->content = json_encode($opts); } $myapi->call(); return $myapi->json; } catch (Exception $e) { jtable_respond(null, 'error', $e->getMessage()); } } 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']; } $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, '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); $ttl = (int) ((isset($input['ttl']) && $input['ttl']) ? $input['ttl'] : $defaults['ttl']); $patch = array( 'rrsets' => array(array( 'name' => $record['name'], 'type' => $record['type'], 'ttl' => $ttl, '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['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['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 = strnatcasecmp($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 = 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(); foreach($_POST['nameserver'] as $ns) { if (isset($ns) && !empty($ns)) { array_push($nameservers, $ns); } } if ($zonekind != "Slave") { $createOptions['nameservers'] = $nameservers; if (!isset($_POST['zone'])) { if (0 == count($nameservers)) { jtable_respond(null, 'error', "Require 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 ($record['type'] == 'NS' and array_search($record['content'], $nameservers) !== FALSE) { continue; } 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'] : ''); $a = api_request($zone['url']); $records = $a['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'] || $record['type'] !== $old_record['type']) { # rename or retype: $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; case "export": $zone = $_GET['zone']; $export = api_request("/servers/${apisid}/zones/${zone}/export"); jtable_respond($export, 'single'); break; case "gettemplatenameservers": $ret = array(); $type = $_GET['prisec']; foreach (user_template_list() as $template) { if ($template['name'] !== $_GET['template']) continue; $rc = 0; foreach ($template['records'] as $record) { if ($record['type'] == "NS") { if (($type == 'pri' && $rc == 0) or ($type == 'sec' && $rc == 1)) { echo $record['content']; exit(0); } $rc++; } } echo ""; } break; case "getformnameservers": $inputs = array(); foreach (user_template_list() as $template) { if ($template['name'] !== $_GET['template']) continue; foreach ($template['records'] as $record) { if ($record['type'] == "NS" and array_search($record['content'], $inputs) === false) { array_push($inputs, $record['content']); echo '
'; } } } break; default: jtable_respond(null, 'error', 'No such action'); break; }