diff --git a/css/base.css b/css/base.css index ccdeb1a..7f3fce0 100644 --- a/css/base.css +++ b/css/base.css @@ -139,3 +139,31 @@ pre { .ui-dialog { max-width: 80%; } +tr.jtable-data-row td { + font-family: monospace; + font-size: 16px; + vertical-align: top; + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; +} +tr.jtable-data-row td.ttl, tr.jtable-data-row td.priority, +.jtable-input input.ttl, .jtable-input input.priority { + text-align: right; +} +tr.jtable-data-row td.content { + max-width: 800px; +} +.jtable-input input { + font-family: monospace; + font-size: 16px; +} +.jtable-input input.name { + width: 100%; +} +.jtable-input input.content { + min-width: 600px; + width: 100%; +} diff --git a/includes/config.inc.php-dist b/includes/config.inc.php-dist index dc173ef..bd0c2cb 100644 --- a/includes/config.inc.php-dist +++ b/includes/config.inc.php-dist @@ -4,7 +4,7 @@ $apiuser = ''; # The PowerDNS API username $apipass = ''; # The PowerDNS API-user password $apiip = ''; # The IP of the PowerDNS API $apiport = '8081'; # The port of the PowerDNS API -$apisid = ''; # PowerDNS's :server_id +$apisid = 'localhost'; # PowerDNS's :server_id $allowzoneadd = FALSE; # Allow normal users to add zones @@ -20,6 +20,9 @@ $allowzoneadd = FALSE; # Allow normal users to add zones $authdb = "../etc/pdns.users.sqlite3"; +# Set a random generated secret to enable auto-login and long living csrf tokens +// $secret = '...'; + $templates = array(); /* $templates[] = array( @@ -27,7 +30,7 @@ $templates[] = array( 'owner' => 'username', # Set to 'public' to make it available to all users 'records' => array( array( - 'label' => '', + 'name' => '', 'type' => 'MX', 'content' => 'mx2.tuxis.nl', 'priority' => '200') @@ -35,6 +38,7 @@ $templates[] = array( ); */ +$defaults['soa_edit'] = 'INCEPTION-INCREMENT'; $defaults['soa_edit_api'] = 'INCEPTION-INCREMENT'; $defaults['defaulttype'] = 'Master'; # Choose between 'Native' or 'Master' $defaults['primaryns'] = 'unconfigured.primaryns'; # The value of the first NS-record @@ -56,5 +60,3 @@ if (!file_exists($authdb)) { $salt = bin2hex(openssl_random_pseudo_bytes(16)); $db->exec("INSERT INTO users (emailaddress, password, isadmin) VALUES ('admin', '".crypt("admin", '$6$'.$salt)."', 1)"); } - -?> diff --git a/includes/misc.inc.php b/includes/misc.inc.php index b41d808..a321832 100644 --- a/includes/misc.inc.php +++ b/includes/misc.inc.php @@ -2,6 +2,22 @@ include('config.inc.php'); +function string_starts_with($string, $prefix) +{ + $length = strlen($prefix); + return (substr($string, 0, $length) === $prefix); +} + +function string_ends_with($string, $suffix) +{ + $length = strlen($suffix); + if ($length == 0) { + return true; + } + + return (substr($string, -$length) === $suffix); +} + function get_db() { global $authdb; @@ -11,17 +27,11 @@ function get_db() { return $db; } -function gen_pw() { - $password = exec('/usr/bin/pwgen -s -B -c -n 10 -1'); - return $password; -} - function get_all_users() { $db = get_db(); $r = $db->query('SELECT id, emailaddress, isadmin FROM users'); $ret = array(); - while ($row = $r->fetchArray()) { - $row['emailaddress'] = htmlspecialchars($row['emailaddress']); + while ($row = $r->fetchArray(SQLITE3_ASSOC)) { array_push($ret, $row); } @@ -39,6 +49,10 @@ function get_user_info($u) { return $userinfo; } +function user_exists($u) { + return (bool) get_user_info($u); +} + function do_db_auth($u, $p) { $db = get_db(); $q = $db->prepare('SELECT * FROM users WHERE emailaddress = ?'); @@ -46,40 +60,52 @@ function do_db_auth($u, $p) { $result = $q->execute(); $userinfo = $result->fetchArray(SQLITE3_ASSOC); $db->close(); - if (isset($userinfo['password']) and (crypt($p, $userinfo['password']) == $userinfo['password'])) { + + if ($userinfo and $userinfo['password'] and (crypt($p, $userinfo['password']) === $userinfo['password'])) { return TRUE; } return FALSE; } -function get_pw($username) { - $db = get_db(); - $q = $db->prepare('SELECT password FROM users WHERE emailaddress = ? LIMIT 1'); - $q->bindValue(1, $username, SQLITE_TEXT); - $result = $q->execute(); - $pw = $result->fetchArray(SQLITE3_ASSOC); - $db->close(); - if (isset($pw['password'])) { - return $pw['password']; +function add_user($username, $isadmin = FALSE, $password = '') { + if (!$password) { + $password = bin2hex(openssl_random_pseudo_bytes(32)); } - - return FALSE; -} - -function add_user($username, $isadmin = '0', $password = FALSE) { - if ($password === FALSE or $password == "") { - $password = get_pw($username); - } elseif (!preg_match('/\$6\$/', $password)) { + if (!string_starts_with($password, '$6$')) { $salt = bin2hex(openssl_random_pseudo_bytes(16)); $password = crypt($password, '$6$'.$salt); } $db = get_db(); - $q = $db->prepare('INSERT OR REPLACE INTO users (emailaddress, password, isadmin) VALUES (?, ?, ?)'); + $q = $db->prepare('INSERT INTO users (emailaddress, password, isadmin) VALUES (?, ?, ?)'); $q->bindValue(1, $username, SQLITE3_TEXT); $q->bindValue(2, $password, SQLITE3_TEXT); - $q->bindValue(3, $isadmin, SQLITE3_INTEGER); + $q->bindValue(3, (int)(bool) $isadmin, SQLITE3_INTEGER); + $ret = $q->execute(); + $db->close(); + + return $ret; +} + +function update_user($username, $isadmin, $password) { + if ($password && !preg_match('/\$6\$/', $password)) { + $salt = bin2hex(openssl_random_pseudo_bytes(16)); + $password = crypt($password, '$6$'.$salt); + } + + $db = get_db(); + + if ($password) { + $q = $db->prepare('UPDATE users SET isadmin = ?, password = ? WHERE emailaddress = ?'); + $q->bindValue(1, (int)(bool)$isadmin, SQLITE3_INTEGER); + $q->bindValue(2, $password, SQLITE3_TEXT); + $q->bindValue(3, $username, SQLITE3_TEXT); + } else { + $q = $db->prepare('UPDATE users SET isadmin = ? WHERE emailaddress = ?'); + $q->bindValue(1, (int)(bool)$isadmin, SQLITE3_INTEGER); + $q->bindValue(2, $username, SQLITE3_TEXT); + } $ret = $q->execute(); $db->close(); @@ -96,6 +122,10 @@ function delete_user($id) { return $ret; } +function valid_user($name) { + return ( bool ) preg_match( "/^[a-z0-9@_.-]+$/i" , $name ); +} + function jtable_respond($records, $method = 'multiple', $msg = 'Undefined errormessage') { $jTableResult = array(); if ($method == 'error') { @@ -119,13 +149,31 @@ function jtable_respond($records, $method = 'multiple', $msg = 'Undefined errorm $jTableResult['RecordCount'] = count($records); } + header('Content-Type: application/json'); print json_encode($jTableResult); exit(0); } -function valid_user($name) { - return ( bool ) preg_match( "/^[a-z0-9@_.-]+$/i" , $name ); +function user_template_list() { + global $templates; + + $templatelist = array(); + foreach ($templates as $template) { + if (is_adminuser() + or (isset($template['owner']) + and ($template['owner'] == get_sess_user() or $template['owner'] == 'public'))) { + array_push($templatelist, $template); + } + } + return $templatelist; } +function user_template_names() { + $templatenames = array('None' => 'None'); + foreach (user_template_list() as $template) { + $templatenames[$template['name']] = $template['name']; + } + return $templatenames; +} ?> diff --git a/includes/session.inc.php b/includes/session.inc.php index 51e5719..0af9f89 100644 --- a/includes/session.inc.php +++ b/includes/session.inc.php @@ -4,102 +4,286 @@ include_once('config.inc.php'); include_once('misc.inc.php'); include_once('wefactauth.inc.php'); -session_start(); +global $current_user; -function is_logged_in() { - if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] == "true") { - return TRUE; +$current_user = false; + +// session startup +function _set_current_user($username, $is_admin = false, $has_csrf_token = false, $is_api = false) { + global $current_user; + + $current_user = array( + 'username' => $username, + 'is_admin' => $is_admin, + 'has_csrf_token' => $has_csrf_token, + 'is_api' => $is_api, + ); +} + +function _check_csrf_token($user) { + global $secret; + + if (isset($_SERVER['HTTP_X_CSRF_TOKEN']) && $_SERVER['HTTP_X_CSRF_TOKEN']) { + $found_token = $_SERVER['HTTP_X_CSRF_TOKEN']; + } elseif (isset($_POST['csrf-token']) && $_POST['csrf-token']) { + $found_token = $_POST['csrf-token']; } else { - global $adminapikey; - global $adminapiips; + $found_token = ''; + } - if (isset($adminapikey) && isset($adminapiips)) { - if (array_search($_SERVER['REMOTE_ADDR'], $adminapiips) !== FALSE) { - if ($_POST['adminapikey'] == $adminapikey) { - # Allow this request, fake that we're logged in. - set_logged_in('admin'); - set_is_adminuser(); - $_SESSION['apientrance'] = 'true'; - return TRUE; - } - } + if (isset($secret) && $secret) { + # if we have a secret keep csrf-token valid across logins + $csrf_hmac_secret = hash_pbkdf2('sha256', 'csrf_hmac', $secret, 100, 0, true); + $userinfo = base64_encode($user['emailaddress']) . ':' . base64_encode($user['password']); + $csrf_token = base64_encode(hash_hmac('sha256', $userinfo, $csrf_hmac_secret, true)); + } else { + # without secret create new token for each session + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = base64_encode(openssl_random_pseudo_bytes(32)); } - return FALSE; + $csrf_token = $_SESSION['csrf_token']; } -} -function set_apiuser() { - $_SESSION['apientrance'] = 'true'; -} - -function is_apiuser() { - if (isset($_SESSION['apientrance']) && $_SESSION['apientrance'] = 'true') { - return TRUE; + if ($found_token === $csrf_token) { + global $current_user; + $current_user['has_csrf_token'] = true; } - return FALSE; + + define('CSRF_TOKEN', $csrf_token); + header("X-CSRF-Token: ${csrf_token}"); } -function set_logged_in($login_user) { - $_SESSION['logged_in'] = 'true'; - $_SESSION['username'] = $login_user; -} +function enc_secret($message) { + global $secret; -function set_is_adminuser() { - $_SESSION['is_adminuser'] = 'true'; -} + if (isset($secret) && $secret) { + $enc_secret = hash_pbkdf2('sha256', 'encryption', $secret, 100, 0, true); + $hmac_secret = hash_pbkdf2('sha256', 'encryption_hmac', $secret, 100, 0, true); -function is_adminuser() { - if (isset($_SESSION['is_adminuser']) && $_SESSION['is_adminuser'] == 'true') { - return TRUE; - } else { - return FALSE; + $mcrypt = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '') or die('missing mcrypt'); + + # add PKCS#7 padding + $blocksize = mcrypt_get_block_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $padlength = $blocksize - (strlen($message) % $blocksize); + $message .= str_repeat(chr($padlength), $padlength); + + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND); + $ciphertext = $iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $enc_secret, $message, MCRYPT_MODE_CBC, $iv); + mcrypt_module_close($mcrypt); + + $mac = hash_hmac('sha256', $ciphertext, $hmac_secret, true); + return 'enc:' . base64_encode($ciphertext) . ':' . base64_encode($mac); } + + return base64_encode($message); } -function get_sess_user() { - return $_SESSION['username']; +function dec_secret($code) { + global $secret; + $is_encrypted = (substr($code, 0, 4) === 'enc:'); + if (isset($secret) && $secret) { + if (!$is_encrypted) return false; + + $msg = explode(':', $code); + if (3 != count($msg)) return false; + + $enc_secret = hash_pbkdf2('sha256', 'encryption', $secret, 100, 0, true); + $hmac_secret = hash_pbkdf2('sha256', 'encryption_hmac', $secret, 100, 0, true); + + $msg[1] = base64_decode($msg[1]); + $msg[2] = base64_decode($msg[2]); + + $mac = hash_hmac('sha256', $msg[1], $hmac_secret, true); + # compare hashes first: this should prevent any timing leak + if (hash('sha256', $mac, true) !== hash('sha256', $msg[2], true)) return false; + if ($mac !== $msg[2]) return false; + + $mcrypt = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '') or die('missing mcrypt'); + $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC); + $iv = substr($msg[1], 0, $iv_size); + $ciphertext = substr($msg[1], $iv_size); + $plaintext = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $enc_secret, $ciphertext, MCRYPT_MODE_CBC, $iv); + mcrypt_module_close($mcrypt); + + # remove PKCS#7 padding + $len = strlen($plaintext); + $padlength = ord($plaintext[$len-1]); + $plaintext = substr($plaintext, 0, $len - $padlength); + + return $plaintext; + } + + if ($is_encrypted) return false; + return base64_decode($code); } -function logout() { - session_destroy(); +function _unset_cookie($name) { + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + setcookie($name, null, -1, null, null, $is_ssl); +} + +function _store_auto_login($value) { + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + // set for 30 days + setcookie('NSEDIT_AUTOLOGIN', $value, time()+60*60*24*30, null, null, $is_ssl); } function try_login() { - global $wefactapiurl; - global $wefactapikey; - if (isset($_POST['username']) and isset($_POST['password'])) { - if (valid_user($_POST['username']) === FALSE) { - return FALSE; - } - $do_local_auth = 1; + if (_try_login($_POST['username'], $_POST['password'])) { + global $secret; - if (isset($wefactapiurl) && isset($wefactapikey)) { - $wefact = do_wefact_auth($_POST['username'], $_POST['password']); - if ($wefact === FALSE) { - return FALSE; - } - if ($wefact !== -1) { - $do_local_auth = 0; + # only store if we have a secret. + if ($secret && isset($_POST['autologin']) && $_POST['autologin']) { + _store_auto_login(enc_secret(json_encode(array( + 'username' => $_POST['username'], + 'password' => $_POST['password'])))); } + return true; } - - if ($do_local_auth == 1) { - if (do_db_auth($_POST['username'], $_POST['password']) === FALSE) { - return FALSE; - } - } - - $userinfo = get_user_info($_POST['username']); - - set_logged_in($_POST['username']); - if (isset($userinfo['isadmin']) && $userinfo['isadmin'] == 1) { - set_is_adminuser(); - } - return TRUE; } - - return FALSE; + return false; } -?> +function _try_login($username, $password) { + global $wefactapiurl, $wefactapikey; + + if (!valid_user($username)) { + return false; + } + + $do_local_auth = true; + + if (isset($wefactapiurl) && isset($wefactapikey)) { + $wefact = do_wefact_auth($username, $password); + if (false === $wefact ) { + return false; + } + if (-1 !== $wefact) { + $do_local_auth = false; + } + } + + if ($do_local_auth && !do_db_auth($username, $password)) { + return false; + } + + $user = get_user_info($username); + if (!$user) { + return false; + } else { + _set_current_user($username, (bool) $user['isadmin']); + + if (session_id()) { + session_unset(); + session_destroy(); + } + session_start() or die('session failure: could not start session'); + session_regenerate_id(true) or die('session failure: regenerated id failed'); + session_unset(); + $_SESSION['username'] = $username; + + # requires session: + _check_csrf_token($user); + return true; + } +} + +function _check_session() { + global $adminapikey, $adminapiips; + + $is_ssl = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off'; + session_set_cookie_params(30*60, null, null, $is_ssl, true); + session_name('NSEDIT_SESSION'); + + if (isset($adminapikey) && '' !== $adminapikey && isset($adminapiips) && isset($_POST['adminapikey'])) { + if (false !== array_search($_SERVER['REMOTE_ADDR'], $adminapiips) + and $_POST['adminapikey'] === $adminapikey) + { + # Allow this request, fake that we're logged in as user. + return _set_current_user('admin', true, true, true); + } + else + { + header('Status: 403 Forbidden'); + exit(0); + } + } + + if (isset($_COOKIE['NSEDIT_SESSION'])) { + if (session_start() && isset($_SESSION['username'])) { + $user = get_user_info($_SESSION['username']); + if (!$user) { + session_destroy(); + session_unset(); + } else { + _set_current_user($_SESSION['username'], (bool) $user['isadmin']); + _check_csrf_token($user); + return; + } + } + // failed: remove cookie + _unset_cookie('NSEDIT_SESSION'); + } + + if (isset($_COOKIE['NSEDIT_AUTOLOGIN'])) { + $login = json_decode(dec_secret($_COOKIE['NSEDIT_AUTOLOGIN']), 1); + if ($login and isset($login['username']) and isset($login['password']) + and _try_login($login['username'], $login['password'])) { + _store_auto_login($_COOKIE['NSEDIT_AUTOLOGIN']); # reset cookie + return; + } + + // failed: remove cookie + _unset_cookie('NSEDIT_AUTOLOGIN'); + } +} + +# auto load session if possible +_check_session(); + +function is_logged_in() { + global $current_user; + return (bool) $current_user; +} + +# GET/HEAD requests only require a logged in user (they shouldn't trigger any +# "writes"); all other requests require the X-CSRF-Token to be present. +function is_csrf_safe() { + global $current_user; + + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + case 'HEAD': + return is_logged_in(); + default: + return (bool) $current_user && (bool) $current_user['has_csrf_token']; + } +} + +function is_apiuser() { + global $current_user; + return $current_user && (bool) $current_user['is_api']; +} + +function is_adminuser() { + global $current_user; + return $current_user && (bool) $current_user['is_admin']; +} + +function get_sess_user() { + global $current_user; + return $current_user ? $current_user['username'] : null; +} + +function logout() { + @session_destroy(); + @session_unset(); + if (isset($_COOKIE['NSEDIT_AUTOLOGIN'])) { + _unset_cookie('NSEDIT_AUTOLOGIN'); + } + if (isset($_COOKIE['NSEDIT_SESSION'])) { + _unset_cookie('NSEDIT_SESSION'); + } +} diff --git a/includes/wefactauth.inc.php b/includes/wefactauth.inc.php index 517dcab..43adc07 100644 --- a/includes/wefactauth.inc.php +++ b/includes/wefactauth.inc.php @@ -78,5 +78,3 @@ function do_wefact_auth($u, $p) { return -1; } } - -?> diff --git a/index.php b/index.php index ea2f477..db142e1 100644 --- a/index.php +++ b/index.php @@ -7,12 +7,11 @@ include_once('includes/misc.inc.php'); if (isset($_GET['logout']) or isset($_POST['logout'])) { logout(); header("Location: index.php"); + exit(0); } -if (!is_logged_in() and isset($_POST['formname']) && $_POST['formname'] == "loginform") { - if (try_login() === TRUE) { - set_logged_in($_POST['username']); - } else { +if (!is_logged_in() and isset($_POST['formname']) and $_POST['formname'] === "loginform") { + if (!try_login()) { $errormsg = "Error while trying to authenticate you\n"; } } @@ -54,18 +53,28 @@ if (!is_logged_in()) { - + - + + + + + + + - +
Username:
Password:
Remember me:
- + @@ -76,19 +85,6 @@ if (!is_logged_in()) { exit(0); } -foreach ($templates as $template) { - if (is_adminuser() or (isset($template['owner']) && $template['owner'] == get_sess_user()) or ($template['owner'] == 'public')) { - $templatelist[] = "'" . $template['name'] . "':'" . $template['name'] . "'"; - } -} - -if (isset($templatelist)) { - $tmpllist = ','; - $tmpllist .= join(',', $templatelist); -} else { - $tmpllist = ''; -} - ?>
@@ -126,21 +122,60 @@ if (isset($templatelist)) {
- - + \ No newline at end of file diff --git a/users.php b/users.php index 2933c01..0e0a2e1 100644 --- a/users.php +++ b/users.php @@ -4,49 +4,95 @@ include_once('includes/config.inc.php'); include_once('includes/session.inc.php'); include_once('includes/misc.inc.php'); -if (!is_logged_in()) { - header("Location: index.php"); +if (!is_csrf_safe()) { + header('Status: 403'); + header('Location: ./index.php'); + jtable_respond(null, 'error', "Authentication required"); } if (!is_adminuser()) { + header('Status: 403'); jtable_respond(null, 'error', "You need adminprivileges to get here"); } -if (isset($_GET['action'])) { - $action = $_GET['action']; -} else { +if (!isset($_GET['action'])) { + header('Status: 400'); jtable_respond(null, 'error', 'No action given'); } -if ($action == "list") { +switch ($_GET['action']) { + +case "list": $users = get_all_users(); jtable_respond($users); -} elseif ($action == "listoptions") { + break; + +case "listoptions": $users = get_all_users(); $retusers = array(); foreach ($users as $user) { - $retusers[] = array ( + $retusers[] = array( 'DisplayText' => $user['emailaddress'], 'Value' => $user['emailaddress']); } jtable_respond($retusers, 'options'); -} elseif ($action == "create" or $action == "update") { - if (valid_user($_POST['emailaddress']) === FALSE) { + break; + +case "create": + $emailaddress = isset($_POST['emailaddress']) ? $_POST['emailaddress'] : ''; + $isadmin = isset($_POST['isadmin']) ? $_POST['isadmin'] : '0'; + $password = isset($_POST['password']) ? $_POST['password'] : ''; + + if (!valid_user($emailaddress)) { jtable_respond(null, 'error', "Please only use ^[a-z0-9@_.-]+$ for usernames"); } - $isadmin = $_POST['isadmin'] ? $_POST['isadmin'] : '0'; - if (add_user($_POST['emailaddress'], $isadmin, $_POST['password']) !== FALSE) { - unset($_POST['password']); - jtable_respond($_POST, 'single'); - } else { - jtable_respond(null, 'error', 'Could not add/change this user'); + + if (!$password) { + jtable_respond(null, 'error', 'Cannot create user without password'); } -} elseif ($action == "delete") { + + if (user_exists($emailaddress)) { + jtable_respond(null, 'error', 'User already exists'); + } + + if (add_user($emailaddress, $isadmin, $password)) { + $result = array('emailaddress' => $emailaddress, 'isadmin' => $isadmin); + jtable_respond($result, 'single'); + } else { + jtable_respond(null, 'error', 'Could not create user'); + } + break; + +case "update": + $emailaddress = isset($_POST['emailaddress']) ? $_POST['emailaddress'] : ''; + $isadmin = isset($_POST['isadmin']) ? $_POST['isadmin'] : '0'; + $password = isset($_POST['password']) ? $_POST['password'] : ''; + + if (!valid_user($emailaddress)) { + jtable_respond(null, 'error', "Please only use ^[a-z0-9@_.-]+$ for usernames"); + } + + if (!user_exists($emailaddress)) { + jtable_respond(null, 'error', 'Cannot update not existing user'); + } + + if (update_user($emailaddress, $isadmin, $password)) { + $result = array('emailaddress' => $emailaddress, 'isadmin' => $isadmin); + jtable_respond($result, 'single'); + } else { + jtable_respond(null, 'error', 'Could not update user'); + } + break; + +case "delete": if (delete_user($_POST['id']) !== FALSE) { jtable_respond(null, 'delete'); } else { - jtable_respond(null, 'error', 'Could not delete this user'); + jtable_respond(null, 'error', 'Could not delete user'); } -} + break; -?> +default: + jtable_respond(null, 'error', 'Invalid action'); + break; +} diff --git a/zones.php b/zones.php index 9ef30d7..952cd58 100644 --- a/zones.php +++ b/zones.php @@ -1,105 +1,210 @@ = 300) { + jtable_respond(null, 'error', "API Error: $code"); } + return $json; } -function _valid_label($name) { - return ( bool ) preg_match( "/^([-.a-z0-9_\/\*]+)?$/i" , $name ); +function zones_api_request($opts = null, $type = 'POST') { + global $apisid; + + return api_request("/servers/${apisid}/zones", $opts, $type); } -function _create_record($name, $records, $input, $zoneurl) { - global $defaults; +function get_all_zones() { + return zones_api_request(); +} - $content = ($input['type'] == "TXT") ? '"'.$input['content'].'"' : $input['content']; +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 (_valid_label($input['name']) === FALSE) { - jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]"); - } - if (is_ascii($content) === FALSE or is_ascii($input['name']) === FALSE) { - jtable_respond(null, 'error', "Please only use ASCII-characters in your fields"); + if (!check_owner($zone)) { + jtable_respond(null, 'error', 'Access denied'); + } + return $zone; + } + } } + header('Status: 404 Not found'); + jtable_respond(null, 'error', "Zone not found"); +} - if (preg_match('/^TXT$/', $input['type'])) { - $content = stripslashes($input['content']); - $content = preg_replace('/(^"|"$)/', '', $content); - $content = addslashes($content); - $content = '"'.$content.'"'; - } +function get_zone_by_url($zoneurl) { + return _get_zone_by_key('url', $zoneurl); +} - array_push($records, array( - 'disabled' => false, - 'type' => $input['type'], - 'name' => $name, - 'ttl' => isset($input['ttl']) ? $input['ttl'] : $defaults['ttl'], - 'priority' => $input['priority'] ? $input['priority'] : $defaults['priority'], - 'content' => $content)); +function get_zone_by_id($zoneid) { + return _get_zone_by_key('id', $zoneid); +} - $patch = array(); - $patch['rrsets'] = array(); - array_push($patch['rrsets'], array( - 'comments' => array(), - 'records' => $records, - 'changetype'=> "REPLACE", - 'type' => $input['type'], - 'name' => $name)); - _do_curl($zoneurl, $patch, 'patch'); - - return $records; +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 = '' ) { +function is_ascii($string) { return ( bool ) ! preg_match( '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x80-\\xff]/' , $string ); } -function getrecords_by_name_type($zoneurl, $name, $type) { - $zone = json_decode(_do_curl($zoneurl), 1); +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) { + if ($record['name'] == $name and $record['type'] == $type) { array_push($records, $record); } } @@ -107,74 +212,157 @@ function getrecords_by_name_type($zoneurl, $name, $type) { return $records; } -function zonesort($a, $b) { - return strnatcmp($a["name"], $b["name"]); +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; } -function add_db_zone($zone, $owner) { - if (valid_user($owner) === FALSE) { - jtable_respond(null, 'error', "$owner is not a valid username"); - } - if (_valid_label($zone) === FALSE) { - jtable_respond(null, 'error', "$zone is not a valid zonename"); +# 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 (is_apiuser()) { - if (!get_user_info($owner)) { - add_user($owner); + 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, $zone, SQLITE3_TEXT); - $q->bindValue(2, $owner, SQLITE3_TEXT); + $q->bindValue(1, $zonename, SQLITE3_TEXT); + $q->bindValue(2, $ownername, SQLITE3_TEXT); $q->execute(); $db->close(); } -function delete_db_zone($owner) { - if (_valid_label($zone) === FALSE) { - jtable_respond(null, 'error', "$zone is not a valid zonename"); +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, $zone, SQLITE3_TEXT); + $q->bindValue(1, $zonename, SQLITE3_TEXT); $q->execute(); $db->close(); } -function get_zone_owner($zone) { - if (_valid_label($zone) === FALSE) { - jtable_respond(null, 'error', "$zone is not a valid zonename"); +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, $zone, SQLITE3_TEXT); + $q->bindValue(1, $zonename, SQLITE3_TEXT); $result = $q->execute(); $zoneinfo = $result->fetchArray(SQLITE3_ASSOC); $db->close(); - if (isset($zoneinfo['emailaddress']) && $zoneinfo['emailaddress'] != NULL ) { + if (isset($zoneinfo['emailaddress']) && $zoneinfo['emailaddress'] != null ) { return $zoneinfo['emailaddress']; } - - return 'admin'; + + return $default; } function get_zone_keys($zone) { $ret = array(); - foreach (json_decode(_do_curl("/servers/:serverid:/zones/".$zone."/cryptokeys"), 1) as $key) { + foreach (api_request($zone['url'] . "/cryptokeys") as $key) { if (!isset($key['active'])) continue; - $key['dstxt'] = $zone.' IN DNSKEY '.$key['dnskey']."\n\n"; + $key['dstxt'] = $zone['name'] . ' IN DNSKEY '.$key['dnskey']."\n\n"; if (isset($key['ds'])) { foreach ($key['ds'] as $ds) { - $key['dstxt'] .= $zone.' IN DS '.$ds."\n"; + $key['dstxt'] .= $zone['name'] . ' IN DS '.$ds."\n"; } + unset($key['ds']); } - unset($key['ds']); $ret[] = $key; } @@ -182,15 +370,7 @@ function get_zone_keys($zone) { } function check_owner($zone) { - if (is_adminuser() === TRUE) { - return TRUE ; - } - - if (get_zone_owner($zone) == get_sess_user()) { - return TRUE; - } - - return FALSE; + return is_adminuser() or ($zone['owner'] === get_sess_user()); } if (isset($_GET['action'])) { @@ -199,185 +379,229 @@ if (isset($_GET['action'])) { jtable_respond(null, 'error', 'No action given'); } -if ($action == "list" or $action== "listslaves") { - $rows = json_decode(_do_curl('servers/:serverid:/zones'), 1); +switch ($action) { + +case "list": +case "listslaves": $return = array(); - foreach ($rows as $zone) { - if (check_owner($zone['name']) === FALSE) + $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 (isset($_POST['domsearch'])) { - $q = $_POST['domsearch']; - if (!preg_match("/$q/", $zone['name']) == 1) { - continue; - } + if ($q && !preg_match("/$q/", $zone['name'])) { + continue; } - $zone['name'] = htmlspecialchars($zone['name']); - $zone['owner'] = get_zone_owner($zone['name']); if ($action == "listslaves" and $zone['kind'] == "Slave") { array_push($return, $zone); } elseif ($action == "list" and $zone['kind'] != "Slave") { - if ($zone['dnssec'] == true) { - $zone['keyinfo'] = get_zone_keys($zone['name']); + if ($zone['dnssec']) { + $zone['keyinfo'] = get_zone_keys($zone); } array_push($return, $zone); } } - usort($return, "zonesort"); + usort($return, "zone_compare"); jtable_respond($return); -} elseif ($action == "create") { - if (is_adminuser() !== TRUE and $allowzoneadd !== TRUE) { + 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($_POST['name']) === FALSE) { + if (!_valid_label($zonename)) { jtable_respond(null, 'error', "Please only use [a-z0-9_/.-]"); } - if (is_ascii($_POST['name']) === FALSE) { - jtable_respond(null, 'error', "Please only use ASCII-characters in your domainname"); + + if (!$zonename || !$zonekind) { + jtable_respond(null, 'error', "Not enough data"); } - if ($_POST['kind'] != null and $_POST['name'] != null) { - $nameservers = array(); - if ($_POST['kind'] != "Slave") { - if (isset($_POST['nameserver1']) && $_POST['nameserver1'] != null) { - array_push($nameservers, $_POST['nameserver1']); - if (isset($_POST['nameserver2']) && $_POST['nameserver2'] != null) { - array_push($nameservers, $_POST['nameserver2']); - } - } else { - jtable_respond(null, 'error', "Not enough data: ".print_r($_POST, 1)); - } - if (isset($defaults['soa_edit_api'])) { - $vars['soa_edit_api'] = $defaults['soa_edit_api']; - } - } - $vars['name'] = $_POST['name']; - $vars['kind'] = $_POST['kind']; - if (isset($_POST['zone'])) { - $vars['zone'] = $_POST['zone']; - $vars['nameservers'] = array(); - } else { - $vars['nameservers'] = $nameservers; - } - _do_curl('/servers/:serverid:/zones', $vars); - if (isset($_POST['owner']) and $_POST['owner'] != 'admin') { - add_db_zone($vars['name'], $_POST['owner']); - } else { - add_db_zone($vars['name'], get_sess_user()); - } - if (isset($_POST['template']) && $_POST['template'] != 'None') { - foreach ($templates as $template) { - if ($template['name'] == $_POST['template']) { - $zoneurl = '/servers/:serverid:/zones/'.$vars['name'].'.'; - foreach ($template['records'] as $record) { - if ($record['label'] != "") { - $name = join('.', array($record['label'], $vars['name'])); - } else { - $name = $vars['name']; - } - if (!isset($record['name'])) { - $record['name'] = ""; - } - $records = getrecords_by_name_type($zoneurl, $name, $record['type']); - $records = _create_record($name, $records, $record, $zoneurl); - } - } - } - } - + + $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'])) { - $vars = $_POST; - $vars['serial'] = 0; - $vars['records'] = array(); - jtable_respond($vars, 'single'); - } - $zoneurl = '/servers/:serverid:/zones/'.$vars['name'].'.'; - if (isset($_POST['owns'])) { - $patch = array(); - $patch['rrsets'] = array(); - array_push($patch['rrsets'], array( - 'comments' => array(), - 'records' => array(), - 'changetype'=> "REPLACE", - 'type' => 'NS', - 'name' => $vars['name'])); - _do_curl($zoneurl, $patch, 'patch'); - foreach ($nameservers as $ns) { - $records = getrecords_by_name_type($zoneurl, $vars['name'], 'NS'); - $records = _create_record($vars['name'], $records, array('type' => 'NS', 'name' => '', 'content' => $ns), $zoneurl); + if (0 == count($nameservers)) { + jtable_respond(null, 'error', "Require nameservers"); } - } - - $vars = _do_curl($zoneurl); - jtable_respond($vars, 'single'); - } else { - jtable_respond(null, 'error', "Not enough data: ".print_r($_POST, 1)); - } -} elseif ($action == "listrecords" && $_GET['zoneurl'] != null) { - $rows = json_decode(_do_curl($_GET['zoneurl']), 1); - $soa = array(); - $ns = array(); - $mx = array(); - $any = array(); - foreach ($rows['records'] as $idx => $record) { - $rows['records'][$idx]['id'] = json_encode($record); - $rows['records'][$idx]['name'] = htmlspecialchars($record['name']); - if ($record['type'] == 'SOA') { array_push($soa, $rows['records'][$idx]); } - elseif ($record['type'] == 'NS') { array_push($ns, $rows['records'][$idx]); } - elseif ($record['type'] == 'MX') { array_push($mx, $rows['records'][$idx]); } - else { - array_push($any, $rows['records'][$idx]); - }; - } - usort($any, "zonesort"); - $ret = array_merge($soa, $ns, $mx, $any); - jtable_respond($ret); -} elseif ($action == "delete") { - _do_curl("/servers/:serverid:/zones/".$_POST['id'], array(), 'delete'); - $zone = preg_replace("/\.$/", "", $_POST['id']); - delete_db_zone($zone); - jtable_respond(null, 'delete'); -} elseif ($action == "createrecord" or $action == "editrecord") { - $name = (!preg_match("/\.".$_POST['domain']."\.?$/", $_POST['name'])) ? $_POST['name'].'.'.$_POST['domain'] : $_POST['name']; - $name = preg_replace("/\.$/", "", $name); - $name = preg_replace("/^\./", "", $name); - $records = array(); - if ($action == "createrecord") { - $records = getrecords_by_name_type($_GET['zoneurl'], $name, $_POST['type']); - } - - $records =_create_record($name, $records, $_POST, $_GET['zoneurl']); - jtable_respond($records[sizeof($records)-1], 'single'); -} elseif ($action == "deleterecord") { - $todel = json_decode($_POST['id'], 1); - $records = getrecords_by_name_type($_GET['zoneurl'], $todel['name'], $todel['type']); - $precords = array(); - - foreach ($records as $record) { - if ( - $record['content'] == $todel['content'] and - $record['type'] == $todel['type'] and - $record['prio'] == $todel['prio'] and - $record['name'] == $todel['name']) { - continue; + $createOptions['nameservers'] = $nameservers; } else { - array_push($precords, $record); + $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"); } } - $patch = array(); - $patch['rrsets'] = array(); - array_push($patch['rrsets'], array( - 'comments' => array(), - 'records' => $precords, - 'changetype'=> "REPLACE", - 'type' => $todel['type'], - 'name' => $todel['name'])); - _do_curl($_GET['zoneurl'], $patch, 'patch'); + + // 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'); -} elseif ($action == "update") { - add_db_zone($_POST['name'], $_POST['owner']); - jtable_respond($_POST, 'single'); -} else { + 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; } -?>