diff --git a/Dockerfile b/Dockerfile index c9ff06c..610dbda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ FROM composer:2.2 RUN apk add --no-cache tini tzdata -ADD . /src/app/ - WORKDIR /src/app -RUN composer install +COPY . . -RUN cp includes/config.environment.inc.php includes/config.inc.php +RUN set -xe; \ + composer install; \ + cp includes/config.environment.inc.php includes/config.inc.php ENV PORT 80 EXPOSE 80 diff --git a/README.markdown b/README.markdown index 6b525a4..0938c08 100644 --- a/README.markdown +++ b/README.markdown @@ -55,6 +55,7 @@ Environment variables summary * ``REDIS_1_HOST`` - define host of the Redis server * ``REDIS_1_NAME`` - define name of the Redis server * ``REDIS_1_PORT`` - define port of the Redis server +* ``REDIS_1_SCHEME`` - define scheme of the Redis server (tcp or tls) * ``REDIS_1_AUTH`` - define password of the Redis server * ``REDIS_1_AUTH_FILE`` - define file containing the password of the Redis server * ``REDIS_1_DATABASES`` - You can modify you config to prevent phpRedisAdmin from using CONFIG command diff --git a/composer.json b/composer.json index e1043de..3b3ab81 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "erik-dubbelboer/php-redis-admin", "description": "Simple web interface to manage Redis databases.", - "version": "1.19.2", + "version": "1.24.0", "license": "CC-BY-3.0", "homepage": "https://github.com/ErikDubbelboer/phpRedisAdmin", "authors": [ @@ -15,7 +15,7 @@ "require": { "ext-mbstring": "*", "ext-json": "*", - "predis/predis": "v1.1.9", + "predis/predis": "v2.3.0", "paragonie/random_compat": ">=2" }, "minimum-stability": "stable", diff --git a/composer.lock b/composer.lock index 41f9438..f724f31 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad3fb9bfb043b086870b317dc9eff9b7", + "content-hash": "401ff61cebe5223d003a47192d7c3d6a", "packages": [ { "name": "paragonie/random_compat", @@ -58,27 +58,28 @@ }, { "name": "predis/predis", - "version": "v1.1.9", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "c50c3393bb9f47fa012d0cdfb727a266b0818259" + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/c50c3393bb9f47fa012d0cdfb727a266b0818259", - "reference": "c50c3393bb9f47fa012d0cdfb727a266b0818259", + "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.4" }, "suggest": { - "ext-curl": "Allows access to Webdis when paired with phpiredis", - "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" }, "type": "library", "autoload": { @@ -91,19 +92,13 @@ "MIT" ], "authors": [ - { - "name": "Daniele Alessandri", - "email": "suppakilla@gmail.com", - "homepage": "http://clorophilla.net/", - "role": "Creator & Maintainer" - }, { "name": "Till Krüss", "homepage": "https://till.im/", "role": "Maintainer" } ], - "description": "Flexible and feature-complete Redis client for PHP and HHVM", + "description": "A flexible and feature-complete Redis client for PHP.", "homepage": "http://github.com/predis/predis", "keywords": [ "nosql", @@ -112,7 +107,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v1.1.9" + "source": "https://github.com/predis/predis/tree/v2.3.0" }, "funding": [ { @@ -120,16 +115,19 @@ "type": "github" } ], - "time": "2021-10-05T19:02:38+00:00" + "time": "2024-11-21T20:00:02+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.1.0" + "platform": { + "ext-mbstring": "*", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/css/frame.css b/css/frame.css index 8969e10..7cf935d 100644 --- a/css/frame.css +++ b/css/frame.css @@ -52,3 +52,13 @@ float: left; margin: 1em; } + +.exception { +border: 1px solid #ff0000; +background-color: rgba(255, 0, 0, 0.2); +color: #880000; +padding: 10px; +border-radius: 5px; +margin: 20px; +} + diff --git a/edit.php b/edit.php index 6f3fd20..998012c 100644 --- a/edit.php +++ b/edit.php @@ -34,72 +34,83 @@ die('ERROR: could not encode value'); } - // String - if ($_POST['type'] == 'string') { - $redis->set($key, $value); - } - - // Hash - else if (($_POST['type'] == 'hash') && isset($_POST['hkey'])) { - if (strlen($_POST['hkey']) > $config['maxkeylen']) { - die('ERROR: Your hash key is to long (max length is '.$config['maxkeylen'].')'); + try { + // String + if ($_POST['type'] == 'string') { + $redis->set($key, $value); } - if ($edit && !$redis->hExists($key, input_convert($_POST['hkey']))) { - $redis->hDel($key, input_convert($_GET['hkey'])); + // Hash + else if (($_POST['type'] == 'hash') && isset($_POST['hkey'])) { + if (strlen($_POST['hkey']) > $config['maxkeylen']) { + die('ERROR: Your hash key is to long (max length is '.$config['maxkeylen'].')'); + } + + if ($edit && !$redis->hExists($key, input_convert($_POST['hkey']))) { + $redis->hDel($key, input_convert($_GET['hkey'])); + } + + $redis->hSet($key, input_convert($_POST['hkey']), $value); } - $redis->hSet($key, input_convert($_POST['hkey']), $value); - } + // List + else if (($_POST['type'] == 'list') && isset($_POST['index'])) { + $size = $redis->lLen($key); + + if (($_POST['index'] == '') || + ($_POST['index'] == $size)) { + // Push it at the end + $redis->rPush($key, $value); + } else if ($_POST['index'] == -1) { + // Push it at the start + $redis->lPush($key, $value); + } else if (($_POST['index'] >= 0) && + ($_POST['index'] < $size)) { + // Overwrite an index + $redis->lSet($key, input_convert($_POST['index']), $value); + } else { + die('ERROR: Out of bounds index'); + } + } - // List - else if (($_POST['type'] == 'list') && isset($_POST['index'])) { - $size = $redis->lLen($key); - - if (($_POST['index'] == '') || - ($_POST['index'] == $size)) { - // Push it at the end - $redis->rPush($key, $value); - } else if ($_POST['index'] == -1) { - // Push it at the start - $redis->lPush($key, $value); - } else if (($_POST['index'] >= 0) && - ($_POST['index'] < $size)) { - // Overwrite an index - $redis->lSet($key, input_convert($_POST['index']), $value); - } else { - die('ERROR: Out of bounds index'); + // Set + else if ($_POST['type'] == 'set') { + if ($_POST['value'] != $_POST['oldvalue']) { + // The only way to edit a Set value is to add it and remove the old value. + $redis->sRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); + $redis->sAdd($key, $value); + } } - } - // Set - else if ($_POST['type'] == 'set') { - if ($_POST['value'] != $_POST['oldvalue']) { - // The only way to edit a Set value is to add it and remove the old value. - $redis->sRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); - $redis->sAdd($key, $value); + // ZSet + else if (($_POST['type'] == 'zset') && isset($_POST['score']) && is_numeric($_POST['score'])) { + // The only way to edit a ZSet value is to add it and remove the old value. + $redis->zRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); + $redis->zAdd($key, input_convert($_POST['score']), $value); } - } - // ZSet - else if (($_POST['type'] == 'zset') && isset($_POST['score'])) { - // The only way to edit a ZSet value is to add it and remove the old value. - $redis->zRem($key, encodeOrDecode('save', $key, input_convert($_POST['oldvalue']))); - $redis->zAdd($key, input_convert($_POST['score']), $value); - } + // Refresh the top so the key tree is updated. + require 'includes/header.inc.php'; - // Refresh the top so the key tree is updated. - require 'includes/header.inc.php'; + ?> + + - - +
+

getMessage() ?>

+
+ 'unix', 'path' => $server['path'])); } else { - $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client('tcp://'.$server['host'].':'.$server['port']); + $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client($server['scheme'].'://'.$server['host'].':'.$server['port']); } try { @@ -132,6 +148,9 @@ } } +if (!isset($config['login']) && !empty($config['login_as_acl_auth'])) { + require_once PHPREDIS_ADMIN_PATH . '/includes/login_acl.inc.php'; +} if ($server['db'] != 0) { if (!$redis->select($server['db'])) { diff --git a/includes/config.environment.inc.php b/includes/config.environment.inc.php index 798281f..06a06fe 100644 --- a/includes/config.environment.inc.php +++ b/includes/config.environment.inc.php @@ -2,6 +2,19 @@ include 'config.sample.inc.php'; +// get configs from environment variables +$config['cookie_auth'] = getenv('COOKIE_AUTH') ?: false; +$config['count_elements_page'] = getenv('COUNT_ELEMENTS_PAGE') ?: 100; +$config['faster'] = getenv('FASTER') ?: true; +$config['filter'] = getenv('FILTER') ?: '*'; +$config['hideEmptyDBs'] = getenv('HIDE_EMPTY_DBS') ?: false; +$config['keys'] = getenv('KEYS') ?: false; +$config['maxkeylen'] = getenv('MAX_KEY_LEN') ?: 100; +$config['scansize'] = getenv('SCAN_SIZE') ?: 1000; +$config['scanmax'] = getenv('SCAN_MAX') ?: 1000; +$config['seperator'] = getenv('SEPERATOR') ?: ':'; +$config['showEmptyNamespaceAsKey'] = getenv('SHOW_EMPTY_NAMESPACE_AS_KEY') ?: false; + $admin_user = getenv('ADMIN_USER'); $admin_pass = getenv('ADMIN_PASS'); @@ -23,6 +36,7 @@ $server_name = getenv($prefix . 'NAME'); $server_host = getenv($prefix . 'HOST'); $server_port = getenv($prefix . 'PORT'); + $server_scheme = getenv($prefix . 'SCHEME'); if (getenv($prefix . 'AUTH_FILE') !== false) { $server_auth = file_get_contents(getenv($prefix . 'AUTH_FILE')); } else { @@ -37,29 +51,36 @@ if (empty($server_name)) { $server_name = $server_host; } - + if (empty($server_auth)) { $server_auth = ""; - } + } - if (empty($server_port)) { + if (empty($server_port) && strpos($server_host, ':') === false) { $server_port = 6379; } + if (empty($server_scheme)) { + $server_scheme = 'tcp'; + } + $config['servers'][] = array( - 'name' => $server_name, - 'host' => $server_host, - 'port' => $server_port, - 'filter' => '*', + 'name' => $server_name, + 'host' => $server_host, + 'port' => $server_port, + 'filter' => $config['filter'], + 'scansize' => $config['scansize'], + 'scanmax' => $config['scanmax'], + 'scheme' => $server_scheme, ); - + if (!empty($server_auth)) { $config['servers'][$i-1]['auth'] = $server_auth; - } - + } + if (!empty($server_databases)) { $config['servers'][$i-1]['databases'] = $server_databases; - } + } $i++; } diff --git a/includes/config.sample.inc.php b/includes/config.sample.inc.php index 10ed201..923a60c 100644 --- a/includes/config.sample.inc.php +++ b/includes/config.sample.inc.php @@ -32,7 +32,8 @@ 'flush' => false, // Set to true to enable the flushdb button for this instance. 'charset' => 'cp1251', // Keys and values are stored in redis using this encoding (default utf-8). 'keys' => false, // Use the old KEYS command instead of SCAN to fetch all keys for this server (default uses config default). - 'scansize' => 1000 // How many entries to fetch using each SCAN command for this server (default uses config default). + 'scansize' => 1000, // How many entries to fetch using each SCAN command for this server (default uses config default). + 'scanmax' => 1000, // In each query, SCAN command may be executed several times. To shorten the duration, it is recommended to limit the total number of entries to fetch (default uses config default). ),*/ ), @@ -60,6 +61,12 @@ ) ),*/ + // Uncomment to enable login as ACL authentication (won't work if 'login' or 'auth' is also used) + // Only support using one server at this moment. + // If you set the default user off, browsers will be redirected to login page. + // The user and password will be stored in browser as plaintext so using HTTPS is strongly recommended. + // 'login_as_acl_auth' => true, + // Use HTML form/cookie-based auth instead of HTTP Basic/Digest auth 'cookie_auth' => false, @@ -83,5 +90,8 @@ 'keys' => false, // How many entries to fetch using each SCAN command. - 'scansize' => 1000 + 'scansize' => 1000, + + // The total number of entries to fetch. Set to 0 or -1 for no limit. + 'scanmax' => 0 ); diff --git a/includes/login_acl.inc.php b/includes/login_acl.inc.php new file mode 100644 index 0000000..e8b7d72 --- /dev/null +++ b/includes/login_acl.inc.php @@ -0,0 +1,100 @@ +auth($login['username'], $login['password']); + } catch (Predis\Response\ServerException $exception) { + return false; + } + return true; +} + +// This fill will perform HTTP basic authentication. The authentication data will be sent and stored as plaintext +// Please make sure to use HTTPS proxy such as Apache or Nginx to prevent traffic eavesdropping. +function authHttpBasic() +{ + $realm = 'phpRedisAdmin'; + + if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { + try { + global $redis; + $redis->ping(); + } catch (Predis\Response\ServerException $exception) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="' . $realm . '"'); + die('NOAUTH -- Authentication required'); + } + } + + $login = [ + 'username' => $_SERVER['PHP_AUTH_USER'], + 'password' => $_SERVER['PHP_AUTH_PW'], + ]; + + if (!authCheck($login)) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="' . $realm . '"'); + die('NOAUTH -- Authentication required'); + } + + return $login; +} + +// Perform auth using a standard HTML
submission and cookies to save login state +function authCookie() +{ + if (!empty($_COOKIE['phpRedisAdminLogin'])) { + // We have a cookie; is it correct? + // Cookie value looks like "username:password-hash" + $login = explode(':', $_COOKIE['phpRedisAdminLogin'], 2); + if (count($login) === 2) { + $login = [ + 'username' => $login[0], + 'password' => $login[1], + ]; + if (authCheck($login)) { + return $login; + } + } + } + + if (isset($_POST['username'], $_POST['password'])) { + // Login form submitted; correctly? + $login = [ + 'username' => $_POST['username'], + 'password' => $_POST['password'], + ]; + + if (authCheck($login)) { + setcookie('phpRedisAdminLogin', $login['username'] . ':' . $login['password']); + // This should be an absolute URL, but that's a bit of a pain to generate; this will work + header("Location: index.php"); + die(); + } + } + + try { + global $redis; + $redis->ping(); + } catch (Predis\Response\ServerException $exception) { + // If we're here, we don't have a valid login cookie and we don't have a + // valid form submission, so redirect to the login page if we aren't + // already on that page + if (!defined('LOGIN_PAGE')) { + header("Location: login.php"); + die(); + } + } + + // We must be on the login page without a valid cookie or submission + return null; +} + +if (!empty($config['cookie_auth'])) { + $login = authCookie(); +} else { + $login = authHttpBasic(); +} diff --git a/index.php b/index.php index 44f7c85..ea509c7 100644 --- a/index.php +++ b/index.php @@ -10,16 +10,16 @@ } else { $next = 0; $keys = array(); - while (true) { $r = $redis->scan($next, 'MATCH', $server['filter'], 'COUNT', $server['scansize']); - $next = $r[0]; $keys = array_merge($keys, $r[1]); - if ($next == 0) { break; } + if ($server['scanmax'] > 0 && count($keys) >= $server['scanmax']) { + break; + } } } @@ -82,7 +82,13 @@ function print_namespace($item, $name, $fullkey, $islast) { // Get the number of items in the key. if (!isset($config['faster']) || !$config['faster']) { - switch ($redis->type($fullkey)) { + $type = ''; + try { + $type = $redis->type($fullkey); + } catch (\Predis\Response\ServerException $th) { + $class[] = 'empty'; + } + switch ($type) { case 'hash': $len = $redis->hLen($fullkey); break; @@ -252,6 +258,9 @@ function getDbInfo($d, $info, $padding = '') {
+
+ scanned keys 0 && count($keys) >= $server['scanmax']) ? ', reached scanmax' : '' ?> +
diff --git a/login.php b/login.php index a80a24c..5c12301 100644 --- a/login.php +++ b/login.php @@ -33,7 +33,7 @@ > + > diff --git a/logout.php b/logout.php index 12cc80b..07e10ae 100644 --- a/logout.php +++ b/logout.php @@ -8,6 +8,10 @@ setcookie('phpRedisAdminLogin', '', 1); header("Location: login.php"); die(); +} else if (isset($config['login_as_acl_auth'])) { + // HTTP Basic auth + header('HTTP/1.1 401 Unauthorized'); + die(''); } else { // HTTP Digest auth $needed_parts = array( diff --git a/overview.php b/overview.php index 3e426db..5c67370 100644 --- a/overview.php +++ b/overview.php @@ -10,16 +10,24 @@ $server['db'] = 0; } - // Setup a connection to Redis. - if(isset($server['scheme']) && $server['scheme'] === 'unix' && $server['path']) { - $redis = new Predis\Client(array('scheme' => 'unix', 'path' => $server['path'])); + + if (isset($config['login_as_acl_auth'])) { + // Currently only support one server at a time + if ($i > 0) { + break; + } } else { - $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client('tcp://'.$server['host'].':'.$server['port']); - } - try { - $redis->connect(); - } catch (Predis\CommunicationException $exception) { - $redis = false; + // Setup a connection to Redis. + if(isset($server['scheme']) && $server['scheme'] === 'unix' && $server['path']) { + $redis = new Predis\Client(array('scheme' => 'unix', 'path' => $server['path'])); + } else { + $redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client('tcp://'.$server['host'].':'.$server['port']); + } + try { + $redis->connect(); + } catch (Predis\CommunicationException $exception) { + $redis = false; + } } if(!$redis) { @@ -38,6 +46,9 @@ $info[$i] = $redis->info(); $info[$i]['size'] = $redis->dbSize(); + if (isset($config['login_as_acl_auth'])) { + $info[$i]['username'] = $redis->acl->whoami(); + } if (!isset($info[$i]['Server'])) { $info[$i]['Server'] = array( @@ -83,6 +94,10 @@
Uptime:
+ +
Username:
+ +
Last save:
-> (-1 to remove the TTL) +> (-1 to remove the TTL)

diff --git a/view.php b/view.php index 1836979..0fdb1b5 100644 --- a/view.php +++ b/view.php @@ -17,8 +17,18 @@ die; } -$type = $redis->type($_GET['key']); -$exists = $redis->exists($_GET['key']); +$type = ''; +$exists = false; +try { + $type = $redis->type($_GET['key']); + $exists = $redis->exists($_GET['key']); +} catch (\Predis\Response\ServerException $th) { + ?> +
+

getMessage() ?>

+
+