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: | ++ | |
- | + |