diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a5fe8e7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [erikdubbelboer] diff --git a/.gitignore b/.gitignore index daf3eea..4dc5d11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ config.inc.php *.phar vendor + +# IDEs metadata +/nbproject/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..610dbda --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM composer:2.2 + +RUN apk add --no-cache tini tzdata + +WORKDIR /src/app + +COPY . . + +RUN set -xe; \ + composer install; \ + cp includes/config.environment.inc.php includes/config.inc.php + +ENV PORT 80 +EXPOSE 80 +ENTRYPOINT [ "sh", "-c", "tini -- php -S 0.0.0.0:$PORT" ] diff --git a/README.markdown b/README.markdown index d126910..0938c08 100644 --- a/README.markdown +++ b/README.markdown @@ -21,14 +21,14 @@ You can find an example database at Installing/Configuring ====================== -To install phpRedisAdmin through [composer](http://getcomposer.org/) you need to execute the following commands: +To install [phpRedisAdmin](https://packagist.org/packages/erik-dubbelboer/php-redis-admin) through [composer](http://getcomposer.org/) you need to execute the following commands: ``` curl -s http://getcomposer.org/installer | php php composer.phar create-project -s dev erik-dubbelboer/php-redis-admin path/to/install ``` -You may also want to copy includes/config.simple.inc.php to includes/config.inc.php +You may also want to copy includes/config.sample.inc.php to includes/config.inc.php and edit it with your specific redis configuration. Instead of using composer, you can also do a manual install using: @@ -39,6 +39,29 @@ cd phpRedisAdmin git clone https://github.com/nrk/predis.git vendor ``` +Docker Image +============ +A public [phpRedisAdmin Docker image](https://hub.docker.com/r/erikdubbelboer/phpredisadmin/) is available on Docker Hub built from the latest tag. +The file ```includes/config.environment.inc.php``` is used as the configuration file to allow environment variables to be used as configuration values. +Example: +``` +docker run --rm -it -e REDIS_1_HOST=myredis.host -e REDIS_1_NAME=MyRedis -p 80:80 erikdubbelboer/phpredisadmin +``` +Also, a Docker Compose manifest with a stack for testing and development is provided. Just issue ```docker-compose up --build``` to start it and browse to http://localhost. See ```docker-compose.yml``` file for configuration details. + +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 +* ``ADMIN_USER`` - define username for user-facing Basic Auth +* ``ADMIN_PASS`` - define password for user-facing Basic Auth + TODO ==== diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1627ff2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.13.x | :white_check_mark: | +| 1.12.x | :white_check_mark: | +| < 1.12 | :x: | + +## Reporting a Vulnerability + +Vulnerabilities can be emailed to erik@dubbelboer.com diff --git a/composer.json b/composer.json index dd19d92..3b3ab81 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "erik-dubbelboer/php-redis-admin", "description": "Simple web interface to manage Redis databases.", - "version": "1.1.2", - "license": "CC-BY-ND", + "version": "1.24.0", + "license": "CC-BY-3.0", "homepage": "https://github.com/ErikDubbelboer/phpRedisAdmin", "authors": [ { @@ -13,8 +13,11 @@ } ], "require": { - "predis/predis": "0.8.*" + "ext-mbstring": "*", + "ext-json": "*", + "predis/predis": "v2.3.0", + "paragonie/random_compat": ">=2" }, "minimum-stability": "stable", - "target-dir": "ErikDubbelboer/phpRedisAdmin" + "target-dir": "ErikDubbelboer/phpRedisAdmin" } diff --git a/composer.lock b/composer.lock index 55ba3ee..f724f31 100644 --- a/composer.lock +++ b/composer.lock @@ -1,35 +1,90 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" ], - "hash": "81de6835a02ee54ae5d87ce2b395ba6e", + "content-hash": "401ff61cebe5223d003a47192d7c3d6a", "packages": [ + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com/" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "predis/predis", - "version": "v0.8.3", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/nrk/predis.git", - "reference": "v0.8.3" + "url": "https://github.com/predis/predis.git", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nrk/predis/zipball/v0.8.3", - "reference": "v0.8.3", + "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "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": { - "psr-0": { - "Predis": "lib/" + "psr-4": { + "Predis\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -38,35 +93,41 @@ ], "authors": [ { - "name": "Daniele Alessandri", - "email": "suppakilla@gmail.com", - "homepage": "http://clorophilla.net/" + "name": "Till Krüss", + "homepage": "https://till.im/", + "role": "Maintainer" } ], - "description": "Flexible and feature-complete PHP client library for Redis", - "homepage": "http://github.com/nrk/predis", + "description": "A flexible and feature-complete Redis client for PHP.", + "homepage": "http://github.com/predis/predis", "keywords": [ "nosql", "predis", "redis" ], - "time": "2013-02-18 14:03:45" + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2024-11-21T20:00:02+00:00" } ], - "packages-dev": [ - - ], - "aliases": [ - - ], + "packages-dev": [], + "aliases": [], "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": [ - - ], - "platform-dev": [ - - ] + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-mbstring": "*", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/css/common.css b/css/common.css index 005a792..9dc6276 100644 --- a/css/common.css +++ b/css/common.css @@ -50,3 +50,8 @@ padding: 3px 0 1px 20px; background: url(http://github.com/images/add.png) left center no-repeat; } + +.data { +white-space: pre-wrap; +} + diff --git a/css/frame.css b/css/frame.css index e54ca9e..7cf935d 100644 --- a/css/frame.css +++ b/css/frame.css @@ -14,7 +14,6 @@ margin-left: -8em; } form .button { -margin-left: -7em; } @@ -53,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/css/index.css b/css/index.css index 6fa6dda..4a56aa9 100644 --- a/css/index.css +++ b/css/index.css @@ -3,11 +3,14 @@ position: absolute; top: 0; bottom: 0; left: 0; -width: 24em; +display: flex; +flex-direction: column; +width: 290px; height: 100%; -padding-left: 1em; -border-right: 1px solid #000; -overflow: hidden; +} + +#header { +padding-left: 10px; } #sidebar a, #sidebar a:visited { @@ -24,11 +27,10 @@ text-decoration: underline; #keys { -position: fixed; -top: 18.5em; -bottom: 0; -width: 24em; -padding-bottom: 1em; +flex-grow: 1; +width: 290px; +margin-top: 10px; +padding-left: 10px; overflow: auto; } @@ -40,11 +42,17 @@ padding: 0; #keys li { font-weight: normal; +white-space: nowrap; } #keys li.folder { font-weight: bold; margin-top: .05em; +cursor: pointer; +} + +#keys li.empty a { +color: #888; } #keys li.current a { @@ -104,18 +112,41 @@ display: none; display: inline; } +#resize { +position: fixed; +top: 0; +left: 300px; +bottom: 0; +width: 5px; +background-color: #aaa; +cursor: col-resize; +padding: 0; +margin: 0; +border-left: 1px solid #000; +} + +#resize-layover { +position: fixed; +top: 0; +left: 305px; +right: 0; +bottom: 0; +width: 100%; +z-index: 1000; +} #frame { position: fixed; top: 0; -left: 25em; +left: 305px; right: 0; bottom: 0; -padding-left: 1em; } #frame iframe { -width: 100%; +width: calc(100% - 3em); height: 100%; +margin-left: 1.5em; +margin-right: 1.5em; } diff --git a/css/login.css b/css/login.css new file mode 100644 index 0000000..f439934 --- /dev/null +++ b/css/login.css @@ -0,0 +1,116 @@ +/* Styles borrowed from http://getbootstrap.com/examples/signin/ */ +h1.logo { + text-align: center; +} +h2 { + font-size: 30px; +} +h2 { + margin-top: 20px; + margin-bottom: 10px; +} +h2 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +.form-signin { + max-width: 330px; + padding: 15px; + margin: 0 auto; +} +.form-signin .form-signin-heading, +.form-signin .form-control { + position: relative; + height: auto; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 10px; +} +.form-signin .form-control:focus { + z-index: 2; +} +.form-signin input[type="text"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.invalid-credentials { + border: 1px solid #ff0000; + background-color: rgba(255, 0, 0, 0.2); + color: #880000; + padding: 10px; + border-radius: 5px; + margin: 20px; +} diff --git a/delete.php b/delete.php index ec7e20e..c329919 100644 --- a/delete.php +++ b/delete.php @@ -1,13 +1,13 @@ del($key); } - die('?&s='.$server['id']); + die('?view&s='.$server['id'].'&d='.$server['db']); +} + +if (isset($_GET['batch_del'])) { + if (empty($_POST['selected_keys'])) { + die('No keys to delete'); + } + $keys = json_decode($_POST['selected_keys']); + + foreach ($keys as $key) { + $redis->del($key); + } + + die('?view&s=' . $server['id'] . '&d=' . $server['db'] . '&key=' . urlencode($keys[0])); } -?> diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ef83df --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + phpredisadmin: + build: . + environment: + - ADMIN_USER=admin + - ADMIN_PASS=admin + - REDIS_1_HOST=redis + - REDIS_1_PORT=6379 + links: + - redis + ports: + - "80:80" + + redis: + image: redis + command: --loglevel verbose diff --git a/edit.php b/edit.php index fade77c..998012c 100644 --- a/edit.php +++ b/edit.php @@ -2,8 +2,7 @@ require_once 'includes/common.inc.php'; - - +global $redis, $config, $csrfToken, $server; // Are we editing or creating a new key? $edit = false; @@ -27,70 +26,91 @@ die('ERROR: Your key is to long (max length is '.$config['maxkeylen'].')'); } - // String - if ($_POST['type'] == 'string') { - $redis->set(input_convert($_POST['key']), input_convert($_POST['value'])); + $key = input_convert($_POST['key']); + $value = input_convert($_POST['value']); + $value = encodeOrDecode('save', $key, $value); + + if ($value === false || is_null($value)) { + die('ERROR: could not encode 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(input_convert($_POST['key']), input_convert($_POST['hkey']))) { - $redis->hDel(input_convert($_POST['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(input_convert($_POST['key']), input_convert($_POST['hkey']), input_convert($_POST['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(input_convert($_POST['key'])); - - if (($_POST['index'] == '') || - ($_POST['index'] == $size) || - ($_POST['index'] == -1)) { - // Push it at the end - $redis->rPush(input_convert($_POST['key']), input_convert($_POST['value'])); - } else if (($_POST['index'] >= 0) && - ($_POST['index'] < $size)) { - // Overwrite an index - $redis->lSet(input_convert($_POST['key']), input_convert($_POST['index']), input_convert($_POST['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(input_convert($_POST['key']), input_convert($_POST['oldvalue'])); - $redis->sAdd(input_convert($_POST['key']), input_convert($_POST['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(input_convert($_POST['key']), input_convert($_POST['oldvalue'])); - $redis->zAdd(input_convert($_POST['key']), input_convert($_POST['score']), input_convert($_POST['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() ?>

+
+

-
+ +

@@ -147,12 +170,12 @@

-> +>

-> +>

@@ -172,13 +195,11 @@ -

-

+?> \ No newline at end of file diff --git a/export.php b/export.php index 69c15c4..d17839e 100644 --- a/export.php +++ b/export.php @@ -2,19 +2,24 @@ require_once 'includes/common.inc.php'; - - +global $redis, $config, $csrfToken, $server; // Export to redis-cli commands -function export_redis($key) { +function export_redis($key, $filter = false, $transform = false) { + global $redis; $type = $redis->type($key); + // we rename the keys as necessary + if($filter !== false && $transform !== false) + $outputKey = str_replace($filter, $transform, $key); + else + $outputKey = $key; // String if ($type == 'string') { - echo 'SET "',addslashes($key),'" "',addslashes($redis->get($key)),'"',PHP_EOL; + echo 'SET "',addslashes($outputKey),'" "',addslashes($redis->get($key)),'"',PHP_EOL; } // Hash @@ -22,7 +27,7 @@ function export_redis($key) { $values = $redis->hGetAll($key); foreach ($values as $k => $v) { - echo 'HSET "',addslashes($key),'" "',addslashes($k),'" "',addslashes($v),'"',PHP_EOL; + echo 'HSET "',addslashes($outputKey),'" "',addslashes($k),'" "',addslashes($v),'"',PHP_EOL; } } @@ -31,7 +36,7 @@ function export_redis($key) { $size = $redis->lLen($key); for ($i = 0; $i < $size; ++$i) { - echo 'RPUSH "',addslashes($key),'" "',addslashes($redis->lIndex($key, $i)),'"',PHP_EOL; + echo 'RPUSH "',addslashes($outputKey),'" "',addslashes($redis->lIndex($key, $i)),'"',PHP_EOL; } } @@ -40,7 +45,7 @@ function export_redis($key) { $values = $redis->sMembers($key); foreach ($values as $v) { - echo 'SADD "',addslashes($key),'" "',addslashes($v),'"',PHP_EOL; + echo 'SADD "',addslashes($outputKey),'" "',addslashes($v),'"',PHP_EOL; } } @@ -51,7 +56,7 @@ function export_redis($key) { foreach ($values as $v) { $s = $redis->zScore($key, $v); - echo 'ZADD "',addslashes($key),'" ',$s,' "',addslashes($v),'"',PHP_EOL; + echo 'ZADD "',addslashes($outputKey),'" ',$s,' "',addslashes($v),'"',PHP_EOL; } } } @@ -64,7 +69,6 @@ function export_json($key) { $type = $redis->type($key); - // String if ($type == 'string') { $value = $redis->get($key); @@ -116,9 +120,12 @@ function export_json($key) { header('Content-type: '.$ct.'; charset=utf-8'); header('Content-Disposition: inline; filename="export.'.$ext.'"'); + $filter = !empty($_POST['filter']) ? trim($_POST['filter']) : false; + $transform = !empty($_POST['transform']) ? trim($_POST['transform']) : false; // JSON if ($_POST['type'] == 'json') { + // Single key if (isset($_GET['key'])) { echo json_encode(export_json($_GET['key'])); @@ -127,7 +134,18 @@ function export_json($key) { $vals = array(); foreach ($keys as $key) { - $vals[$key] = export_json($key); + + // if we have a filter and no match, nothing to do + if($filter !== false && stripos($key, $filter) === false) + continue; + + // we rename the keys as necessary + if($filter !== false && $transform !== false) + $outputKey = str_replace($filter, $transform, $key); + else + $outputKey = $key; + + $vals[$outputKey] = export_json($key); } echo json_encode($vals); @@ -136,6 +154,7 @@ function export_json($key) { // Redis Commands else { + // Single key if (isset($_GET['key'])) { export_redis($_GET['key']); @@ -143,7 +162,12 @@ function export_json($key) { $keys = $redis->keys('*'); foreach ($keys as $key) { - export_redis($key); + + // if we have a filter and no match, we skip + if($filter !== false && stripos($key, $filter) === false) + continue; + + export_redis($key, $filter, $transform); } } } @@ -163,7 +187,8 @@ function export_json($key) { ?>

Export

-
+ +

@@ -173,9 +198,19 @@ function export_json($key) {

-

+ +

+ + +

+ +

+ + +

+ + -

flushdb(); diff --git a/import.php b/import.php index 5d74391..5e5a89e 100644 --- a/import.php +++ b/import.php @@ -1,9 +1,7 @@

Import

-
+ +

-

-

$v) { - unset($process[$key][$k]); - - if (is_array($v)) { - $process[$key][stripslashes($k)] = $v; - $process[] = &$process[$key][stripslashes($k)]; - } else { - $process[$key][stripslashes($k)] = stripslashes($v); - } - } + if (isset($_SESSION['phpredisadmin_csrf'])) { + $csrfToken = $_SESSION['phpredisadmin_csrf']; + } else { + $csrfToken = bin2hex(random_bytes(16)); + $_SESSION['phpredisadmin_csrf'] = $csrfToken; } - - unset($process); +} else { + $csrfToken = 'nosession'; } - +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if ($_POST['csrf'] !== $csrfToken) { + die('bad csrf token'); + } +} // These includes are needed by each script. @@ -40,27 +37,22 @@ } - - if (isset($login['servers'])) { $i = current($login['servers']); } else { $i = 0; } - if (isset($_GET['s']) && is_numeric($_GET['s']) && ($_GET['s'] < count($config['servers']))) { $i = $_GET['s']; } -$server = $config['servers'][$i]; -$server['id'] = $i; +$server = $config['servers'][$i]; +$server['id'] = $i; $server['charset'] = isset($server['charset']) && $server['charset'] ? $server['charset'] : false; - mb_internal_encoding('utf-8'); - if (isset($login, $login['servers'])) { if (array_search($i, $login['servers']) === false) { die('You are not allowed to access this database.'); @@ -75,9 +67,14 @@ if (!isset($server['db'])) { - $server['db'] = 0; + if (isset($_GET['d']) && is_numeric($_GET['d'])) { + $server['db'] = $_GET['d']; + } else { + $server['db'] = 0; + } } + if (!isset($server['filter'])) { $server['filter'] = '*'; } @@ -94,12 +91,55 @@ $server['seperator'] = $config['seperator']; } +if (!isset($server['keys'])) { + $server['keys'] = $config['keys']; +} + +if (!isset($server['scansize'])) { + if (isset($config['scansize'])) { + $server['scansize'] = $config['scansize']; + } else { + $server['scansize'] = 1000; + } +} + +if (!isset($server['scanmax'])) { + if (isset($config['scanmax'])) { + $server['scanmax'] = $config['scanmax']; + } else { + $server['scanmax'] = 0; + } +} + +if (!isset($server['serialization'])) { + if (isset($config['serialization'])) { + $server['serialization'] = $config['serialization']; + } +} + +if (!isset($config['hideEmptyDBs'])) { + $config['hideEmptyDBs'] = false; +} + +if (!isset($config['showEmptyNamespaceAsKey'])) { + $config['showEmptyNamespaceAsKey'] = false; +} + +if (!isset($server['scheme']) || empty($server['scheme'])) { + $server['scheme'] = 'tcp'; +} + // Setup a connection to Redis. -$redis = !$server['port'] ? new Predis\Client($server['host']) : new Predis\Client('tcp://'.$server['host'].':'.$server['port']); +if ($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($server['scheme'].'://'.$server['host'].':'.$server['port']); +} + try { $redis->connect(); } catch (Predis\CommunicationException $exception) { - $redis = false; + die('ERROR: ' . $exception->getMessage()); } if (isset($server['auth'])) { @@ -108,11 +148,12 @@ } } +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'])) { die('ERROR: Selecting database failed ('.$server['host'].':'.$server['port'].','.$server['db'].')'); } } - -?> diff --git a/includes/config.environment.inc.php b/includes/config.environment.inc.php new file mode 100644 index 0000000..06a06fe --- /dev/null +++ b/includes/config.environment.inc.php @@ -0,0 +1,86 @@ + array( + 'password' => $admin_pass, + ), + ); +} + +$i=1; +$config['servers'] = array(); + +while (true) { + + $prefix = 'REDIS_' . $i . '_'; + + $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 { + $server_auth = getenv($prefix . 'AUTH'); + } + $server_databases = getenv($prefix . 'DATABASES'); + + if (empty($server_host)) { + break; + } + + if (empty($server_name)) { + $server_name = $server_host; + } + + if (empty($server_auth)) { + $server_auth = ""; + } + + 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' => $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 b132bed..923a60c 100644 --- a/includes/config.sample.inc.php +++ b/includes/config.sample.inc.php @@ -8,6 +8,9 @@ 'host' => '127.0.0.1', 'port' => 6379, 'filter' => '*', + 'scheme' => 'tcp', // Optional. Connection scheme. 'tcp' - for TCP connection, 'unix' - for connection by unix domain socket + 'path' => '', // Optional. Path to unix domain socket. Uses only if 'scheme' => 'unix'. Example: '/var/run/redis/redis.sock' + 'hide' => false, // Optional. Override global setting. Hide empty databases in the database list. // Optional Redis authentication. //'auth' => 'redispasswordhere' // Warning: The password is sent in plain-text to the Redis server. @@ -23,16 +26,23 @@ 'host' => 'localhost', 'port' => 6379, 'db' => 1, // Optional database number, see http://redis.io/commands/select + 'databases' => 1, // Optional number of databases (prevents use of CONFIG command). 'filter' => 'something:*', // Show only parts of database for speed or security reasons. - 'seperator' => '/', // Use a different seperator on this database. + 'seperator' => '/', // Use a different seperator on this database (default uses config default). '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). + '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). ),*/ ), 'seperator' => ':', + 'showEmptyNamespaceAsKey' => false, + // Hide empty databases in the database list (global, valid for all servers unless set at server level) + 'hideEmptyDBs' => false, // Uncomment to show less information and make phpRedisAdmin fire less commands to the Redis server. Recommended for a really busy Redis server. //'faster' => true, @@ -51,11 +61,37 @@ ) ),*/ + // 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, + + + /*'serialization' => array( + 'foo*' => array( // Match like KEYS + // Function called when saving to redis. + 'save' => function($data) { return json_encode(json_decode($data)); }, + // Function called when loading from redis. + 'load' => function($data) { return json_encode(json_decode($data), JSON_PRETTY_PRINT); }, + ), + ),*/ + // You can ignore settings below this point. 'maxkeylen' => 100, - 'count_elements_page' => 100 -); + 'count_elements_page' => 100, + + // Use the old KEYS command instead of SCAN to fetch all keys. + 'keys' => false, -?> + // How many entries to fetch using each SCAN command. + 'scansize' => 1000, + + // The total number of entries to fetch. Set to 0 or -1 for no limit. + 'scanmax' => 0 +); diff --git a/includes/functions.inc.php b/includes/functions.inc.php index 22e9253..589d324 100644 --- a/includes/functions.inc.php +++ b/includes/functions.inc.php @@ -16,6 +16,8 @@ function format_html($str) { function input_convert($str) { + global $server; + if (isset($server['charset']) && $server['charset']) { return mb_convert_encoding($str, $server['charset'], 'utf-8'); } else { @@ -24,39 +26,30 @@ function input_convert($str) { } -function format_ago($time, $ago = false) { +function format_time($time) { $minute = 60; $hour = $minute * 60; $day = $hour * 24; $when = $time; - if ($when >= 0) - $suffix = 'ago'; - else { - $when = -$when; - $suffix = 'in the future'; - } - if ($when > $day) { - $when = round($when / $day); - $what = 'day'; + $tmpday = floor($when / $day); + $tmphour = floor(($when / $hour) - (24*$tmpday)); + $tmpminute = floor(($when / $minute) - (24*60*$tmpday) - ($tmphour * 60)); + $tmpsec = floor($when - (24*60*60*$tmpday) - ($tmphour * 60 * 60) - ($tmpminute * 60)); + return sprintf("%d day%s %d hour%s %d min%s %d sec%s",$tmpday,($tmpday != 1) ? 's' : '',$tmphour,($tmphour != 1) ? 's' : '',$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); } else if ($when > $hour) { - $when = round($when / $hour); - $what = 'hour'; + $tmphour = floor($when / $hour); + $tmpminute = floor(($when / $minute) - ($tmphour * 60)); + $tmpsec = floor($when - ($tmphour * 60 * 60) - ($tmpminute * 60)); + return sprintf("%d hour%s %d min%s %d sec%s",$tmphour,($tmphour != 1) ? 's' : '',$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); } else if ($when > $minute) { - $when = round($when / $minute); - $what = 'minute'; + $tmpminute = floor($when / $minute); + $tmpsec = floor($when - ($tmpminute * 60)); + return sprintf("%d min%s %d sec%s",$tmpminute,($tmpminute != 1) ? 's' : '',$tmpsec,($tmpsec != 1) ? 's' : ''); } else { - $what = 'second'; - } - - if ($when != 1) $what .= 's'; - - if ($ago) { - return "$when $what $suffix"; - } else { - return "$when $what"; + return sprintf("%d sec%s",$when,($when != 1) ? 's' : ''); } } @@ -72,6 +65,15 @@ function format_size($size) { } +function format_ttl($seconds) { + if ($seconds > 60) { + return sprintf('%d (%s)', $seconds, format_time($seconds)); + } else { + return $seconds; + } +} + + function str_rand($length) { $r = ''; @@ -82,3 +84,23 @@ function str_rand($length) { return $r; } + +function encodeOrDecode($action, $key, $data) { + global $server; + + if (isset($_GET['raw']) || !isset($server['serialization'])) { + return $data; + } + + foreach ($server['serialization'] as $pattern => $closures) { + if (fnmatch($pattern, $key)) { + return $closures[$action]($data); + } + } + + return $data; +} + +function getRelativePath($base) { + return substr($_SERVER['REQUEST_URI'], strpos($_SERVER['REQUEST_URI'], $base)); +} diff --git a/includes/header.inc.php b/includes/header.inc.php index aa618f1..7679bd6 100644 --- a/includes/header.inc.php +++ b/includes/header.inc.php @@ -4,6 +4,7 @@ header('Content-Type: text/html; charset=utf-8'); header('Cache-Control: private'); +header('X-Frame-Options: sameorigin'); ?> @@ -35,5 +36,9 @@ + + diff --git a/includes/login.inc.php b/includes/login.inc.php index 633bd94..8cfb26d 100644 --- a/includes/login.inc.php +++ b/includes/login.inc.php @@ -1,63 +1,144 @@ 1, + 'nc' => 1, + 'cnonce' => 1, + 'qop' => 1, + 'username' => 1, + 'uri' => 1, + 'response' => 1 + ); -$needed_parts = array( - 'nonce' => 1, - 'nc' => 1, - 'cnonce' => 1, - 'qop' => 1, - 'username' => 1, - 'uri' => 1, - 'response' => 1 - ); + $data = array(); + $keys = implode('|', array_keys($needed_parts)); -$data = array(); -$keys = implode('|', array_keys($needed_parts)); + preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); -preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $data[$m[1]] = $m[3] ? $m[3] : $m[4]; + unset($needed_parts[$m[1]]); + } -foreach ($matches as $m) { - $data[$m[1]] = $m[3] ? $m[3] : $m[4]; - unset($needed_parts[$m[1]]); -} + if (!empty($needed_parts)) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die; + } -if (!empty($needed_parts)) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die; -} + if (!isset($config['login'][$data['username']])) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die('Invalid username and/or password combination.'); + } -if (!isset($config['login'][$data['username']])) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die('Invalid username and/or password combination.'); -} + $login = $config['login'][$data['username']]; + $login['name'] = $data['username']; -$login = $config['login'][$data['username']]; -$login['name'] = $data['username']; + $password = md5($login['name'].':'.$realm.':'.$login['password']); -$password = md5($login['name'].':'.$realm.':'.$login['password']); + $response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri'])); -$response = md5($password.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.md5($_SERVER['REQUEST_METHOD'].':'.$data['uri'])); + if ($data['response'] !== $response) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); + die('Invalid username and/or password combination.'); + } + + return $login; +} + +// Perform auth using a standard HTML
submission and cookies to save login state +function authCookie() +{ + global $config; + + $generateCookieHash = function($username) use ($config) { + if (!isset($config['login'][$username])) { + throw new \RuntimeException("Invalid username"); + } + + // Storing this value client-side so we need to be careful that it + // doesn't reveal anything nor can be guessed. + // Using SHA512 because MD5, SHA1 are both now considered broken + return hash( + 'sha512', + implode(':', array( + $username, + $_SERVER['HTTP_USER_AGENT'], + $_SERVER['REMOTE_ADDR'], + $config['login'][$username]['password'], + )) + ); + }; + + if (!empty($_COOKIE['phpRedisAdminLogin'])) { + // We have a cookie; is it correct? + // Cookie value looks like "username:password-hash" + $cookieVal = explode(':', $_COOKIE['phpRedisAdminLogin']); + if (count($cookieVal) === 2) { + list($username, $cookieHash) = $cookieVal; + if (isset($config['login'][$username])) { + $userData = $config['login'][$username]; + $expectedHash = $generateCookieHash($username); + + if ($cookieHash === $expectedHash) { + // Correct username & password + return $userData; + } + } + } + } + + if (isset($_POST['username'], $_POST['password'])) { + // Login form submitted; correctly? + if (isset($config['login'][$_POST['username']])) { + $userData = $config['login'][$_POST['username']]; + if ($_POST['password'] === $userData['password']) { + // Correct username & password. Set cookie and redirect to home page + $cookieValue = $_POST['username'] . ':' . $generateCookieHash($_POST['username']); + setcookie('phpRedisAdminLogin', $cookieValue); + + // This should be an absolute URL, but that's a bit of a pain to generate; this will work + header("Location: index.php"); + die(); + } + } + } + + // 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 ($data['response'] != $response) { - header('HTTP/1.1 401 Unauthorized'); - header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.$opaque.'"'); - die('Invalid username and/or password combination.'); +if (!empty($config['cookie_auth'])) { + $login = authCookie(); +} else { + $login = authHttpDigest(); } ?> 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/includes/page.inc.php b/includes/page.inc.php index 2f1a0e9..2941e68 100644 --- a/includes/page.inc.php +++ b/includes/page.inc.php @@ -1,6 +1,5 @@ array('common'), 'js' => array('jquery') diff --git a/index.php b/index.php index b768ecb..ea509c7 100644 --- a/index.php +++ b/index.php @@ -1,10 +1,27 @@ keys($server['filter']); + if (!empty($server['keys'])) { + $keys = $redis->keys($server['filter']); + } 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; + } + } + } sort($keys); @@ -17,7 +34,11 @@ continue; } - $key = explode($server['seperator'], $key); + $key = explode($server['seperator'], $key); //@todo: may be separator ? + if ($config['showEmptyNamespaceAsKey'] && $key[count($key) - 1] == '') { + array_pop($key); + $key[count($key) - 1] .= ':'; + } // $d will be a reference to the current namespace. $d = &$namespaces; @@ -61,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; @@ -80,10 +107,15 @@ function print_namespace($item, $name, $fullkey, $islast) { } } + if (empty($name) && $name != '0') { + $name = ''; + $class[] = 'empty'; + } ?> > - () + + ()
  •  () - [X] + [X]
      + +
      +
      - resetStat(); +if (isset($_GET['reset'])) { + $redis->config('resetstat'); header('Location: info.php'); die; } - - // Fetch the info $info = $redis->info(); $alt = false; - - - $page['css'][] = 'frame'; $page['js'][] = 'frame'; @@ -29,11 +22,9 @@ ?>

      Info

      -

      -Reset usage statistics +Reset usage statistics

      - @@ -45,7 +36,7 @@ } ?> - > + >= t.offset().top) && @@ -61,17 +126,9 @@ $(function() { } }); - $('a').click(function() { - $('li.current').removeClass('current'); - }); - - $('li a').click(function() { - $(this).parent().addClass('current'); - }); - $('#btn_server_filter').click(function() { var filter = $('#server_filter').val(); - location.href = top.location.pathname + '?overview&s=' + $('#server').val() + '&filter=' + filter; + location.href = top.location.pathname + '?overview&s=' + $('#server').val() + '&d=' + ($('#database').val() || '') + '&filter=' + filter; }); $('#server_filter').keydown(function(e){ @@ -107,20 +164,46 @@ $(function() { }); }); - $('.deltree').click(function(e) { + var isResizing = false; + var lastDownX = 0; + var lastWidth = 0; + + var resizeSidebar = function(w) { + $('#sidebar').css('width', w); + $('#keys').css('width', w); + $('#resize').css('left', w + 10); + $('#resize-layover').css('left', w + 15); + $('#frame').css('left', w + 15); + }; + + if (parseInt($.cookie('sidebar')) > 0) { + resizeSidebar(parseInt($.cookie('sidebar'))); + } + + $('#resize').on('mousedown', function (e) { + isResizing = true; + lastDownX = e.clientX; + lastWidth = $('#sidebar').width(); + $('#resize-layover').css('z-index', 1000); e.preventDefault(); + }); + $(document).on('mousemove', function (e) { + if (!isResizing) { + return; + } - if (confirm('Are you sure you want to delete this whole tree and all it\'s keys?')) { - $.ajax({ - type: "POST", - url: this.href, - data: 'post=1', - success: function(url) { - top.location.href = top.location.pathname+url; - } - }); + var w = lastWidth - (lastDownX - e.clientX); + if (w < 250 ) { + w = 250; + } else if (w > 1000) { + w = 1000; } - }); + resizeSidebar(w); + $.cookie('sidebar', w); + }).on('mouseup', function (e) { + isResizing = false; + $('#resize-layover').css('z-index', 0); + }); }); diff --git a/js/jquery-cookie.js b/js/jquery-cookie.js new file mode 100644 index 0000000..29769eb --- /dev/null +++ b/js/jquery-cookie.js @@ -0,0 +1,114 @@ +/*! + * jQuery Cookie Plugin v1.4.1 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2006, 2014 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD (Register as an anonymous module) + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + // If we can't parse the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return $.isFunction(converter) ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + + if (arguments.length > 1 && !$.isFunction(value)) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setMilliseconds(t.getMilliseconds() + days * 864e+5); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}, + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + cookies = document.cookie ? document.cookie.split('; ') : [], + i = 0, + l = cookies.length; + + for (; i < l; i++) { + var parts = cookies[i].split('='), + name = decode(parts.shift()), + cookie = parts.join('='); + + if (key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = {}; + + $.removeCookie = function (key, options) { + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return !$.cookie(key); + }; + +})); diff --git a/login.php b/login.php new file mode 100644 index 0000000..5c12301 --- /dev/null +++ b/login.php @@ -0,0 +1,45 @@ + + +

      phpRedisAdmin

      + + + + + + +
      +

      Invalid username/password

      +

      Please try again.

      +
      + + + + > + + + > + + + + + diff --git a/logout.php b/logout.php index 2d6aca8..07e10ae 100644 --- a/logout.php +++ b/logout.php @@ -1,40 +1,56 @@ 1, - 'nc' => 1, - 'cnonce' => 1, - 'qop' => 1, - 'username' => 1, - 'uri' => 1, - 'response' => 1 - ); - -$data = array(); -$keys = implode('|', array_keys($needed_parts)); - -preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); - -foreach ($matches as $m) { - $data[$m[1]] = $m[3] ? $m[3] : $m[4]; - unset($needed_parts[$m[1]]); -} - - -if (!isset($_GET['nonce'])) { - header('Location: logout.php?nonce='.$data['nonce']); - die; +require_once 'includes/common.inc.php'; +global $redis, $config, $csrfToken, $server; + +if (!empty($config['cookie_auth'])) { + // Cookie-based auth + 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( + 'nonce' => 1, + 'nc' => 1, + 'cnonce' => 1, + 'qop' => 1, + 'username' => 1, + 'uri' => 1, + 'response' => 1 + ); + + $data = array(); + $keys = implode('|', array_keys($needed_parts)); + + preg_match_all('/('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))/', $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[3] ? $m[3] : $m[4]; + unset($needed_parts[$m[1]]); + } + + + if (!isset($_GET['nonce'])) { + header('Location: logout.php?nonce='.$data['nonce']); + die; + } + + + if ($data['nonce'] == $_GET['nonce']) { + unset($_SERVER['PHP_AUTH_DIGEST']); + + if (!empty($config['cookie_auth'])) { + $login = authCookie(); + } else { + $login = authHttpDigest(); + } + } + + + header('Location: logout.php'); } - - -if ($data['nonce'] == $_GET['nonce']) { - unset($_SERVER['PHP_AUTH_DIGEST']); - - require 'includes/login.inc.php'; -} - - -header('Location: '.substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'logout.php'))); - -?> diff --git a/overview.php b/overview.php index caeb2bb..5c67370 100644 --- a/overview.php +++ b/overview.php @@ -1,9 +1,7 @@ connect(); - } catch (Predis\CommunicationException $exception) { - $redis = false; + + if (isset($config['login_as_acl_auth'])) { + // Currently only support one server at a time + if ($i > 0) { + break; + } + } else { + // 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) { @@ -36,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( @@ -79,9 +92,25 @@ - - - + + + + + + +
      Key
      Value
      Memory used:
      Uptime:
      Last save:
      [S]
      Uptime:
      Username:
      Last save:
      + = 0) { + echo format_time(time() - $info[$i]['Persistence']['rdb_last_save_time']) . " ago"; + } else { + echo format_time(-(time() - $info[$i]['Persistence']['rdb_last_save_time'])) . "in the future"; + } + } else { + echo 'never'; + } + ?> + [S]
      @@ -93,7 +122,7 @@

      -Redis Documentation +Redis Documentation

      $config['maxkeylen']) { @@ -18,7 +16,7 @@ ?>

      Edit Name of

      -
      + + @@ -44,9 +43,7 @@ >

      -

      -

      Edit TTL

      -
      + +

      @@ -35,12 +31,10 @@

      -> (-1 to remove the TTL) +> (-1 to remove the TTL)

      -

      -

      Invalid key @@ -18,23 +17,29 @@ 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() ?>

      +
      +

      - [R] - [X] - [E] + [R] + [X] + [E]

      ttl($_GET['key']); @@ -59,16 +62,20 @@ $encoding = null; } - switch ($type) { case 'string': $value = $redis->get($_GET['key']); + $value = encodeOrDecode('load', $_GET['key'], $value); $size = strlen($value); break; case 'hash': $values = $redis->hGetAll($_GET['key']); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); + ksort($values); break; case 'list': @@ -77,13 +84,23 @@ case 'set': $values = $redis->sMembers($_GET['key']); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); + sort($values); break; case 'zset': $values = $redis->zRange($_GET['key'], 0, -1); - $size = count($values); + foreach ($values as $k => $value) { + $values[$k] = encodeOrDecode('load', $_GET['key'], $value); + } + $size = count($values); break; + + default: + $size = -1; } if (isset($values) && ($count_elements_page !== false)) { @@ -95,13 +112,25 @@
      Type:
      -
      TTL:
      [E]
      +
      TTL:
      [E]
      Encoding:
      -
      Size:
      +
      Size:
      + +
      @@ -172,10 +201,10 @@ if ($type == 'string') { ?> -
      - [E] +
      + [E]
      - [X] + [X]
      @@ -190,10 +219,10 @@
      Key
      Value
       
       
      $value) { ?> - >
      - [E] + >
      + [E]
      - [X] + [X]
      @@ -217,11 +246,12 @@ for ($i = $start; $i < $end; ++$i) { $value = $redis->lIndex($_GET['key'], $i); + $value = encodeOrDecode('load', $_GET['key'], $value); ?> - >
      - [E] + >
      + [E]
      - [X] + [X]
      @@ -237,12 +267,12 @@
      Value
       
       
      exists($value) ? ''.nl2br(format_html($value)).'' : nl2br(format_html($value)); + $display_value = $redis->exists($value) ? ''.format_html($value).'' : format_html($value); ?> - >
      - [E] + >
      + [E]
      - [X] + [X]
      @@ -258,11 +288,11 @@ zScore($_GET['key'], $value); - $display_value = $redis->exists($value) ? ''.nl2br(format_html($value)).'' : nl2br(format_html($value)); + $display_value = $redis->exists($value) ? ''.format_html($value).'' : format_html($value); ?> - >
      - [E] - [X] + >
      + [E] + [X]
      @@ -272,7 +302,7 @@

      - Add another value + Add another value