├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .dockerignore ├── .github ├── ct.yml ├── dependabot.yml ├── helm-docs.sh ├── kubeval.sh └── workflows │ ├── chart-test.yml │ ├── dependency-checks.yml │ └── docker-publish.yml ├── .gitignore ├── .s2i └── bin │ └── assemble ├── .snyk ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── bin ├── bcrypt-password.js ├── healthcheck.js └── redis-commander.js ├── config ├── custom-environment-variables.json └── default.json ├── dist ├── debian │ └── init.d │ │ └── redis-commander ├── pm2 │ ├── pm2-config.json.template │ └── readme.md └── systemd │ ├── readme.md │ └── redis-commander.service ├── docker-compose.yml ├── docker ├── entrypoint.sh └── harden.sh ├── docs ├── GUI_EXAMPLE.png ├── adding_datatypes.md ├── configuration.md ├── connections.md └── security_checks.md ├── ecosystem.config.js ├── k8s ├── helm-chart │ ├── README.md │ ├── example-values-as-json.yaml │ ├── example-values-as-yml.yaml │ ├── redis-commander │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── README.md.gotmpl │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _capabilities.tpl │ │ │ ├── _helpers.tpl │ │ │ ├── configmap.yaml │ │ │ ├── deployment.yaml │ │ │ ├── hpa.yaml │ │ │ ├── ingress-legacy.yaml │ │ │ ├── ingress.yaml │ │ │ ├── service.yaml │ │ │ ├── serviceaccount.yaml │ │ │ ├── tests │ │ │ │ └── test-connection.yaml │ │ │ └── virtual-service.yaml │ │ ├── values.schema.json │ │ └── values.yaml │ └── release_charts ├── redis-commander │ ├── deployment-password-protected-redis.yaml │ ├── deployment.yaml │ ├── ingress.yml │ └── service.yaml └── redis │ ├── deployment-with-password.yaml │ ├── deployment.yaml │ └── service.yaml ├── lib ├── app.js ├── connections.js ├── express │ └── middlewares.js ├── ioredis-stream.js ├── redisCommands │ └── redisCore.js ├── routes │ ├── apiv1.js │ ├── home.js │ ├── index.js │ └── tools.js └── util.js ├── package-lock.json ├── package.json ├── sbom.json ├── test └── testUtil.js └── web ├── static ├── bootstrap │ ├── css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── css │ └── default.css ├── favicon.png ├── healthcheck ├── images │ ├── RedisCommandLogo.png │ ├── icon-download.png │ ├── icon-edit.png │ ├── icon-info.png │ ├── icon-plus.png │ ├── icon-refresh.png │ ├── icon-trash.png │ ├── treeBinary.png │ ├── treeHash.png │ ├── treeJson.png │ ├── treeList.png │ ├── treeRoot.png │ ├── treeRootDisconnect.png │ ├── treeSet.png │ ├── treeStream.png │ ├── treeString.png │ └── treeZSet.png ├── scripts │ ├── binaryView.js │ ├── browserify.js │ ├── jquery-2.2.4.js │ ├── jquery.resize.min.js │ └── redisCommander.js └── templates │ ├── connectionsBar.ejs │ ├── detectRedisDb.ejs │ ├── editBinary.ejs │ ├── editBranch.ejs │ ├── editHash.ejs │ ├── editList.ejs │ ├── editReJSON.ejs │ ├── editSet.ejs │ ├── editStream.ejs │ ├── editString.ejs │ ├── editZSet.ejs │ └── serverInfo.ejs └── views ├── home ├── home-sso.ejs └── home.ejs ├── layout.ejs ├── modals ├── addHashFieldModal.ejs ├── addKeyModal.ejs ├── addListValueModal.ejs ├── addSetMemberModal.ejs ├── addXSetMemberModal.ejs ├── addZSetMemberModal.ejs ├── editHashFieldModal.ejs ├── editListValueModal.ejs ├── editSetMemberModal.ejs ├── editZSetMemberModal.ejs ├── renameKeyModal.ejs └── signinModal.ejs └── tools ├── exportData.ejs └── importData.ejs /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:22 2 | 3 | RUN apt-get install -y lsb-release curl gpg 4 | RUN <> $GITHUB_OUTPUT 108 | echo "docker_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT 109 | 110 | 111 | - name: Show docker image tags to build 112 | run: | 113 | echo "Docker image: ${{ steps.prep.outputs.docker_image }}" 114 | echo "Image tags: ${{ steps.prep.outputs.tags }}" 115 | 116 | - name: Docker build and push to GHCR and Dockerhub for prepared tags 117 | uses: docker/build-push-action@v6 118 | with: 119 | builder: ${{ steps.buildx.outputs.name }} 120 | context: . 121 | platforms: linux/amd64,linux/arm/v7,linux/arm64 122 | push: true 123 | tags: ${{ steps.prep.outputs.tags }} 124 | # platform linux/riscv64 starts with alpine:edge, not available on 3.15 by now 125 | # see "docker manifest inspect alpine:3.15 | alpine:edge 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules/ 3 | *.rdb 4 | *.iml 5 | /config/local.json 6 | /config/local-*.json 7 | .DS_Store 8 | /docs/coverage/ 9 | .nyc_output/ 10 | /test/test-ca/ 11 | /test/local/ 12 | 13 | # files pulled by actions, no check in 14 | helm-docs 15 | .github/kubeval 16 | k8s/helm-chart/redis-commander/example.values.yaml 17 | -------------------------------------------------------------------------------- /.s2i/bin/assemble: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # The assemble script builds the application artifacts from a source and 3 | # places them into appropriate directories inside the image. 4 | 5 | 6 | # Add a start script to package.json - relies on existence of environment variables 7 | echo 'Adding start script to package.json using REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD env vars...' 8 | sed -i 's?"healthcheck?"start": "node ./bin/redis-commander --redis-port ${REDIS_PORT} --redis-host ${REDIS_HOST} --redis-password ${REDIS_PASSWORD} --port 8080",\n "healthcheck?' /tmp/src/package.json 9 | 10 | # Execute the default S2I script 11 | source ${STI_SCRIPTS_PATH}/assemble 12 | 13 | # You can write S2I scripts in any programming language, as long as the 14 | # scripts are executable inside the builder image. 15 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.25.0 3 | ignore: 4 | SNYK-CC-K8S-8: 5 | - 'k8s/redis/deployment.yaml > *': 6 | reason: Redis server needs rw filesystem for db snapshots 7 | created: 2023-05-17T09:21:37.429Z 8 | - 'k8s/redis/deployment-with-password.yaml > *': 9 | reason: Redis server needs rw filesystem for db snapshots 10 | created: 2023-05-17T09:21:37.429Z 11 | - 'k8s/redis-commander/deployment.yaml > *': 12 | reason: Redis-Commander rw filesystem needed to create config file from connection env vars 13 | created: 2023-05-17T09:21:37.429Z 14 | - 'k8s/redis-commander/deployment-password-protected-redis.yaml > *': 15 | reason: Redis-Commander rw filesystem needed to create config file from connection env vars 16 | created: 2023-05-17T09:21:37.429Z 17 | SNYK-CC-K8S-41: 18 | - 'k8s/redis/deployment.yaml > *': 19 | reason: Example files not meant for production 20 | created: 2023-05-17T09:21:37.429Z 21 | - 'k8s/redis/deployment-with-password.yaml > *': 22 | reason: Example files not meant for production 23 | created: 2023-05-17T09:21:37.429Z 24 | SNYK-CC-K8S-42: 25 | - 'k8s/redis/deployment.yaml > *': 26 | reason: Example files not meant for production 27 | created: 2023-05-17T09:21:37.429Z 28 | - 'k8s/redis/deployment-with-password.yaml > *': 29 | reason: Example files not meant for production 30 | created: 2023-05-17T09:21:37.429Z 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Redis-Commander CHANGELOG 2 | 3 | ## Next Version 4 | #### Bugfixes 5 | * update express from 4.21.1 to 4.21.2 (fix CVE-2024-52798) 6 | * update cross-spawn from 7.0.3 to 7.0.6 (fix CVE-2024-21538) 7 | * update base image to Alpine@3.21 and Node.js@22 8 | 9 | #### Enhancements 10 | * update ioredis from 5.4.1 to 5.5.0 11 | 12 | ## Version 0.9.0 13 | #### Bugfixes 14 | * update jsonwebtoken from 8.5.1 to 9.0.0 (fix CVE-2022-23529, CVE-2022-23541, CVE-2022-23539, CVE-2022-23540) 15 | * update json5 from 2.2.1 to 2.2.3 (fix CVE-2022-46175) 16 | * update cmdparser from 0.0.3 to 0.1.0 (fix CVE-2021-43138), #517 17 | * partial update of semver to 7.5.4 (fix CVE-2022-2588) 18 | * update @babel/traverse from 7.22.5 to 7.23.3 (fix CVE-2023-45133) 19 | * update browserify-sign from 4.2.1 to 4.2.2 (fix CVE-2023-46234) 20 | * update elliptic from 6.5.4 to 6.6.0 (fix CVE-2024-42459, CVE-2024-42460, CVE-2024-42461, CVE-2024-48948) 21 | * update cookie from 0.6.0 to 0.7.1 (fix CVE-2024-47764) 22 | 23 | #### Enhancements 24 | * allow using IPv6 addresses for Redis connection definitions. (except REDIS_HOSTS env var, here no IPv6 allowed, use host names instead) 25 | * allow setting a custom HTTP header name used for the JWT session authentication token 26 | * add option to overwrite global folding character per connection 27 | * add Sentinel TLS connections and improved configuration and env var handling for TLS secured connections, #514 28 | * add Redis Cluster support, #160 #216 #527 29 | * allow defining additional commands as safe for read-only mode, defaulting to "info" and "select", #542 30 | * update base image to alpine@3.17 using NodeJS 18.x now, #511 31 | * update helm chart autoscaling apis for newer K8s versions, #520 32 | * update helm chart to allow setting ingressClassName for newer K8s versions, #494 33 | * update UI for better visibility on how to close redis commands modal, #456 34 | * update ioredis from 4.28.5 to 5.4.1 35 | * update dependencies yargs@17.7.2, ejs@3.1.10, jstree@3.3.17, config@3.3.12, body-parser@1.20.3 36 | * update "@cyclonedx/cyclonedx-npm"@1.19.3 37 | * improve password login check and prevent timing attacks on username check 38 | 39 | ## Version 0.8.1 40 | #### Bugfixes 41 | * fix text not copied when in json view mode 42 | * fix display of big integer and float numbers in json view, #479 43 | * update to alpine:3.16 as base image, #495 44 | * update shell-quote from 1.7.2 to 1.7.3 (fix CVE-2021-42740) 45 | #### Enhancements 46 | * display ReJSON data as formatted json with support for big numbers and floats, #478 47 | * add editing ReJSON data, #452 48 | * update dependencies to fix security vulnerabilities in ansi-regex@5.0.1, filelist@1.0.4, minimatch@3.1.2, shell-quote@1.7.3 49 | * update dependencies express@4.18.2, body-parser@1.20.1, ejs@3.1.8, async@3.2.4, clipboard@2.0.11, yargs@17.6.0, inflection@1.13.4. config@3.3.8 50 | * improve documentation, #498 #500 #506 51 | * add software bill of material in CycloneDX format 52 | 53 | ## Version 0.8.0 54 | #### Bugfixes 55 | #### Enhancements 56 | * update dependencies to fix security vulnerabilities in minimist, json-viewer, async, config, clipboard 57 | * make url path of signin route configurable (config file and env var), #467 58 | * add redis username and sentinel username support, #476 59 | * update helm chart to allow setting redis username 60 | * fix json display of big numbers not fitting into javascript "number" type, #400 61 | 62 | ## Version 0.7.3 63 | #### Bugfixes 64 | #### Enhancements 65 | * minimum node version supported 12.x 66 | * update ejs from 2.7.4 to 3.1.6 67 | * update dependencies to fix vulnerabilities in async, tar, yargs, async, ejs, cached-path-relative 68 | * add new import/export function with redis DUMP command and base64 encoded content to work around problems with 69 | * update base image to Alpine 3.15 with NodeJS 16 70 | 71 | ## Version 0.7.2 72 | #### Bugfixes 73 | #### Enhancements 74 | * update dependencies to fix vulnerabilities in async, tar, yargs, async, ejs, cached-path-relative 75 | * update documentation regarding command line params and environment variables 76 | * update kubernetes examples for seccomp/apparmor profile and not mounting service account token 77 | * update helm chart for service accounts and account token mount 78 | multi-line redis values or some special data types and binary values 79 | 80 | ## Version 0.7.2 81 | #### Bugfixes 82 | #### Enhancements 83 | * check for jwt token algorithms used to reject "none" algorithm 84 | * update dependencies to fix vulnerabilities in elliptic and some other 85 | * add helper script to generated bcrypt password hash and allow setting http auth password hash from file inside docker, #434 86 | * update base image to alpine:3.12 87 | * switch from node-redis-dump to node-redis-dump2 and remove now obsolete docker build patch 88 | 89 | ## Version 0.7.1 90 | #### Bugfixes 91 | * update handling of big numbers displayed as json formatted values. For big numbers wrong values may be shown, #400 92 | * increase width of cli input to use full width available, #404 93 | * fix problem not setting sentinel password from command line, #416 94 | * fix missing quotes for keys with a backslash, #421 95 | * fix possible bug comparing sentinel connections 96 | * block "monitor" at cli to not block redis connections, #424 97 | * fix bug showing additional white spaces in edit hash popup, #426 98 | * fix bug wih config validation for boolean values 99 | * validate urlPrefix config param given for correct use of slashes (start+trailing), #419 100 | 101 | #### Enhancements 102 | * Adding maxHashFieldSize config to limit the size of hash fields, #409 (chrisregnier) 103 | * set user in Dockerfile as numeric value to allow Kubernetes to enforce non-root user 104 | * update Kubernetes examples with security settings for Redis Commander 105 | * add config examples for starting Redis Commander with SystemD or PM2, #158 106 | * allow flagging redis connection as optional, if true no permanant auto-reconnect is tried if server is down, reconnection done on request only, #230 107 | * add basic helm chart for k8s installation, based on PR by @aabdennour, #412 108 | * allow partial export of redis data 109 | * add function to rename existing keys, #378 110 | * update dependencies to fix vulnerabilities in multiple packages 111 | * better handle special chars and spaces inside env vars given to docker container 112 | 113 | ## Version 0.7.0 114 | #### Bugfixes 115 | * fix error on Windows on getting package installation path, #388 116 | * fix wrong connection info data shown on import and export page (sentinel and sockets) 117 | 118 | #### Enhancements 119 | * update dependencies to fix vulnerabilities in multiple packages 120 | * change deprecated package "optimist" to "yargs" to fix prototype pollution in dependent minimist package 121 | * add new route /sso to login with signed Json Web Token from external apps with a PSK 122 | 123 | #### Breaking Change 124 | * Base image changed from end-of-life Node-8 to pure Alpine 3.11, booth package managers (npm and yarn) 125 | are available but installed as system package now under different path (`/usr/bin`). 126 | This change is relevant only when this image is used as base image for other container. 127 | 128 | ## Version 0.6.7 129 | #### Bugfixes 130 | * do not display content of passwords read from env var or file on docker startup, #372 131 | * fix display errors on early display of import/export page 132 | * dependency updates for security fixes (elliptic) and change runtime umask to 027 133 | * fix problem with sentinel connections without explict group name given, #381 134 | * fix problem not showing all nodes after refresh (menu entry), #382 135 | 136 | #### Enhancements 137 | * add new docker env vars to load passwords from file (REDIS_PASSWORD_FILE, SENTINEL_PASSWORD_FILE), #364 138 | * add docker image HEALTHCHECK command 139 | * add basic support to display redis string values as hex values, #140 140 | * add basic support to display ReJSON type data, #371 141 | * switch library to display json objects from "json-tree" to "jquery.json-viewer", #375 142 | * add config value and env var to display valid json data as default as formatted json tree object (VIEW_JSON_DEFAULT), #375 143 | * add config value and env var to disable display of strings as hexadecimal binary data (BINARY_AS_HEX), #376 144 | * add basic validation to redis connection params given via command line and config files, #377 145 | * allow docker image security scanner to work even if apk related files are removed 146 | * add json formatted view to List, Set and SortedSet elements too 147 | 148 | ## Version 0.6.6 149 | #### Bugfixes 150 | * fix display bug for keys starting with configured foldingchar, #355 151 | * fix bug where cli params do not overwrite other config params, #354 152 | * fix handling of some special chars inside env vars at docker startup script 153 | * disable MULTI command via redis cli to prevent crashes, #358 154 | * fix double html encoding of key data, #362 155 | 156 | #### Enhancements 157 | * dependencies updated to fix security problems 158 | * add valid url on startup to access redis commander via browser 159 | * improve console log message for redis connection errors 160 | * add dialog for auto-detection of used redis databases, #121 161 | * change api content-type of methods to "application/json" and move arrays returned down into json object "data" property 162 | 163 | ## Version 0.6.5 164 | #### Bugfixes 165 | * fix display of keys having multiple consecutive folding chars, #342 166 | * fix connection id handling for node >= 10.x, #270 167 | * fix setting initial ui.locked, cliOpen and height from config file 168 | 169 | #### Enhancements 170 | * add redis stream support (display, add, delete), thanks to Adrian Oanca and vflopes 171 | * fix redis sentinel connection handling and make it configurable via config file too 172 | * allow configuration of max allowed request body size via env var or config file, #352 173 | * add json view to hash sets 174 | * improve logging if run behind http reverse proxy like nginx, add config setting and env var, #348 175 | * some ui improvements 176 | * some dependencies updated to fix security problems 177 | * improve documentation 178 | 179 | ## Version 0.6.4 180 | #### Bugfixes 181 | * fix redis connections via unix sockets, #270 182 | * build redis server command list dynamically to allow usage of all new redis commands via cli (read-write and read-only mode), #210 183 | #### Enhancements 184 | * some ui improvements 185 | * some dependencies updated to fix security problems 186 | 187 | ## Version 0.6.3 188 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | WORKDIR /redis-commander 4 | 5 | # optional build arg to let the hardening process remove all package manager (apk, npm, yarn) too to not allow 6 | # installation of packages anymore, default: do not remove "apk" to allow others to use this as a base image 7 | # for own images 8 | ARG REMOVE_APK=0 9 | 10 | ENV SERVICE_USER=redis 11 | ENV HOME=/redis-commander 12 | ENV NODE_ENV=production 13 | 14 | # only single copy command for most parts as other files are ignored via .dockerignore 15 | # to create less layers 16 | COPY . . 17 | 18 | # for Openshift compatibility set project config dir itself group root and make it group writeable 19 | RUN apk update \ 20 | && apk upgrade \ 21 | && apk add --no-cache ca-certificates dumb-init sed jq nodejs npm yarn icu-libs icu-data-full \ 22 | && update-ca-certificates \ 23 | && echo -e "\n---- Create runtime user and fix file access rights ----------" \ 24 | && adduser "${SERVICE_USER}" -h "${HOME}" -G root -S -u 10000 \ 25 | && chown -R root:root "${HOME}" \ 26 | && chown -R "${SERVICE_USER}" "${HOME}/config" \ 27 | && chmod g+w "${HOME}/config" \ 28 | && chmod ug+r,o-rwx "${HOME}"/config/*.json \ 29 | && echo -e "\n---- Check config file syntax --------------------------------" \ 30 | && for i in "${HOME}"/config/*.json; do echo "checking config file $i"; cat "$i" | jq empty; ret=$?; if [ $ret -ne 0 ]; then exit $ret; fi; done \ 31 | && echo -e "\n---- Installing app ------------------------------------------" \ 32 | && npm install --production -s \ 33 | && echo -e "\n---- Cleanup and hardening -----------------------------------" \ 34 | && "${HOME}/docker/harden.sh" \ 35 | && rm -rf /tmp/* /root/.??* /root/cache /var/cache/apk/* 36 | 37 | USER 10000 38 | 39 | HEALTHCHECK --interval=1m --timeout=2s CMD ["/redis-commander/bin/healthcheck.js"] 40 | 41 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 42 | CMD ["/redis-commander/docker/entrypoint.sh"] 43 | 44 | EXPOSE 8081 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joseph M. Ferner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 9 | Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | For security fixes only the latest version released to NPM and current version at 6 | `master` branch are supported. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Before reporting please check if this vulnerability exists in latest versions too. 11 | 12 | For reporting please open a issue or pull request on github. If it should be discussed privately 13 | please open a issue and ask so to get feedback from a maintainer. 14 | 15 | Report security bugs in third-party modules to the person or team maintaining the module. 16 | You can also report a vulnerability through the Node Security Project. 17 | -------------------------------------------------------------------------------- /bin/bcrypt-password.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | /** small helper script to generate a bcrypt hashed password from plain text password 5 | * Example: 6 | * 7 | * $ export HTTP_PASSWORD_HASH=$(bcrypt-password.js -p myplainpass) 8 | * $ echo $HTTP_PASSWORD_HASH 9 | * $2b$10$BQPbC8dlxeEqB/nXOkyjr.tlafGZ28J3ug8sWIMRoeq5LSVOXpl3W 10 | * 11 | * or 12 | * $ bcrypt-password.js -p myplainpass > my-secrets-file 13 | * $ cat my-secrets-file 14 | * $2b$10$BQPbC8dlxeEqB/nXOkyjr.tlafGZ28J3ug8sWIMRoeq5LSVOXpl3W 15 | * 16 | * 17 | * This generated password can be given to redis commander as a password-hash file 18 | * (param "--http-auth-password-hash") or set as env var "HTTP_PASSWORD_HASH". 19 | * 20 | * Additionally the docker container of redis commander is reading the hashed 21 | * http auth password from a secrets file too. 22 | */ 23 | 24 | const yargs = require('yargs'); 25 | let bcrypt; 26 | try { 27 | bcrypt = require('bcrypt'); 28 | // console.debug('using native bcrypt implementation'); 29 | } catch (e) { 30 | bcrypt = require('bcryptjs'); 31 | // console.debug('using javascript bcryptjs implementation'); 32 | } 33 | 34 | let args = yargs 35 | .alias('h', 'help') 36 | .alias('h', '?') 37 | .options('password', { 38 | alias: 'p', 39 | type: 'string', 40 | describe: 'The plain text password to hash' 41 | }) 42 | .check(function(value) { 43 | if (typeof value['password'] === 'undefined' || value['password'].trim() === '') { 44 | console.error('password parameter missing and must not be empty'); 45 | console.error('usage: bcrypt-password.js -p '); 46 | process.exit(-1); 47 | } 48 | return true; 49 | }) 50 | .usage('Usage: $0 [options]') 51 | .wrap(yargs.terminalWidth()) 52 | .argv; 53 | 54 | if (args.help) { 55 | yargs.help(); 56 | return process.exit(0); 57 | } 58 | 59 | console.log(bcrypt.hashSync(args['password'], 10)); 60 | -------------------------------------------------------------------------------- /bin/healthcheck.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | // fix the cwd to project base dir for browserify and config loading 6 | const path = require('path'); 7 | process.chdir( path.join(__dirname, '..') ); 8 | 9 | process.env.ALLOW_CONFIG_MUTATIONS = true; 10 | const config = require('config'); 11 | const http = require('http'); 12 | const util = require('./../lib/util'); 13 | 14 | // will fail if config is invalid - than redis commander itself cannot run too, therefore healthcheck would fail also 15 | // needed to fix potential problems with port/urlPrefix and so on 16 | try { 17 | util.validateConfig(); 18 | } 19 | catch(e) { 20 | console.error(e.message); 21 | process.exit(1); 22 | } 23 | 24 | let host = config.get('server.address'); 25 | if (!host || host === '0.0.0.0' || host === '::') host = '127.0.0.1'; 26 | let port = config.get('server.port'); 27 | let urlPrefix = config.get('server.urlPrefix'); 28 | 29 | http.get(`http://${host}:${port}${urlPrefix}/healthcheck`, (resp) => { 30 | let data = ''; 31 | 32 | resp.on('data', (chunk) => { 33 | data += chunk; 34 | }); 35 | 36 | // The whole response has been received. Print out the result. 37 | resp.on('end', () => { 38 | if (data.trim() === 'ok') process.exit(0); 39 | else { 40 | console.log('got unexpected response from server: ' + data); 41 | process.exit(1); 42 | } 43 | }); 44 | }).on("error", (err) => { 45 | console.log("error connection to server: " + err.message); 46 | process.exit(1); 47 | }); 48 | -------------------------------------------------------------------------------- /config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "noSave": "NOSAVE", 3 | "noLogData": "NO_LOG_DATA", 4 | "ui": { 5 | "foldingChar": "FOLDING_CHAR", 6 | "jsonViewAsDefault": "VIEW_JSON_DEFAULT", 7 | "binaryAsHex": "BINARY_AS_HEX", 8 | "maxHashFieldSize": "MAX_HASH_FIELD_SIZE" 9 | }, 10 | "redis": { 11 | "readOnly": "READ_ONLY", 12 | "flushOnImport": "FLUSH_ON_IMPORT", 13 | "useScan": "USE_SCAN", 14 | "scanCount": "SCAN_COUNT", 15 | "rootPattern": "ROOT_PATTERN", 16 | "connectionName": "REDIS_CONNECTION_NAME", 17 | "defaultLabel": "REDIS_LABEL" 18 | }, 19 | "server": { 20 | "address": "ADDRESS", 21 | "port": "PORT", 22 | "urlPrefix": "URL_PREFIX", 23 | "signinPath": "SIGNIN_PATH", 24 | "httpAuthHeaderName": "NAUGHTY_ISTIO_WORKAROUND_HEADER", 25 | "trustProxy": "TRUST_PROXY", 26 | "clientMaxBodySize": "CLIENT_MAX_BODY_SIZE", 27 | "httpAuth": { 28 | "username": "HTTP_USER", 29 | "password": "HTTP_PASSWORD", 30 | "passwordHash": "HTTP_PASSWORD_HASH" 31 | } 32 | }, 33 | "sso": { 34 | "enabled": "SSO_ENABLED", 35 | "jwtSharedSecret": "SSO_JWT_SECRET", 36 | "allowedIssuer": "SSO_ISSUER", 37 | "audience": "SSO_AUDIENCE", 38 | "subject": "SSO_SUBJECT" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "noSave": false, 3 | "noLogData": false, 4 | "ui": { 5 | "sidebarWidth": 250, 6 | "locked": false, 7 | "cliHeight": 320, 8 | "cliOpen": false, 9 | "foldingChar": ":", 10 | "jsonViewAsDefault": "none", 11 | "binaryAsHex": true, 12 | "maxHashFieldSize": 0 13 | }, 14 | "redis": { 15 | "readOnly": false, 16 | "flushOnImport": false, 17 | "useScan": true, 18 | "scanCount": 100, 19 | "rootPattern": "*", 20 | "connectionName": "redis-commander", 21 | "defaultLabel": "local", 22 | "defaultSentinelGroup": "mymaster", 23 | "extraAllowedReadOnlyCommands": [ 24 | "select", 25 | "info" 26 | ] 27 | }, 28 | "server": { 29 | "address": "0.0.0.0", 30 | "port": 8081, 31 | "urlPrefix": "", 32 | "signinPath": "signin", 33 | "httpAuthHeaderName": "Authorization", 34 | "trustProxy": false, 35 | "clientMaxBodySize": "100kb", 36 | "httpAuth": { 37 | "username": "", 38 | "password": "", 39 | "passwordHash": "", 40 | "comment": "to enable http auth set username and either password or passwordHash", 41 | "jwtSecret": "" 42 | } 43 | }, 44 | "sso": { 45 | "enabled": false, 46 | "jwtSharedSecret": "", 47 | "jwtAlgorithms": ["HS256", "HS384", "HS512"], 48 | "allowedIssuer": "", 49 | "audience": "", 50 | "subject": "" 51 | }, 52 | "connections": [] 53 | } 54 | -------------------------------------------------------------------------------- /dist/debian/init.d/redis-commander: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Written by Jardel Weyrich 4 | # 5 | ### BEGIN INIT INFO 6 | # Provides: redis-commander 7 | # Required-Start: $local_fs $network $named $time $syslog 8 | # Required-Stop: $local_fs $network $named $time $syslog 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: start Redis Commander (redis-commander) 12 | ### END INIT INFO 13 | 14 | # Defaults 15 | RUN_MODE="daemons" 16 | 17 | # Reads config file (will override defaults above) 18 | #[ -r /etc/default/redis-commander ] && . /etc/default/redis-commander 19 | 20 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 21 | NAME="redis-commander" 22 | USER="redis" 23 | GROUP="redis" 24 | DESC="Redis Commander" 25 | DAEMON="/usr/bin/redis-commander" 26 | DAEMONOPTS="" 27 | PIDDIR="/var/run/$NAME" 28 | PIDFILE="$PIDDIR/$NAME.pid" 29 | LOGDIR="/var/log/$NAME" 30 | LOGFILE="$LOGDIR/$NAME.log" 31 | 32 | # clear conflicting settings from the environment 33 | unset TMPDIR 34 | 35 | # See if the daemons are there 36 | test -x $DAEMON || exit 0 37 | 38 | . /lib/lsb/init-functions 39 | 40 | case "$1" in 41 | start) 42 | log_daemon_msg "Starting $DESC" 43 | # Make sure we have our PIDDIR and LOGDIR, even if they're on a tmpfs. 44 | install -o root -g root -m 755 -d $PIDDIR 45 | install -o root -g root -m 755 -d $LOGDIR 46 | 47 | if [ "$RUN_MODE" != "inetd" ]; then 48 | log_progress_msg "$NAME" 49 | if ! start-stop-daemon --start --quiet --oknodo --chuid "$USER:$GROUP" --background --make-pidfile --pidfile $PIDFILE --no-close --startas $DAEMON -- $DAEMONOPTS >> $LOGFILE 2>&1; then 50 | log_end_msg 1 51 | exit 1 52 | fi 53 | # Change the log permissions. 54 | chown -R $USER:adm $LOGDIR 55 | fi 56 | 57 | log_end_msg 0 58 | ;; 59 | stop) 60 | log_daemon_msg "Stopping $DESC" 61 | 62 | if [ "$RUN_MODE" != "inetd" ]; then 63 | log_progress_msg "$NAME" 64 | start-stop-daemon --stop --quiet --oknodo --retry 10 --pidfile $PIDFILE 65 | 66 | # Wait a little and remove stale PID file 67 | sleep 1 68 | if [ -f $PIDFILE ] && ! ps h `cat $PIDFILE` > /dev/null 69 | then 70 | # Stale PID file (process was succesfully stopped). 71 | rm -f $PIDFILE 72 | fi 73 | fi 74 | 75 | log_end_msg 0 76 | ;; 77 | reload) 78 | if [ "$RUN_MODE" != "inetd" ]; then 79 | log_daemon_msg "Reloading $DESC" 80 | 81 | start-stop-daemon --stop --quiet --signal HUP --pidfile $PIDFILE 82 | 83 | log_end_msg 0 84 | fi 85 | ;; 86 | restart|force-reload) 87 | $0 stop 88 | sleep 1 89 | $0 start 90 | ;; 91 | status) 92 | status="0" 93 | if [ "$RUN_MODE" != "inetd" ]; then 94 | status_of_proc -p $PIDFILE $DAEMON $NAME || status=$? 95 | fi 96 | exit $status 97 | ;; 98 | *) 99 | echo "Usage: /etc/init.d/$NAME {start|stop|reload|restart|force-reload|status}" 100 | exit 1 101 | ;; 102 | esac 103 | 104 | exit 0 105 | -------------------------------------------------------------------------------- /dist/pm2/pm2-config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [{ 3 | "name" : "redis-commander", 4 | "script" : "./bin/redis-commander.js", 5 | "cwd" : "add_installation_directory_here", 6 | "max_memory_restart" : "100M", 7 | "combine_logs" : true, 8 | "min_uptime" : 5000, 9 | "restart_delay" : 1000, 10 | "watch" : false, 11 | "ignore_watch" : ["dist", "bin", "docker", "k8s"], 12 | "pid_file" : "./redis-commander.pidfile", 13 | "env" : { 14 | "NODE_ENV" : "production", 15 | "NODE_APP_INSTANCE" : "", 16 | }, 17 | "args" : [ 18 | ] 19 | }] 20 | } 21 | -------------------------------------------------------------------------------- /dist/pm2/readme.md: -------------------------------------------------------------------------------- 1 | This directory contains an configuration template for the PM2 process monitor 2 | to start node apps and monitor them with automatic restart if something 3 | goes wrong. 4 | 5 | For more information see here: https://pm2.io/runtime/ 6 | 7 | The following fields should be updated to fit the local installation of redis commander: 8 | * "cwd" - installation directory of redis commander 9 | * "env" - add all environment variables needed to start redis commander 10 | * "args" - add arguments as needed 11 | 12 | The app can be registered at pm2 with the command: 13 | 14 | `pm2 deploy /path/to/custom/pm2-config.json` 15 | 16 | For running with pm2 either use this config file template adapted to your needs or the 17 | `ecosystem.config.js` file from the applications base directory 18 | -------------------------------------------------------------------------------- /dist/systemd/readme.md: -------------------------------------------------------------------------------- 1 | ## SystemD Service Unit 2 | 3 | template of service description to register redis-commander 4 | as a systemd service unit. 5 | 6 | ### Installation 7 | 8 | copy the `redis-commander.service` file to the `/etc/systemd/system/` directory 9 | and modify it to fit the local installation of redis commander. 10 | 11 | The following lines MUST be modified: 12 | 13 | * "Environment" - add as many "Environment" lines as needed, one line per environment variable set 14 | * "ExecStart" - update installation dir to match startup file inside local bin directory (`xxx/bin/redis-commander.js`) 15 | * "User" - add name of unprivileged user to run redis commander under. Do not run this app as user "root"! 16 | 17 | Now reload systemd as root to register this service unit: 18 | 19 | `systemctl daemon-reload` 20 | 21 | Service can now be started or stopped with: 22 | * `systemctl start redis-commander` 23 | * `systemctl stop redis-commander` 24 | -------------------------------------------------------------------------------- /dist/systemd/redis-commander.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Redis commander 3 | Documentation=https://github.com/joeferner/redis-commander/ 4 | After=network.target 5 | 6 | [Service] 7 | # add multiple "Environment" lines as needed, one for every env var to set 8 | Environment=NODE_ENV=production 9 | Environment=NODE_APP_INSTANCE= 10 | Type=simple 11 | User=_insert_unprivileged_username_here_ 12 | ExecStart=/usr/bin/node /installation_path/bin/redis-commander.js 13 | Restart=on-failure 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3' 3 | services: 4 | redis: 5 | container_name: redis 6 | hostname: redis 7 | image: redis 8 | 9 | redis-commander: 10 | container_name: redis-commander 11 | hostname: redis-commander 12 | image: ghcr.io/joeferner/redis-commander:latest 13 | build: . 14 | restart: always 15 | environment: 16 | - REDIS_HOSTS=local:redis:6379 17 | ports: 18 | - "8081:8081" 19 | user: redis 20 | -------------------------------------------------------------------------------- /docker/harden.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #set -x 3 | set -e 4 | 5 | # this script is taken from 6 | # https://github.com/ellerbrock/docker-collection/tree/master/dockerfiles/alpine-harden 7 | 8 | # Be informative after successful login. 9 | printf "\n\nApp container image built on %s." "$(date)" > /etc/motd 10 | 11 | # Improve strength of diffie-hellman-group-exchange-sha256 (Custom DH with SHA2). 12 | # See https://stribika.github.io/2015/01/04/secure-secure-shell.html 13 | # 14 | # Columns in the moduli file are: 15 | # Time Type Tests Tries Size Generator Modulus 16 | # 17 | # This file is provided by the openssh package on Fedora. 18 | moduli=/etc/ssh/moduli 19 | if [ -f ${moduli} ]; then 20 | cp ${moduli} ${moduli}.orig 21 | awk '$5 >= 2000' ${moduli}.orig > ${moduli} 22 | rm -f ${moduli}.orig 23 | fi 24 | 25 | # Remove existing crontabs, if any. 26 | rm -fr /var/spool/cron 27 | rm -fr /etc/crontabs 28 | rm -fr /etc/periodic 29 | 30 | # Remove all but a handful of admin commands. 31 | find /sbin /usr/sbin ! -type d \ 32 | -a ! -name login_duo \ 33 | -a ! -name setup-proxy \ 34 | -a ! -name sshd \ 35 | -a ! -name start.sh \ 36 | -a ! -name apk \ 37 | -delete 38 | 39 | # Remove world-writable permissions. 40 | # This breaks apps that need to write to /tmp, 41 | # such as ssh-agent. 42 | find / -xdev -type d -perm +0002 -exec chmod o-w {} + 43 | find / -xdev -type f -perm +0002 -exec chmod o-w {} + 44 | 45 | # Remove unnecessary user accounts. 46 | sed -i -r "/^(${SERVICE_USER}|root|sshd)/!d" /etc/group 47 | sed -i -r "/^(${SERVICE_USER}|root|sshd)/!d" /etc/passwd 48 | 49 | # Remove interactive login shell for everybody but user. 50 | sed -i -r "/^${SERVICE_USER}:/! s#^(.*):[^:]*\$#\1:/sbin/nologin#" /etc/passwd 51 | 52 | sysdirs=" 53 | /bin 54 | /etc 55 | /lib 56 | /sbin 57 | /usr 58 | " 59 | 60 | # Remove apk configs and apk/node package managers. 61 | # do not remove files below /lib/apk - db folder needed by many security scanners 62 | # to check for outdated packages 63 | if [ "$REMOVE_APK" != "0" ]; then 64 | # this not working using node: base image, only with alpine ones where these are extra packages 65 | apk del npm yarn 66 | find $sysdirs -xdev -regex '.*apk.*' \! -regex '/lib/apk.*' -exec rm -fr {} + 67 | find /sbin /usr/sbin -name apk -delete 68 | fi 69 | 70 | # Remove crufty... 71 | # /etc/shadow- 72 | # /etc/passwd- 73 | # /etc/group- 74 | find $sysdirs -xdev -type f -regex '.*-$' -exec rm -f {} + 75 | 76 | # Ensure system dirs are owned by root and not writable by anybody else. 77 | find $sysdirs -xdev -type d \ 78 | -exec chown root:root {} \; \ 79 | -exec chmod 0755 {} \; 80 | 81 | # Remove all suid files. 82 | find $sysdirs -xdev -type f -a -perm +4000 -delete 83 | 84 | # Remove other programs that could be dangerous. 85 | find $sysdirs -xdev \( \ 86 | -name hexdump -o \ 87 | -name chgrp -o \ 88 | -name chmod -o \ 89 | -name chown -o \ 90 | -name ln -o \ 91 | -name od -o \ 92 | -name strings -o \ 93 | -name su \ 94 | \) -delete 95 | 96 | # Remove init scripts since we do not use them. 97 | rm -fr /etc/init.d 98 | rm -fr /lib/rc 99 | rm -fr /etc/conf.d 100 | rm -fr /etc/inittab 101 | rm -fr /etc/runlevels 102 | rm -fr /etc/rc.conf 103 | 104 | # Remove kernel tunables since we do not need them. 105 | rm -fr /etc/modprobe.d 106 | rm -fr /etc/modules 107 | rm -fr /etc/mdev.conf 108 | rm -fr /etc/acpi 109 | 110 | # removed sysctl from delete to explicitly set some values for this container 111 | rm -fr /etc/sysctl* 112 | 113 | # Remove root homedir since we do not need it. 114 | rm -fr /root 115 | 116 | # Remove fstab since we do not need it. 117 | rm -f /etc/fstab 118 | 119 | # Remove broken symlinks (because we removed the targets above). 120 | find $sysdirs -xdev -type l -exec test ! -e {} \; -delete 121 | -------------------------------------------------------------------------------- /docs/GUI_EXAMPLE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/docs/GUI_EXAMPLE.png -------------------------------------------------------------------------------- /docs/adding_datatypes.md: -------------------------------------------------------------------------------- 1 | # HowTo to add support for new Redis data types 2 | 3 | This document gives a short overview about steps needed to display/add/edit new 4 | redis data types introduced via redis server extension modules and so on. 5 | 6 | Adding support for new types is most easy if redis command `type` returns 7 | a unique string and not on of the already supported datatypes (e.g. "stream"). 8 | For non unique types or types not directly know to redis (e.g. binary data has type "string"). 9 | there must be some kind of differentiation done server-side to toggle between 10 | booth types. 11 | 12 | This guide assumes Redis returns a new unique type. 13 | 14 | ### Display data (read-only support) 15 | 16 | 1. Add new EJS template inside `web/static/templates` folder to display data 17 | 2. Design new treeview icon inside folder `web/static/images` 18 | 3. modify file `web/static/scripts/redisCommander.js` the methods `getIconForType()` 19 | and point to your own icon for the given new type. 20 | 4. modify file `web/static/scripts/redisCommander.js` the methods `loadKey()`, add another case to the `switch(keyData.type). 21 | The method called here should render the new template with given data. 22 | 5. modify `lib/routes/apiv1.js` method `getKeyDetails()` and add handling of new 23 | datatype to switch expression. The new getDetails method must fetch all data displayed 24 | at the EJS template together with the TTL field. 25 | For unsupported commands in "ioredis" library the `redisConnection.call('COMMAND', arguments)` 26 | syntax should be used. 27 | 6. Update README and CHANGELOG to mention new support. 28 | 29 | ### Add new data for this type 30 | 31 | The following steps add support to add new data for this type to Redis Commander. 32 | It does not allow modification of existing data. 33 | 34 | 1. Update EJS template `web/views/modals/addKeyModal.ejs`, add new type to the "keyType" dropdown. 35 | If necessary add new Fields to the form that are hidden as default and only made visible 36 | whenever user selects this new type. The javascript code to handle form modifications on 37 | type selection can be found in `web/static/scripts/redisCommander.js` at method `setupAddKeyButton()`. 38 | 2. Add server-side code to add new key data. On submit of the form inside the browser it calls 39 | the browser method `addNewKey()` which in turn post the data to the server, triggering the 40 | method `saveKey()` from `lib/routes/apiv1.js`. Add new data type to the `switch (type)` part here. 41 | 3. Add new "post", "put" routes to "apiv1.js" file for more explicit datatype support. 42 | 3. Update README and CHANGELOG to mention new support. 43 | 44 | ### Delete data for this type 45 | 46 | Deleting data via right-click on tree or pressing "DEL" key is supported 47 | out-of-the box via 48 | 49 | 1. Add delete button to the client-side EJS template that displays the data. 50 | located under `web/static/templates`. This button should trigger the `deleteKey()`. 51 | 2. Make sure the client template only renders the button if not in read-only mode 52 | (template variable `redisReadOnly` is not true) 53 | 3. To allow deletion via "delete" route add them to the "apiv1.js" file. 54 | This can be done together with the Modify data implementation. 55 | 4. Update README and CHANGELOG to mention new support. 56 | 57 | ### Modify existing data 58 | 59 | 1. Decide if data for this type can be edited directly (like string) or should be displayed only and 60 | another modal dialog is needed to modify them., Update view template under 61 | `web/static/templates` accordingly. 62 | 2. Make sure to allow modification of data only if not in read-only mode 63 | (template variable `redisReadOnly` is not true) - e.g. no "Save" button or loading edit modal 64 | when for read-only instances. 65 | 3. (optional) Add new modal to edit data, add EJS template at `web/views/modals`. 66 | Some more complex datatypes with "sub-data" (e.g. lists, sets, ...) use another modal ta add 67 | this new sub-data, e.g. add new data to a list and so on. These modals trigger 68 | their respective client-side javascript methods to check validity or post them 69 | to the correct server api. 70 | 4. Add extra methods to populate modals with data (similiar to `addXSetMember()` `editXSetMember()` and 71 | `removeXSetMember()` in `web/static/scripts/redisCommander.js`). 72 | 5. (optional) add delete button to the modify entry modal. This button can either trigger an 73 | explicit delete method server-side or (as most other do) set value to `tombstone` and send 74 | form to the update/modify method non server (similiar to `removeXSetMember()` in 75 | `web/static/scripts/redisCommander.js`) 76 | 6. Include all new modals at the end of the file `web/views/layout.ejs` beneath the other modals. 77 | 7. Add new "post" routes to "apiv1.js" file for more explicit datatype support. 78 | 8. Update README and CHANGELOG to mention new support. 79 | -------------------------------------------------------------------------------- /docs/security_checks.md: -------------------------------------------------------------------------------- 1 | # Security Considerations 2 | 3 | Some points to check for a secure usage of this image. 4 | Some of these are more general, some are relevant only using redis commander 5 | as container (marked with "Docker" below but relevant for Kubernetes and similiar too) 6 | 7 | ### Use Authentication for Web-Access 8 | 9 | per Default Redis Commander does not active HTTP authentication - everyone being able 10 | to access the web frontend can do whatever he likes to your redis databases configure 11 | (read: DELETE all keys or modify as he likes) 12 | 13 | Redis Commander does not has a full blown user manangement, only basic support for 14 | authentication. No external user store like LDAP or similiar is possible. 15 | (If such feature is required please create an issue to discuss implementation before posting a Pull Request) 16 | 17 | #### a) HTTP Basic authentication 18 | One user account (username/password) can be configured to protect the web page. 19 | This account has full rights on the database (as much as redis server allows). 20 | * command line: `--http-user ` and `--http-pass ` 21 | * environment variables: `HTTP_USER` and `HTTP_PASSWORD` or `HTTP_PASSWORD_HASH` 22 | * config file: `server.httpAuth.username` and `server.httpAuth.password` or `server.httpAuth.passwordHash` 23 | Booth values must be given (username and either password or password hash) 24 | 25 | the passwords can be given as the content of files too for the docker container. Just set the env vars 26 | `HTTP_PASSWORD_FILE` or `HTTP_PASSWORD_HASH_FILE` to the name of the files containing the passwords/hash. 27 | 28 | #### b) SSO login via JSON Web Token (JWT) 29 | Alternative authentication of different users can be transfered to other web apps 30 | which generate a JSON web token (see RFC ) that is given when calling redis commander. 31 | The url to call for this SSO feature is `https://:/sso` with the JWT token send as parameter `access_token` 32 | either via HTTP GET or HTTP POST: 33 | 34 | example: `HTTP GET https://:/sso?access_token=dfgfdg.token...` 35 | 36 | The parameters to validate the jwt are configured in the config file below the 37 | `sso` config object. Currently only JWT signing with a shared secret is supported. 38 | Alternative configuration can be done via environment variables: `SSO_ENABLED`, 39 | `SSO_JWT_SECRET`, `SSO_ISSUER`, `SSO_AUDIENCE`, `SSO_SUBJECT`. 40 | 41 | SSO JWT login must be enabled explicitly within the configuration and all values to check the 42 | token validity for should be set. SSO JWT login is disabled as default. 43 | 44 | ### Use TLS 45 | Redis commander does not support TLS encryption out-of-the box. To add TLS encryption 46 | a reverse proxy like NGinx, Apache, Traefik or similiar with working HTTPS setup must 47 | be put in front of Redis Commander to handle the HTTP encryption. 48 | Only if used from localhost alone TLS encryption may be dropped. 49 | 50 | ### Use Read-Only Mode if possible 51 | The default setup of Redis Commander allows read-write access to the redis databases. 52 | With a confg switch it is possible to start redis-commander in read-only mode and disallow 53 | all redis commands that do modify data. 54 | 55 | Configuration parameters: 56 | * command line: `--read-only` 57 | * environment variable: `READ_ONLY` 58 | * config file: `redis.readOnly` 59 | 60 | If read-only as well as read-write access via Redis Commander is needed 61 | it is possible to start two instances of this app, one normal and ane with read-only mode 62 | enabled. 63 | 64 | Technical info how this is implemented: 65 | * After database connect the `command` command is issued to the server and the list returned 66 | is parsed for read-only commands that will be allowed (see redis doc: https://redis.io/commands/command). 67 | Due to this redis servers with custom plugins should work as expected too. 68 | * If `command` execution failes a hard-coded whitelist of commands allowed is used 69 | (file `lib/redisCommands/redisCore.js` all lists starting with `_readCmds...`) 70 | 71 | ### Remove all Package Manager (Docker) 72 | Whenever the image is final and no modifications during container runtime to install or update 73 | software are needed (should be the case!) all package managers can be removed final step of the 74 | container image build. 75 | 76 | For production usage this image should be build by yourself and stored in a trusted registry. 77 | For the build the docker build-arg `REMOVE_APK=1` should be set - this deletes all package 78 | managers (apk, npm, yarn) at the end of the build before finalizing the image. 79 | 80 | The image published on Dockerhub does not remove package manager to allow others 81 | to create new iages based on this one (`REMOVE_APK=0`). 82 | 83 | ### Do not run image as root (Docker) 84 | Current image is build to run as a unprivileged user and not as root. 85 | 86 | ### Set passwords as secrets (Docker) 87 | Do not set passwords (http auth, redis server, ...) as normal command line parameter (even if supported) 88 | but either use custom config files with correct file protection modes or set them 89 | as Kubernetes Secrets / Docker Secrets. 90 | 91 | An alternative is mounting files (with restrictive permissions on them) into the container which 92 | where the passwords is stored in. The name o f the files can be set via docker image environment variables 93 | `HTTP_PASSWORD_FILE`, `HTTP_PASSWORD_HASH_FILE`, `REDIS_PASSWORD_FILE` and `SENTINEL_PASSWORD_FILE`. 94 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | var argsPath = path.join(os.homedir(), '.redis-commander-args'); 6 | 7 | var args = ''; 8 | try { 9 | args = fs.readFileSync(argsPath).toString(); 10 | console.log(`Custom args from '${argsPath}' file: '${args}'`); 11 | } catch (e) { 12 | console.log(`args file '${argsPath}' not found. Default: no args`); 13 | } 14 | 15 | module.exports = { 16 | apps: [{ 17 | name: 'Redis Commander', 18 | script: 'bin/redis-commander.js', 19 | args: args, 20 | env: { 21 | NODE_ENV: "production", 22 | }, 23 | }] 24 | }; 25 | -------------------------------------------------------------------------------- /k8s/helm-chart/README.md: -------------------------------------------------------------------------------- 1 | # Basic Helm Chart for Redis-Commander 2 | 3 | For a description see file [README.md](redis-commander/README.md) inside the chart directory. 4 | 5 | ## Helm chart documentations 6 | 7 | The README files are auto-generated with helm-docs [1] from the gotmpl file and the documentation 8 | of each possible value within the values.yml. 9 | The most easy way to update Helm-chart documentation is to call the script used by the GitHub action 10 | to validate the documentation: `.github/helmdocs.sh`. This script downloads helm-docs and run it against 11 | the chart directory updating helm documentation files as needed. 12 | 13 | ## Helm chart values.yml schema validation 14 | 15 | To validate the data provided to the helm chart within the `values.yml` file a JSON schema file is provided. 16 | Helm v3 automatically uses this file to check the input for invalid or missing values. This file must be 17 | updated manually if new values are added to the helm chart. 18 | 19 | More information about this file and how write it can be found on the Helm docs [2]. A good introduction 20 | write-up can be found in the following blog post from Austin Dewey [3] 21 | 22 | [1] https://github.com/norwoodj/helm-docs 23 | [2] https://helm.sh/docs/topics/charts/#schema-files 24 | [3] https://austindewey.com/2020/06/13/helm-tricks-input-validation-with-values-schema-json/ 25 | -------------------------------------------------------------------------------- /k8s/helm-chart/example-values-as-json.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # example definition of a more complex json configuration files to mount it into the docker container via 3 | # helm chart - everything from the "local_production_json" key is written into the configmap generated by helm 4 | # and mounted as a file into the container from the configmap 5 | # 6 | # The data are written as file "local-production-docker.json" to not overwrite the file "local-production.json" 7 | # that gets create from within the docker startup script. Redis commander first evaluates the "local-production.json" 8 | # and afterward "local-production-docker.json" - keys defined here overwrite settings from "local-production.json". 9 | # 10 | # this example defines all data to write into the "local_production.json" file as JSON - the 11 | # data are written as-is into the config file without any conversion 12 | connections: 13 | local_production_json: >- 14 | { 15 | "noSave": false, 16 | "noLogData": false, 17 | "ui": { 18 | "foldingChar": "|" 19 | }, 20 | "redis": { 21 | "readOnly": true 22 | }, 23 | "server": { 24 | "clientMaxBodySize": "500kb", 25 | "httpAuth": { 26 | "username": "the-user", 27 | "password": "is-secret" 28 | } 29 | }, 30 | "connections": [ 31 | { 32 | "label": "redis-sentinel-service-x", 33 | "sentinels": "19.94.12.11:26379, 19.94.12.12:26379", 34 | "sentinelName": "mymaster", 35 | "dbIndex": 0 36 | }, 37 | { 38 | "label": "redis-sentinel-service-y", 39 | "sentinels": "20.20.12.11:26379", 40 | "sentinelName": "mymaster", 41 | "dbIndex": 0 42 | }, 43 | { 44 | "label": "redis-server-service-xz", 45 | "host": "19.94.12.11", 46 | "port": "6379", 47 | "dbIndex": 0 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /k8s/helm-chart/example-values-as-yml.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # example definition of a more complex json configuration files to mount it into the docker container via 3 | # helm chart - everything from the "local_production_json" key is written into the configmap generated by helm 4 | # and mounted as a file into the container from the configmap 5 | # 6 | # The data are written as file "local-production-docker.json" to not overwrite the file "local-production.json" 7 | # that gets create from within the docker startup script. Redis commander first evaluates the "local-production.json" 8 | # and afterward "local-production-docker.json" - keys defined here overwrite settings from "local-production.json". 9 | # 10 | # this example defines all data to write into the "local-production-docker.json" file as YAML object 11 | # this YAML is converted into JSON before writing it into the docker container as JSON config file 12 | connections: 13 | local_production_json: 14 | noSave: false 15 | noLogData: false 16 | ui: 17 | foldingChar: "|" 18 | redis: 19 | readOnly: true 20 | server: 21 | clientMaxBodySize: "500kb" 22 | httpAuth: 23 | username: "the-user" 24 | password: "is-secret" 25 | connections: 26 | - label: "redis-sentinel-service-x" 27 | sentinels: "19.94.12.11:26379, 19.94.12.12:26379" 28 | sentinelName: "mymaster" 29 | dbIndex: 0 30 | - label: "redis-sentinel-service-y" 31 | sentinels: "20.20.12.11:26379" 32 | sentinelName: "mymaster" 33 | dbIndex": 0 34 | - label: "redis-server-service-xz" 35 | host: "19.94.12.11" 36 | port: "6379" 37 | dbIndex: 0 38 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/Chart.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v2 3 | name: redis-commander 4 | description: A Helm chart for redis-commander 5 | 6 | # A chart can be either an 'application' or a 'library' chart. 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | type: application 10 | 11 | # This is the chart version. This version number should be incremented each time you make changes 12 | # to the chart and its templates, including the app version. 13 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 14 | version: 0.6.0 15 | 16 | # This is the version number of the application being deployed. This version number should be 17 | # incremented each time you make changes to the application. Versions are not expected to 18 | # follow Semantic Versioning. They should reflect the version the application is using. 19 | appVersion: latest 20 | 21 | keywords: 22 | - redis 23 | home: https://joeferner.github.io/redis-commander 24 | sources: 25 | - https://github.com/joeferner/redis-commander 26 | 27 | maintainers: 28 | - name: Joe Ferner 29 | url: https://github.com/joeferner 30 | - name: Stefan Seide 31 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/README.md: -------------------------------------------------------------------------------- 1 | # redis-commander 2 | 3 | A Helm chart for redis-commander 4 | 5 | ![Version: 0.6.0](https://img.shields.io/badge/Version-0.6.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) 6 | 7 | ## Install 8 | 9 | Install using this repo after local git checkout itself with setting redis server host value 10 | to `redis` 11 | 12 | ```sh 13 | cd 14 | helm -n myspace install redis-web-ui ./k8s/helm-chart/redis-commander --set redis.host=redis 15 | ``` 16 | 17 | ## Values 18 | 19 | | Key | Type | Default | Description | 20 | |-----|------|---------|-------------| 21 | | affinity | object | `{}` | optional set pod affinity definitions for kubernetes | 22 | | autoscaling | object | `{"enabled":false,"maxReplicas":1,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Autoscaling configuration for k8s deployment | 23 | | configMapData | object | `{}` | optional data to add to the configmap generated by this helm chart. This might be useful if extra files shall be created inside the docker container which can be mounted defining the "volumeMounts" and "volumes" below. | 24 | | connections | object | `{}` | optional object to set the "local_production_json" property to let Helm render a "local-production.json" file from a configmap to preconfigure more complex configuration examples with connection data too without the need to set all parameter via environment variables (where available). For a working example see either file "example-values-as-json.yaml" where the file content is written as json formatted string or file "example-values-as-yml.yaml" with all config values for the file are defined as YAML. | 25 | | env | list | `[]` | Extra env vars for the main pod redis-commander in array structure ([{name: ... , value: ...}, {name: ... , value: ...}]). | 26 | | fullnameOverride | string | `""` | | 27 | | httpAuth.password | string | `""` | Specify http basic password for the web ui | 28 | | httpAuth.username | string | `""` | Specify http basic username and password to protect access to redis commander web ui | 29 | | image.apparmorProfile | string | `"runtime/default"` | Enable AppArmor per default when available on k8s host, change to "unconfined" to disable. Either AppArmor or SecComp may be enabled by the container runtime | 30 | | image.pullPolicy | string | `"Always"` | Deployment pull policy, either "Always" or "IfNotPresent" | 31 | | image.repository | string | `"ghcr.io/joeferner/redis-commander"` | Docker image for deployment | 32 | | image.seccompProfile | string | `"runtime/default"` | Enable SecComp profile when used by cluster, change to "unconfined" to disable. Either AppArmor or SecComp may be enabled by the container runtime | 33 | | image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. | 34 | | imagePullSecrets | list | `[]` | Optional image pull secrets for private docker registries | 35 | | ingress.annotations | object | `{}` | Add additional annotations for the ingess spec Example: 'kubernetes.io/ingress.class: nginx' or 'kubernetes.io/tls-acme: "true"' | 36 | | ingress.className | string | `""` | optional name of an IngressClass used for this Ingress, available since k8s 1.18 https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource | 37 | | ingress.enabled | bool | `false` | Enable Ingress for the service | 38 | | ingress.hosts[0] | object | `{"host":"chart-example.local","paths":["/"]}` | Host name to use for the ingress definition | 39 | | ingress.hosts[0].paths | list | `["/"]` | list of paths within the given host for path-based routing, otherwise the root path "/" will be used | 40 | | ingress.legacy | bool | `false` | Use *Legacy*, deprecated Ingress versions. Ingress apiVersions prior to `networking.k8s.io/v1` are deprecated and removed in kubernetes 1.22. Set the `legacy` flag to *true* if you are using kubernetes older than 1.19 or OpenShift v3 and require support for the older API versions. | 41 | | ingress.pathType | string | `"ImplementationSpecific"` | Set the pathType for the v1 Ingress resource. This setting is ignored for `legacy` Ingress resources. Details on **Path Type** are available here; https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types | 42 | | ingress.tls | list | `[]` | | 43 | | istio.enabled | bool | `false` | Enable Istio VirtualService for the service The endpoint (target) is defined by the regular k8s service already defined by the chart | 44 | | istio.gateway | string | `""` | Gateway name to use for the istio definition | 45 | | istio.host | string | `""` | Host name to use for the istio definition | 46 | | istio.hostPrefix | string | `"/"` | Host prefix to use for the istio definition | 47 | | kubeVersion | string | `""` | Optional override Kubernetes version | 48 | | nameOverride | string | `""` | | 49 | | nodeSelector | object | `{}` | optional set pod node selector definitions for kubernetes | 50 | | podAnnotations | object | `{}` | | 51 | | podSecurityContext | object | `{}` | | 52 | | redis.host | string | `"redis-master"` | Specifies a single Redis host | 53 | | redis.hosts | string | `""` | Alternative: Specifies multiple redis endpoints ,... instead of one in "redis.host" Example: "local:localhost:6379,myredis:10.10.20.30" | 54 | | redis.password | string | `""` | Specifies redis password | 55 | | redis.username | string | `""` | Specifies redis username - supported since Redis 6.0 with ACL support. | 56 | | replicaCount | int | `1` | Number of replicas to create for deployment, should be 1 | 57 | | resources | object | `{}` | We usually recommend not to specify default resources and to leave this as a conscious choice for the user. This also increases chances charts run on environments with little resources, such as Minikube. If you do want to specify resources, uncomment the following lines, adjust them as necessary, and remove the curly braces after 'resources:'. | 58 | | securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":false,"runAsNonRoot":true}` | Configuration of the linux security context for the docker image. This restricts the rights of the running docker image as far as possible. "readOnlyRootFilesystem" must be set to false to auto-generate a config file with multiple redis hosts or sentinel hosts | 59 | | service.annotations | object | `{}` | Add additional annotations for the service spec Example: 'my.custom.annotation: value' | 60 | | service.port | int | `80` | External port where service is available | 61 | | service.type | string | `"ClusterIP"` | Type of k8s service to export | 62 | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | 63 | | serviceAccount.create | bool | `false` | Specifies whether a service account should be created When no service account is created the account credentials of the default account are also not automatically mounted into the pod (automountServiceAccountToken: false), tokens only mounted when service account is used but Redis-Commander itself does not use the k8s api server token | 64 | | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | 65 | | tolerations | list | `[]` | optional set pod toleration definitions for kubernetes | 66 | | volumeMounts | list | `[]` | optional list of volumes to mount into the docker deployment. This can either be a local storage volume or a configmap to mount data as file. Each list item needs a "name" and a "mountPath". Setting this will most of the time also require setting a "volumes" entry. | 67 | | volumes | list | `[]` | optional list of volumes to mount into the docker deployment. This can either be a local storage volume or a configmap to mount data as file. Each list item needs a "name" and a "mountPath". Setting this will most of the time also require setting a "volumeMounts" entry. | 68 | 69 | ## Example 70 | 71 | Another alternative is the usage of the helm repo hosted at the github pages site. 72 | 73 | ```sh 74 | # add repo 75 | helm repo add redis-commander https://joeferner.github.io/redis-commander/ 76 | 77 | # custom values 78 | cat > myvalues.yaml < 14 | helm -n myspace install redis-web-ui ./k8s/helm-chart/redis-commander --set redis.host=redis 15 | ``` 16 | 17 | {{ template "chart.requirementsSection" . }} 18 | 19 | {{ template "chart.valuesSection" . }} 20 | 21 | ## Example 22 | 23 | Another alternative is the usage of the helm repo hosted at the github pages site. 24 | 25 | ```sh 26 | # add repo 27 | helm repo add redis-commander https://joeferner.github.io/redis-commander/ 28 | 29 | # custom values 30 | cat > myvalues.yaml <=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | --- 6 | apiVersion: networking.k8s.io/v1beta1 7 | {{- else -}} 8 | apiVersion: extensions/v1beta1 9 | {{- end }} 10 | kind: Ingress 11 | metadata: 12 | name: {{ $fullName }} 13 | labels: 14 | {{- include "redis-commander.labels" . | nindent 4 }} 15 | {{- with .Values.ingress.annotations }} 16 | annotations: 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | spec: 20 | {{- if .Values.ingress.tls }} 21 | tls: 22 | {{- range .Values.ingress.tls }} 23 | - hosts: 24 | {{- range .hosts }} 25 | - {{ . | quote }} 26 | {{- end }} 27 | secretName: {{ .secretName }} 28 | {{- end }} 29 | {{- end }} 30 | rules: 31 | {{- range .Values.ingress.hosts }} 32 | - host: {{ .host | quote }} 33 | http: 34 | paths: 35 | {{- range .paths }} 36 | - path: {{ . }} 37 | backend: 38 | serviceName: {{ $fullName }} 39 | servicePort: {{ $svcPort }} 40 | {{- end }} 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ingress.enabled (not .Values.ingress.legacy) -}} 2 | {{- $fullName := include "redis-commander.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- $pathType := .Values.ingress.pathType -}} 5 | --- 6 | apiVersion: networking.k8s.io/v1 7 | kind: Ingress 8 | metadata: 9 | name: {{ $fullName }} 10 | labels: 11 | {{- include "redis-commander.labels" . | nindent 4 }} 12 | {{- with .Values.ingress.annotations }} 13 | annotations: 14 | {{- toYaml . | nindent 4 }} 15 | {{- end }} 16 | spec: 17 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" (include "redis-commander.capabilities.kubeVersion" .)) }} 18 | ingressClassName: {{ .Values.ingress.className | quote }} 19 | {{- end }} 20 | {{- if .Values.ingress.tls }} 21 | tls: 22 | {{- range .Values.ingress.tls }} 23 | - hosts: 24 | {{- range .hosts }} 25 | - {{ . | quote }} 26 | {{- end }} 27 | secretName: {{ .secretName }} 28 | {{- end }} 29 | {{- end }} 30 | rules: 31 | {{- range .Values.ingress.hosts }} 32 | - host: {{ .host | quote }} 33 | http: 34 | paths: 35 | {{- range .paths }} 36 | - path: {{ . }} 37 | pathType: {{ $pathType }} 38 | backend: 39 | service: 40 | name: {{ $fullName }} 41 | port: 42 | number: {{ $svcPort }} 43 | {{- end }} 44 | {{- end }} 45 | {{- end }} 46 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/templates/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{ include "redis-commander.fullname" . }} 6 | labels: 7 | {{- include "redis-commander.labels" . | nindent 4 }} 8 | {{- with .Values.service.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | spec: 13 | type: {{ .Values.service.type }} 14 | ports: 15 | - port: {{ .Values.service.port }} 16 | targetPort: http 17 | protocol: TCP 18 | name: http 19 | selector: 20 | {{- include "redis-commander.selectorLabels" . | nindent 4 }} 21 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | --- 3 | apiVersion: v1 4 | kind: ServiceAccount 5 | metadata: 6 | name: {{ include "redis-commander.serviceAccountName" . }} 7 | labels: 8 | {{- include "redis-commander.labels" . | nindent 4 }} 9 | {{- with .Values.serviceAccount.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: "{{ include "redis-commander.fullname" . }}-test-connection" 6 | labels: 7 | {{- include "redis-commander.labels" . | nindent 4 }} 8 | annotations: 9 | "helm.sh/hook": test-success 10 | spec: 11 | containers: 12 | - name: wget 13 | image: busybox 14 | command: ['wget'] 15 | args: ['{{ include "redis-commander.fullname" . }}:{{ .Values.service.port }}'] 16 | restartPolicy: Never 17 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/templates/virtual-service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.istio.enabled }} 2 | --- 3 | apiVersion: networking.istio.io/v1beta1 4 | kind: VirtualService 5 | metadata: 6 | name: {{ include "redis-commander.fullname" . }} 7 | labels: 8 | {{- include "redis-commander.labels" . | nindent 4 }} 9 | {{- with .Values.ingress.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | gateways: 15 | - {{ .Values.istio.gateway }} 16 | hosts: 17 | - {{ .Values.istio.host }} 18 | http: 19 | - match: 20 | - uri: 21 | prefix: {{ .Values.istio.hostPrefix }} 22 | route: 23 | - destination: 24 | host: {{ include "redis-commander.fullname" . }} 25 | port: 26 | number: {{ .Values.service.port }} 27 | {{- end}} 28 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "type": "object", 4 | "properties": { 5 | "affinity": { 6 | "type": "object" 7 | }, 8 | "autoscaling": { 9 | "type": "object", 10 | "properties": { 11 | "enabled": { 12 | "type": "boolean" 13 | }, 14 | "maxReplicas": { 15 | "type": "integer" 16 | }, 17 | "minReplicas": { 18 | "type": "integer" 19 | }, 20 | "targetCPUUtilizationPercentage": { 21 | "type": "integer" 22 | } 23 | } 24 | }, 25 | "env": { 26 | "type": "array" 27 | }, 28 | "fullnameOverride": { 29 | "type": "string" 30 | }, 31 | "httpAuth": { 32 | "type": "object", 33 | "properties": { 34 | "password": { 35 | "type": "string" 36 | }, 37 | "username": { 38 | "type": "string" 39 | } 40 | } 41 | }, 42 | "image": { 43 | "type": "object", 44 | "properties": { 45 | "apparmorProfile": { 46 | "type": "string" 47 | }, 48 | "pullPolicy": { 49 | "type": "string" 50 | }, 51 | "repository": { 52 | "type": "string" 53 | }, 54 | "seccompProfile": { 55 | "type": "string" 56 | }, 57 | "tag": { 58 | "type": "string" 59 | } 60 | } 61 | }, 62 | "imagePullSecrets": { 63 | "type": "array" 64 | }, 65 | "ingress": { 66 | "type": "object", 67 | "properties": { 68 | "annotations": { 69 | "type": "object" 70 | }, 71 | "enabled": { 72 | "type": "boolean" 73 | }, 74 | "hosts": { 75 | "type": "array", 76 | "items": { 77 | "type": "object", 78 | "properties": { 79 | "host": { 80 | "type": "string" 81 | }, 82 | "paths": { 83 | "type": "array" 84 | } 85 | } 86 | } 87 | }, 88 | "legacy": { 89 | "type": "boolean" 90 | }, 91 | "pathType": { 92 | "type": "string" 93 | }, 94 | "tls": { 95 | "type": "array" 96 | } 97 | } 98 | }, 99 | "kubeVersion": { 100 | "type": "string" 101 | }, 102 | "nameOverride": { 103 | "type": "string" 104 | }, 105 | "nodeSelector": { 106 | "type": "object" 107 | }, 108 | "podAnnotations": { 109 | "type": "object" 110 | }, 111 | "podSecurityContext": { 112 | "type": "object" 113 | }, 114 | "redis": { 115 | "type": "object", 116 | "properties": { 117 | "host": { 118 | "type": "string" 119 | }, 120 | "hosts": { 121 | "type": "string" 122 | }, 123 | "password": { 124 | "type": "string" 125 | }, 126 | "username": { 127 | "type": "string" 128 | } 129 | } 130 | }, 131 | "replicaCount": { 132 | "type": "integer" 133 | }, 134 | "resources": { 135 | "type": "object" 136 | }, 137 | "securityContext": { 138 | "type": "object", 139 | "properties": { 140 | "allowPrivilegeEscalation": { 141 | "type": "boolean" 142 | }, 143 | "capabilities": { 144 | "type": "object", 145 | "properties": { 146 | "drop": { 147 | "type": "array", 148 | "items": { 149 | "type": "string" 150 | } 151 | } 152 | } 153 | }, 154 | "readOnlyRootFilesystem": { 155 | "type": "boolean" 156 | }, 157 | "runAsNonRoot": { 158 | "type": "boolean" 159 | } 160 | } 161 | }, 162 | "service": { 163 | "type": "object", 164 | "properties": { 165 | "port": { 166 | "type": "integer" 167 | }, 168 | "type": { 169 | "type": "string" 170 | }, 171 | "annotations": { 172 | "type": "object" 173 | } 174 | } 175 | }, 176 | "serviceAccount": { 177 | "type": "object", 178 | "properties": { 179 | "annotations": { 180 | "type": "object" 181 | }, 182 | "create": { 183 | "type": "boolean" 184 | }, 185 | "name": { 186 | "type": "string" 187 | } 188 | } 189 | }, 190 | "tolerations": { 191 | "type": "array" 192 | }, 193 | "connections": { 194 | "type": "object" 195 | }, 196 | "volumeMounts": { 197 | "type": "array" 198 | }, 199 | "volumes": { 200 | "type": "array" 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /k8s/helm-chart/redis-commander/values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Default values for redis-commander. 3 | # This is a YAML-formatted file. 4 | # Declare variables to be passed into your templates. 5 | # All values should be commented with "# -- text..." to allow auto-generation of the documentation 6 | 7 | # -- Number of replicas to create for deployment, should be 1 8 | replicaCount: 1 9 | image: 10 | # -- Docker image for deployment 11 | repository: ghcr.io/joeferner/redis-commander 12 | # -- Deployment pull policy, either "Always" or "IfNotPresent" 13 | pullPolicy: Always 14 | # -- Enable AppArmor per default when available on k8s host, change to "unconfined" to disable. 15 | # Either AppArmor or SecComp may be enabled by the container runtime 16 | apparmorProfile: runtime/default 17 | # -- Enable SecComp profile when used by cluster, change to "unconfined" to disable. 18 | # Either AppArmor or SecComp may be enabled by the container runtime 19 | seccompProfile: runtime/default 20 | # -- Overrides the image tag whose default is the chart appVersion. 21 | tag: "" 22 | 23 | # -- Optional image pull secrets for private docker registries 24 | imagePullSecrets: [] 25 | 26 | # -- Optional override Kubernetes version 27 | kubeVersion: "" 28 | 29 | nameOverride: "" 30 | fullnameOverride: "" 31 | redis: 32 | # -- Specifies a single Redis host 33 | host: "redis-master" 34 | 35 | # -- Specifies redis username - supported since Redis 6.0 with ACL support. 36 | username: "" 37 | # -- Specifies redis password 38 | password: "" 39 | 40 | # -- Alternative: Specifies multiple redis endpoints ,... instead of one in "redis.host" 41 | # Example: "local:localhost:6379,myredis:10.10.20.30" 42 | hosts: "" 43 | 44 | httpAuth: 45 | # -- Specify http basic username and password to protect access to redis commander web ui 46 | username: "" 47 | # -- Specify http basic password for the web ui 48 | password: "" 49 | 50 | # -- Extra env vars for the main pod redis-commander in array structure ([{name: ... , value: ...}, {name: ... , value: ...}]). 51 | env: [] 52 | 53 | serviceAccount: 54 | # -- Specifies whether a service account should be created 55 | # When no service account is created the account credentials of the default account are also not automatically 56 | # mounted into the pod (automountServiceAccountToken: false), tokens only mounted when service account is used 57 | # but Redis-Commander itself does not use the k8s api server token 58 | create: false 59 | # -- Annotations to add to the service account 60 | annotations: {} 61 | # -- The name of the service account to use. 62 | # If not set and create is true, a name is generated using the fullname template 63 | name: "" 64 | 65 | podAnnotations: {} 66 | 67 | podSecurityContext: {} 68 | # fsGroup: 2000 69 | 70 | # -- Configuration of the linux security context for the docker image. This restricts the 71 | # rights of the running docker image as far as possible. 72 | # 73 | # "readOnlyRootFilesystem" must be set to false to auto-generate a config file with multiple redis hosts or 74 | # sentinel hosts 75 | securityContext: 76 | runAsNonRoot: true 77 | readOnlyRootFilesystem: false 78 | allowPrivilegeEscalation: false 79 | capabilities: 80 | drop: 81 | - ALL 82 | 83 | service: 84 | # -- Type of k8s service to export 85 | type: ClusterIP 86 | # -- External port where service is available 87 | port: 80 88 | # -- Add additional annotations for the service spec 89 | # Example: 90 | # 'my.custom.annotation: value' 91 | annotations: {} 92 | # my.custom.annotation: value 93 | 94 | ingress: 95 | # -- Enable Ingress for the service 96 | enabled: false 97 | # -- Use *Legacy*, deprecated Ingress versions. 98 | # Ingress apiVersions prior to `networking.k8s.io/v1` are deprecated and 99 | # removed in kubernetes 1.22. 100 | # Set the `legacy` flag to *true* if you are using kubernetes older than 1.19 or 101 | # OpenShift v3 and require support for the older API versions. 102 | legacy: false 103 | # -- optional name of an IngressClass used for this Ingress, available since k8s 1.18 104 | # https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource 105 | className: "" 106 | # -- Set the pathType for the v1 Ingress resource. This setting is ignored for `legacy` Ingress resources. 107 | # Details on **Path Type** are available here; https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types 108 | pathType: ImplementationSpecific 109 | # -- Add additional annotations for the ingess spec 110 | # Example: 111 | # 'kubernetes.io/ingress.class: nginx' or 'kubernetes.io/tls-acme: "true"' 112 | annotations: {} 113 | # kubernetes.io/ingress.class: nginx 114 | # kubernetes.io/tls-acme: "true" 115 | hosts: 116 | # -- Host name to use for the ingress definition 117 | - host: chart-example.local 118 | # -- list of paths within the given host for path-based routing, otherwise the root path "/" will be used 119 | paths: 120 | - "/" 121 | tls: [] 122 | # - secretName: chart-example-tls 123 | # hosts: 124 | # - chart-example.local 125 | 126 | istio: 127 | # -- Enable Istio VirtualService for the service 128 | # The endpoint (target) is defined by the regular k8s service already defined by the chart 129 | enabled: false 130 | # -- Gateway name to use for the istio definition 131 | gateway: "" 132 | # -- Host name to use for the istio definition 133 | host: "" 134 | # -- Host prefix to use for the istio definition 135 | hostPrefix: "/" 136 | 137 | # -- We usually recommend not to specify default resources and to leave this as a conscious 138 | # choice for the user. This also increases chances charts run on environments with little 139 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 140 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 141 | resources: {} 142 | # limits: 143 | # cpu: 100m 144 | # memory: 128Mi 145 | # requests: 146 | # cpu: 100m 147 | # memory: 128Mi 148 | 149 | # -- Autoscaling configuration for k8s deployment 150 | autoscaling: 151 | enabled: false 152 | minReplicas: 1 153 | maxReplicas: 1 154 | targetCPUUtilizationPercentage: 80 155 | # targetMemoryUtilizationPercentage: 80 156 | 157 | # -- optional set pod node selector definitions for kubernetes 158 | nodeSelector: {} 159 | 160 | # -- optional set pod toleration definitions for kubernetes 161 | tolerations: [] 162 | 163 | # -- optional set pod affinity definitions for kubernetes 164 | affinity: {} 165 | 166 | # -- optional object to set the "local_production_json" property to let Helm render a "local-production.json" 167 | # file from a configmap to preconfigure more complex configuration examples with connection data too 168 | # without the need to set all parameter via environment variables (where available). 169 | # For a working example see either file "example-values-as-json.yaml" where the file content is written as json 170 | # formatted string or file "example-values-as-yml.yaml" with all config values for the file are defined as YAML. 171 | connections: {} 172 | 173 | # -- optional data to add to the configmap generated by this helm chart. 174 | # This might be useful if extra files shall be created inside the docker container which can be mounted 175 | # defining the "volumeMounts" and "volumes" below. 176 | configMapData: {} 177 | 178 | # -- optional list of volumes to mount into the docker deployment. This can either be a local storage volume 179 | # or a configmap to mount data as file. Each list item needs a "name" and a "mountPath". Setting this will most of 180 | # the time also require setting a "volumes" entry. 181 | volumeMounts: [] 182 | 183 | # -- optional list of volumes to mount into the docker deployment. This can either be a local storage volume 184 | # or a configmap to mount data as file. Each list item needs a "name" and a "mountPath". Setting this will most of 185 | # the time also require setting a "volumeMounts" entry. 186 | volumes: [] 187 | -------------------------------------------------------------------------------- /k8s/helm-chart/release_charts: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # author Abdennour Toumi (https://github.com/abdennour) 3 | 4 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"; 5 | cd "${dir}" || exit 6 | helm package redis-commander 7 | helm repo index . 8 | -------------------------------------------------------------------------------- /k8s/redis-commander/deployment-password-protected-redis.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis-commander 6 | annotations: 7 | # Tell Kubernetes to apply the AppArmor or SecComp profile "runtime/default". (whatever is used) 8 | # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater. 9 | # and fails if AppArmor enabled but profile not found (may happens on borked k8s installs only) 10 | # set to "unconfined" to disable AppArmor (first annotation) or SecComp (second annotation) 11 | container.apparmor.security.beta.kubernetes.io/redis-commander: runtime/default 12 | container.security.alpha.kubernetes.io/redis-commander: runtime/default 13 | spec: 14 | replicas: 1 15 | selector: 16 | matchLabels: 17 | app: redis-commander 18 | template: 19 | metadata: 20 | labels: 21 | app: redis-commander 22 | tier: backend 23 | spec: 24 | automountServiceAccountToken: false 25 | containers: 26 | - name: redis-commander 27 | image: ghcr.io/joeferner/redis-commander 28 | imagePullPolicy: Always 29 | env: 30 | - name: REDIS_HOST 31 | value: "redis" 32 | - name: REDIS_PASSWORD 33 | value: "SECRET" 34 | - name: K8S_SIGTERM 35 | value: "1" 36 | ports: 37 | - name: redis-commander 38 | containerPort: 8081 39 | livenessProbe: 40 | httpGet: 41 | path: /favicon.png 42 | port: 8081 43 | initialDelaySeconds: 10 44 | timeoutSeconds: 5 45 | # adapt to your needs base on data stored inside redis (number of keys and size of biggest keys) 46 | # or comment out for less secure installation 47 | resources: 48 | limits: 49 | cpu: "500m" 50 | memory: "512M" 51 | securityContext: 52 | runAsNonRoot: true 53 | readOnlyRootFilesystem: false 54 | allowPrivilegeEscalation: false 55 | capabilities: 56 | drop: 57 | - ALL 58 | -------------------------------------------------------------------------------- /k8s/redis-commander/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis-commander 6 | annotations: 7 | # Tell Kubernetes to apply the AppArmor or SecComp profile "runtime/default". (whatever is used) 8 | # Note that this is ignored if the Kubernetes node is not running version 1.4 or greater. 9 | # and fails if AppArmor enabled but profile not found (may happens on borked k8s installs only) 10 | # set to "unconfined" to disable AppArmor (first annotation) or SecComp (second annotation) 11 | container.apparmor.security.beta.kubernetes.io/redis-commander: runtime/default 12 | container.security.alpha.kubernetes.io/redis-commander: runtime/default 13 | spec: 14 | replicas: 1 15 | selector: 16 | matchLabels: 17 | app: redis-commander 18 | template: 19 | metadata: 20 | labels: 21 | app: redis-commander 22 | tier: backend 23 | spec: 24 | automountServiceAccountToken: false 25 | containers: 26 | - name: redis-commander 27 | image: ghcr.io/joeferner/redis-commander 28 | imagePullPolicy: Always 29 | env: 30 | - name: REDIS_HOSTS 31 | value: "instance1:redis:6379" 32 | - name: K8S_SIGTERM 33 | value: "1" 34 | ports: 35 | - name: redis-commander 36 | containerPort: 8081 37 | livenessProbe: 38 | httpGet: 39 | path: /favicon.png 40 | port: 8081 41 | initialDelaySeconds: 10 42 | timeoutSeconds: 5 43 | # adapt to your needs base on data stored inside redis (number of keys and size of biggest keys) 44 | # or comment out for less secure installation 45 | resources: 46 | limits: 47 | cpu: "500m" 48 | memory: "512M" 49 | securityContext: 50 | runAsNonRoot: true 51 | readOnlyRootFilesystem: false 52 | allowPrivilegeEscalation: false 53 | capabilities: 54 | drop: 55 | - ALL 56 | -------------------------------------------------------------------------------- /k8s/redis-commander/ingress.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: redis-commander-ingress 6 | labels: 7 | app: redis-commander 8 | annotations: 9 | ## ssl + hsts config 10 | # ingress.kubernetes.io/ssl-proxy-headers: "X-Forwarded-Proto:https" 11 | # ingress.kubernetes.io/ssl-redirect: "true" 12 | # ingress.kubernetes.io/force-hsts: "true" 13 | # ingress.kubernetes.io/hsts-max-age: "315360000" 14 | # ingress.kubernetes.io/hsts-preload: "true" 15 | # ingress.kubernetes.io/hsts-include-subdomains: true 16 | spec: 17 | rules: 18 | - host: redis.local 19 | http: 20 | paths: 21 | - path: "/" 22 | backend: 23 | serviceName: redis-commander 24 | servicePort: 8081 25 | -------------------------------------------------------------------------------- /k8s/redis-commander/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: redis-commander 6 | labels: 7 | app: redis-commander 8 | tier: backend 9 | spec: 10 | ports: 11 | - port: 8081 12 | targetPort: redis-commander 13 | name: redis-commander 14 | selector: 15 | app: redis-commander 16 | tier: backend 17 | -------------------------------------------------------------------------------- /k8s/redis/deployment-with-password.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis 6 | annotations: 7 | container.apparmor.security.beta.kubernetes.io/redis-commander: runtime/default 8 | container.security.alpha.kubernetes.io/redis-commander: runtime/default 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: redis 14 | template: 15 | metadata: 16 | labels: 17 | app: redis 18 | tier: backend 19 | spec: 20 | automountServiceAccountToken: false 21 | containers: 22 | - name: redis 23 | image: redis 24 | args: ["--requirepass", "SECRET"] 25 | imagePullPolicy: IfNotPresent 26 | ports: 27 | - name: redis 28 | containerPort: 6379 29 | resources: 30 | limits: 31 | cpu: 1000m 32 | memory: 1Gi 33 | securityContext: 34 | runAsNonRoot: true 35 | readOnlyRootFilesystem: false 36 | allowPrivilegeEscalation: false 37 | capabilities: 38 | drop: 39 | - ALL 40 | -------------------------------------------------------------------------------- /k8s/redis/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: redis 6 | annotations: 7 | container.apparmor.security.beta.kubernetes.io/redis-commander: runtime/default 8 | container.security.alpha.kubernetes.io/redis-commander: runtime/default 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: redis 14 | template: 15 | metadata: 16 | labels: 17 | app: redis 18 | tier: backend 19 | spec: 20 | automountServiceAccountToken: false 21 | containers: 22 | - name: redis 23 | image: redis 24 | imagePullPolicy: IfNotPresent 25 | ports: 26 | - name: redis 27 | containerPort: 6379 28 | resources: 29 | limits: 30 | cpu: 1000m 31 | memory: 1Gi 32 | securityContext: 33 | runAsNonRoot: true 34 | readOnlyRootFilesystem: false 35 | allowPrivilegeEscalation: false 36 | capabilities: 37 | drop: 38 | - ALL 39 | -------------------------------------------------------------------------------- /k8s/redis/service.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: redis 6 | labels: 7 | app: redis 8 | tier: backend 9 | spec: 10 | ports: 11 | - port: 6379 12 | targetPort: redis 13 | name: redis 14 | selector: 15 | app: redis 16 | tier: backend 17 | -------------------------------------------------------------------------------- /lib/express/middlewares.js: -------------------------------------------------------------------------------- 1 | /** this express middleware checks if the global readOnly flag for redis connections 2 | * is set and returns an HTTP 403 if found true. 3 | * This middleware should be used on all API routes that modify data inside redis server 4 | * and should not be allowed while redis-commander is startet read-only. 5 | * 6 | * @param {object} req Express request object 7 | * @param {object} res Express response object 8 | * @param {function} next next Express middleware function to call on success 9 | * @return {*|Promise} 10 | * @private 11 | */ 12 | let _checkReadOnlyMode = function(req, res, next) { 13 | if (req.app.locals.redisReadOnly) { 14 | return res.status(403).json({status: 'FAIL', message: 'read-only mode'}); 15 | } 16 | next(); 17 | }; 18 | 19 | /** method called to extract url parameter 'connectionId' from all routes. 20 | * The connection object found is attached to the res.locals.connection variable for all 21 | * following routes to work with. The connectionId param is attached to res.locals.connectionId. 22 | * 23 | * This method exits with JSON error response if no connection is found. 24 | * 25 | * @param {object} req Express request object 26 | * @param {object} res Express response object 27 | * @param {function} next The next middleware function to call 28 | * @param {string} [connectionId] The value of the connectionId parameter. 29 | */ 30 | function _findConnection (req, res, next, connectionId) { 31 | res.locals.connection = req.app.locals.redisConnections.findByConnectionId(connectionId); 32 | if (res.locals.connection) { 33 | res.locals.connectionId = connectionId; 34 | 35 | // try to reconnect if it is an optional connection and not connected right now 36 | if (res.locals.connection.status === 'end' && res.locals.connection.options.isOptional) { 37 | res.locals.connection.connect(); 38 | } 39 | } 40 | else { 41 | console.error('Connection with id ' + connectionId + ' not found, requested by url ' + req.originalUrl); 42 | return _printError(res, next, null, req.originalUrl); 43 | } 44 | next(); 45 | } 46 | 47 | /** print error message server side and return an HTTP page with text error message to the client. 48 | * 49 | * @param res Express response object 50 | * @param next Express next middleware function 51 | * @param err optional error objet to extract error message from for logging 52 | * @param errFuncName name of function or url the error occured 53 | * @return {*} 54 | */ 55 | function _printError(res, next, err, errFuncName) { 56 | console.error('On ' + errFuncName + ': - no connection'); 57 | if (err) { 58 | console.error('Got error ' + JSON.stringify(err)); 59 | return (typeof next === 'function') ? next(err) : res.send('ERROR: Invalid Connection: ' + JSON.stringify(err)); 60 | } 61 | else { 62 | return res.status(404).end('Not Found'); 63 | } 64 | } 65 | 66 | 67 | exports.checkReadOnlyMode = _checkReadOnlyMode; 68 | exports.findConnection = _findConnection; 69 | exports.printError = _printError; 70 | -------------------------------------------------------------------------------- /lib/ioredis-stream.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is taken from HFXBus (https://github.com/exocet-engineering/hfx-bus) 3 | 4 | HFXBus is a bus implementation for NodeJS backed by Redis Streams and PubSub. 5 | 6 | This is needed to add basic stream support to ioredis library as this is not supported 7 | with current versions (4.9.x) 8 | 9 | License for this file as taken from HFXBus Project: Apache License 2.0 as of 14.04.2019 10 | https://github.com/exocet-engineering/hfx-bus/blob/master/LICENSE 11 | 12 | Author: Exocet 13 | */ 14 | 15 | 'use strict'; 16 | const Redis = require('ioredis'); 17 | 18 | const parseObjectResponse = (reply, customParser = null) => { 19 | if (!Array.isArray(reply)) 20 | return reply; 21 | const data = {}; 22 | for (let i = 0; i < reply.length; i += 2) { 23 | if (customParser) { 24 | data[reply[i]] = customParser(reply[i], reply[i+1]); 25 | continue; 26 | } 27 | data[reply[i]] = reply[i+1]; 28 | } 29 | return data; 30 | }; 31 | 32 | const parseMessageResponse = (reply) => { 33 | if (!Array.isArray(reply)) 34 | return []; 35 | return reply.map((message) => { 36 | return {id:message[0], data:parseObjectResponse(message[1])}; 37 | }); 38 | }; 39 | 40 | const parseStreamResponse = (reply) => { 41 | if (!Array.isArray(reply)) 42 | return reply; 43 | const object = {}; 44 | for (const stream of reply) 45 | object[stream[0]] = parseMessageResponse(stream[1]); 46 | return object; 47 | }; 48 | 49 | const addCommand = { 50 | xgroup:(target) => target.Command.setReplyTransformer('xgroup', (reply) => reply), 51 | xadd:(target) => target.Command.setReplyTransformer('xadd', (reply) => reply), 52 | xread:(target) => target.Command.setReplyTransformer('xread', parseStreamResponse), 53 | xpending:(target) => target.Command.setReplyTransformer('xpending', (reply) => { 54 | if (!reply || reply.length === 0) 55 | return []; 56 | if (reply.length === 4 && !isNaN(reply[0])) 57 | return { 58 | count:parseInt(reply[0]), 59 | minId:reply[1], 60 | maxId:reply[2], 61 | consumers:(reply[3] || []).map((consumer) => { 62 | return { 63 | name:consumer[0], 64 | count:parseInt(consumer[1]) 65 | }; 66 | }) 67 | }; 68 | return reply.map((message) => { 69 | return { 70 | id:message[0], 71 | consumerName:message[1], 72 | elapsedMilliseconds:parseInt(message[2]), 73 | deliveryCount:parseInt(message[3]) 74 | }; 75 | }); 76 | }), 77 | xreadgroup:(target) => target.Command.setReplyTransformer('xreadgroup', parseStreamResponse), 78 | xrange:(target) => target.Command.setReplyTransformer('xrange', parseMessageResponse), 79 | xrevrange:(target) => target.Command.setReplyTransformer('xrevrange', parseMessageResponse), 80 | xclaim:(target) => target.Command.setReplyTransformer('xclaim', parseMessageResponse), 81 | xinfo:(target) => target.Command.setReplyTransformer('xinfo', (reply) => parseObjectResponse(reply, (key, value) => { 82 | switch (key) { 83 | case 'first-entry': 84 | case 'last-entry': 85 | if (!Array.isArray(value)) 86 | return value; 87 | return { 88 | id:value[0], 89 | data:parseObjectResponse(value[1]) 90 | }; 91 | default: 92 | return value; 93 | } 94 | })), 95 | xack:(target) => target.Command.setReplyTransformer('xack', (reply) => parseInt(reply)), 96 | xlen:(target) => target.Command.setReplyTransformer('xlen', (reply) => parseInt(reply)), 97 | xtrim:(target) => target.Command.setReplyTransformer('xtrim', (reply) => parseInt(reply)), 98 | xdel:(target) => target.Command.setReplyTransformer('xdel', (reply) => parseInt(reply)) 99 | }; 100 | 101 | let isPrepared = false; 102 | 103 | module.exports = () => { 104 | 105 | if (isPrepared) 106 | return void 0; 107 | 108 | isPrepared = true; 109 | 110 | Object.keys(addCommand).forEach((command) => { 111 | const {string, buffer} = Redis.prototype.createBuiltinCommand(command); 112 | Redis.prototype[command] = string; 113 | Redis.prototype[command+'Buffer'] = buffer; 114 | addCommand[command](Redis); 115 | }); 116 | 117 | }; 118 | -------------------------------------------------------------------------------- /lib/redisCommands/redisCore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // first start - may be reworked to use 'COMMANDS' or 'COMMAND INFO' command if available (redis >=2.8.13) 4 | // but this will be fallback if they are not working (for whatever reason) 5 | const _readCmdsV3 = [ 6 | 'REFRESH', 7 | 'AUTH password', 8 | 'BITCOUNT key [start] [end]', 9 | 'BITOP operation destkey key [key ...]', 10 | 'BITPOS key bit [start] [end]', 11 | 'CLIENT LIST [TYPE normal|master|replica|pubsub]', 12 | 'CLIENT GETNAME', 13 | 'CLUSTER COUNT-FAILURE-REPORTS node-id', 14 | 'CLUSTER COUNTKEYSINSLOT slot', 15 | 'CLUSTER GETKEYSINSLOT slot count', 16 | 'CLUSTER INFO', 17 | 'CLUSTER KEYSLOT key', 18 | 'CLUSTER NODES', 19 | 'CLUSTER SLAVES node-id', 20 | 'CLUSTER REPLICAS node-id', 21 | 'CLUSTER SLOTS', 22 | 'COMMANDS', 23 | 'COMMAND COUNT', 24 | 'COMMAND GETKEYS', 25 | 'COMMAND INFO command-name [command-name ...]', 26 | 'CONFIG GET parameter', 27 | 'DBSIZE', 28 | 'DEBUG OBJECT key', 29 | 'DISCARD', 30 | 'DUMP key', 31 | 'ECHO message', 32 | 'EXEC', 33 | 'EXISTS key', 34 | 'GET key', 35 | 'GETBIT key offset', 36 | 'GETRANGE key start end', 37 | 'HEXISTS key field', 38 | 'HGET key field', 39 | 'HGETALL key', 40 | 'HKEYS key', 41 | 'HLEN key', 42 | 'HMGET key field [field ...]', 43 | 'HSCAN key cursor [MATCH pattern] [COUNT count]', 44 | 'HVALS key', 45 | 'INFO [section]', 46 | 'KEYS pattern', 47 | 'LASTSAVE', 48 | 'LINDEX key index', 49 | 'LLEN key', 50 | 'LRANGE key start stop', 51 | 'MGET key [key ...]', 52 | 'MONITOR', 53 | 'MULTI', 54 | 'OBJECT subcommand [arguments ...]', 55 | 'PING [message]', 56 | 'PUBSUB CHANNELS [pattern]', 57 | 'PUBSUB NUMSUB [channel-1 ...]', 58 | 'PUBSUB NUMPAT', 59 | 'PSUBSCRIBE pattern [pattern ...]', 60 | 'PTTL key', 61 | 'PUNSUBSCRIBE [pattern ...]', 62 | 'QUIT', 63 | 'RANDOMKEY', 64 | 'ROLE', 65 | 'SCAN cursor [MATCH pattern] [COUNT count]', 66 | 'SCARD key', 67 | 'SDIFF key [key ...]', 68 | 'SELECT index', 69 | 'SINTER key [key ...]', 70 | 'SISMEMBER key member', 71 | 'SMEMBERS key', 72 | 'SRANDMEMBER key [count]', 73 | 'SSCAN key cursor [MATCH pattern] [COUNT count]', 74 | 'STRLEN key', 75 | 'SUBSCRIBE channel [channel ...]', 76 | 'SUNION key [key ...]', 77 | 'TIME', 78 | 'TTL key', 79 | 'TYPE key', 80 | 'UNSUBSCRIBE [channel ...]', 81 | 'UNWATCH', 82 | 'WATCH key [key ...]', 83 | 'ZCARD key', 84 | 'ZCOUNT key min max', 85 | 'ZLEXCOUNT key min max', 86 | 'ZRANGE key start stop [WITHSCORES]', 87 | 'ZRANGEBYLEX key min max [LIMIT offset count]', 88 | 'ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]', 89 | 'ZRANK key member', 90 | 'ZREVRANGE key start stop [WITHSCORES]', 91 | 'ZREVRANGEBYLEX key max min [LIMIT offset count]', 92 | 'ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]', 93 | 'ZREVRANK key member', 94 | 'ZSCAN key cursor [MATCH pattern] [COUNT count]', 95 | 'ZSCORE key member' 96 | ]; 97 | 98 | const _readCmdsV32 = _readCmdsV3.concat([ 99 | 'CLIENT REPLY ON|OFF|SKIP', 100 | 'GEOHASH key member [member ...]', 101 | 'GEOPOS key member [member ...]', 102 | 'GEODIST key member1 member2 [unit]', 103 | 'HSTRLEN key field' 104 | ]); 105 | 106 | const _readCmdsV4 = _readCmdsV32.concat([ 107 | 'MEMORY DOCTOR', 108 | 'MEMORY HELP', 109 | 'MEMORY MALLOC-STATS', 110 | 'MEMORY STATS', 111 | 'MEMORY USAGE key [SAMPLES count]' 112 | ]); 113 | 114 | const _readCmdsV5 = _readCmdsV4.concat([ 115 | 'CLIENT ID', 116 | 'XINFO [CONSUMERS key groupname] [GROUPS key] [STREAM key] [HELP]', 117 | 'XRANGE key start end [COUNT count]', 118 | 'XREVRANGE key end start [COUNT count]', 119 | 'XLEN key', 120 | 'XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]', 121 | 'XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]', 122 | 'XPENDING key group [start end count] [consumer]' 123 | ]); 124 | 125 | // redis v6 with redis stack add loadable module support with 126 | // redisjson redissearch, redisgraph, redistimeseries, redisbloom 127 | // https://redis.io/docs/stack/ 128 | const _readCmdsV6 = _readCmdsV4.concat([ 129 | 'JSON.ARRINDEX key path value [start [stop]]', 130 | 'JSON.ARRLEN key [path]', 131 | 'JSON.GET key [INDENT indent] [NEWLINE newline] [SPACE space] [paths [paths ...]]', 132 | 'JSON.MGET key [key ...] path', 133 | 'JSON.OBJKEYS key [path]', 134 | 'JSON.OBJLEN key [path]', 135 | 'JSON.RESP key [path]', 136 | 'JSON.STRLEN key [path]', 137 | 'JSON.TYPE key [path]', 138 | 'GRAPH.CONFIG GET name', 139 | 'GRAPH.EXPLAIN graph query', 140 | 'GRAPH.LIST', 141 | 'GRAPH.RO_QUERY graph query', 142 | 'GRAPH.SLOWLOG graph' 143 | ]); 144 | 145 | 146 | 147 | module.exports = { 148 | readCmds: _readCmdsV5, 149 | writeCmds: [ 150 | 'APPEND key value', 151 | 'BGREWRITEAOF', 152 | 'BGSAVE', 153 | 'BLPOP key [key ...] timeout', 154 | 'BRPOP key [key ...] timeout', 155 | 'BRPOPLPUSH source destination timeout', 156 | 'CONFIG SET parameter value', 157 | 'CONFIG RESETSTAT', 158 | 'DEBUG SEGFAULT', 159 | 'DECR key', 160 | 'DECRBY key decrement', 161 | 'DEL key [key ...]', 162 | 'DISCARD', 163 | 'EVAL script numkeys key [key ...] arg [arg ...]', 164 | 'EVALSHA sha1 numkeys key [key ...] arg [arg ...]', 165 | 'EXPIRE key seconds', 166 | 'EXPIREAT key timestamp', 167 | 'FLUSHALL', 168 | 'FLUSHDB', 169 | 'GETSET key value', 170 | 'HDEL key field [field ...]', 171 | 'HINCRBY key field increment', 172 | 'HINCRBYFLOAT key field increment', 173 | 'HMSET key field value [field value ...]', 174 | 'HSET key field value', 175 | 'HSETNX key field value', 176 | 'INCR key', 177 | 'INCRBY key increment', 178 | 'INCRBYFLOAT key increment', 179 | 'LINSERT key BEFORE|AFTER pivot value', 180 | 'LPOP key', 181 | 'LPUSH key value [value ...]', 182 | 'LPUSHX key value', 183 | 'LREM key count value', 184 | 'LSET key index value', 185 | 'LTRIM key start stop', 186 | 'MIGRATE host port key destination-db timeout', 187 | 'MOVE key db', 188 | 'MSET key value [key value ...]', 189 | 'MSETNX key value [key value ...]', 190 | 'OBJECT subcommand [arguments ...]', 191 | 'PERSIST key', 192 | 'PEXPIRE key milliseconds', 193 | 'PEXPIREAT key milliseconds-timestamp', 194 | 'PSETEX key milliseconds value', 195 | 'PSUBSCRIBE pattern [pattern ...]', 196 | 'PUBLISH channel message', 197 | 'RENAME key newkey', 198 | 'RENAMENX key newkey', 199 | 'RESTORE key ttl serialized-value', 200 | 'RPOP key', 201 | 'RPOPLPUSH source destination', 202 | 'RPUSH key value [value ...]', 203 | 'RPUSHX key value', 204 | 'SADD key member [member ...]', 205 | 'SAVE', 206 | 'SCRIPT EXISTS script [script ...]', 207 | 'SCRIPT FLUSH', 208 | 'SCRIPT KILL', 209 | 'SCRIPT LOAD script', 210 | 'SDIFFSTORE destination key [key ...]', 211 | 'SET key value', 212 | 'SETBIT key offset value', 213 | 'SETEX key seconds value', 214 | 'SETNX key value', 215 | 'SETRANGE key offset value', 216 | 'SHUTDOWN [NOSAVE|SAVE]', 217 | 'SINTERSTORE destination key [key ...]', 218 | 'SLAVEOF host port', 219 | 'SLOWLOG subcommand [argument]', 220 | 'SMOVE source destination member', 221 | 'SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]', 222 | 'SPOP key', 223 | 'SREM key member [member ...]', 224 | 'SUNIONSTORE destination key [key ...]', 225 | 'SYNC', 226 | 'ZADD key score member [score] [member]', 227 | 'ZINCRBY key increment member', 228 | 'ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]', 229 | 'ZREM key member [member ...]', 230 | 'ZREMRANGEBYLEX key min max', 231 | 'ZREMRANGEBYRANK key start stop', 232 | 'ZREMRANGEBYSCORE key min max', 233 | 'ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]' 234 | ] 235 | }; 236 | -------------------------------------------------------------------------------- /lib/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (app, urlPrefix) { 4 | app.use(`${urlPrefix}/`, require('./home')()); 5 | let apiRoutes = require('./apiv1')(app); 6 | app.use(`${urlPrefix}/apiv1`, apiRoutes.apiv1); 7 | app.use(`${urlPrefix}/apiv2`, apiRoutes.apiv2); 8 | app.use(`${urlPrefix}/tools`, require('./tools')()); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/routes/tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tools as export, import, flush ... 3 | * 4 | * @author Dmitriy Yurchenko 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = function() { 10 | 11 | const config = require('config'); 12 | const express = require('express'); 13 | const router = express.Router(); 14 | const RedisDump = require('node-redis-dump2'); 15 | const myUtils = require('../util'); 16 | const middlewares = require('../express/middlewares'); 17 | 18 | let _findConnection = function(req, res, next) { 19 | let connectionId = req.query.connection || req.body.connection; 20 | if (!connectionId) return res.status(422).end('ConnectionId missing'); 21 | middlewares.findConnection(req, res, next, connectionId); 22 | }; 23 | 24 | /** 25 | * Make dump by redis database. 26 | */ 27 | router.get('/export', _findConnection, function (req, res) { 28 | let exportType = req.query.type; 29 | let keyPrefix = req.query.keyPrefix; 30 | let dump = new RedisDump({client: res.locals.connection}); 31 | 32 | dump.export({ 33 | type: exportType || 'redis', 34 | keyPrefix: keyPrefix, 35 | callback: function(err, data) { 36 | if (err) { 37 | console.error('Could\'t not make redis dump!', err); 38 | return res.status(500).end('Error on dump'); 39 | } 40 | 41 | res.setHeader('Content-disposition', 'attachment; filename=db.' + (new Date().getTime()) + '.redis'); 42 | res.setHeader('Content-Type', 'application/octet-stream'); 43 | 44 | switch (exportType) { 45 | case 'json': 46 | res.end(JSON.stringify(data)); 47 | break; 48 | 49 | default: 50 | res.end(data); 51 | break; 52 | } 53 | } 54 | }); 55 | }); 56 | 57 | /** 58 | * Import redis data. 59 | */ 60 | router.post('/import', middlewares.checkReadOnlyMode, _findConnection, function (req, res) { 61 | let dump = new RedisDump({client: res.locals.connection}); 62 | try { 63 | // check if it is a redis RESTORE or RESTOREB64 command - change import type than to dump with base 64 encoded binary data 64 | // use default redis otherwise 65 | if (typeof req.body.data !== 'string') throw new Error('invalid import data send in body'); 66 | 67 | let importType = 'redis'; 68 | let reDump = /^RESTORE(B64)?\s/mi; 69 | if (reDump.test(req.body.data.trimStart())) { 70 | importType = 'dump-base64'; 71 | } 72 | dump.import({ 73 | type: importType, 74 | data: req.body.data, 75 | clear: req.body.clear, 76 | callback: function(err, report) { 77 | report.status = 'OK'; 78 | if (err) { 79 | report.status = 'FAIL'; 80 | console.error('Could\'t not import redis data!', err); 81 | } 82 | 83 | res.end(JSON.stringify(report)); 84 | } 85 | }); 86 | } 87 | catch(e) { 88 | console.error('Could\'t not import redis data! Exception:', e); 89 | res.json({inserted: 0, errors: -1, status: 'FAIL', message: 'Exception processing inport data'}); 90 | } 91 | }); 92 | 93 | /** 94 | * Export form. 95 | * 96 | * connections - list of all redis connections for drop-down 97 | */ 98 | router.get('/forms/export', function (req, res) { 99 | res.render('tools/exportData.ejs', { 100 | connections: req.app.locals.redisConnections.convertConnectionsInfoForUI(), 101 | layout: false 102 | }); 103 | }); 104 | 105 | /** 106 | * Import form. 107 | * 108 | * connections - list of all redis connections for drop-down 109 | * flushOnImport - default state of checkbox flushdb (either checked or nothing (=unchecked)) 110 | */ 111 | router.get('/forms/import', middlewares.checkReadOnlyMode, function (req, res) { 112 | res.render('tools/importData.ejs', { 113 | connections: req.app.locals.redisConnections.convertConnectionsInfoForUI(), 114 | flushOnImport: config.get('redis.flushOnImport') ? 'checked' : '', 115 | layout: false 116 | }); 117 | }); 118 | 119 | return router; 120 | }; 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Joe Ferner ", 3 | "contributors": [ 4 | "Evan Oxfeld ", 5 | "Dmitriy Yurchenko ", 6 | "Alex Ehrnschwender ", 7 | "Matthew Scragg ", 8 | "Stefan Seide =12.0" 62 | }, 63 | "bin": { 64 | "redis-commander": "./bin/redis-commander.js" 65 | }, 66 | "preferGlobal": true, 67 | "scripts": { 68 | "build": "browserify -r readline-browserify -r cmdparser -r lossless-json | uglifyjs -cm -o web/static/scripts/browserify.js", 69 | "healthcheck": "./bin/healthcheck.js", 70 | "postinstall": "echo '==> INFO: Errors with module \"bcrypt\" can be ignored'", 71 | "snyk-protect": "snyk protect", 72 | "helm-doc": "helm-docs -c k8s/helm-chart", 73 | "helm-test": "helm template redis-commander -f k8s/helm-chart/redis-commander/values.yaml k8s/helm-chart/redis-commander -n redis | kubeval", 74 | "helm-lint": "docker run -it --workdir=/data --volume \"$(pwd)/k8s/helm-chart/redis-commander:/data\" quay.io/helmpack/chart-testing ct lint --charts . --validate-maintainers=false", 75 | "test": "mocha", 76 | "test-with-lcov": "nyc --reporter=lcov --report-dir ./docs/coverage/nodejs mocha --timeout 10000 --reporter mocha-junit-reporter --reporter-options mochaFile=./docs/coverage/junit.xml --exit", 77 | "sbom": "cyclonedx-npm --omit optional --mc-type application --output-format json --output-file ./sbom.json" 78 | }, 79 | "snyk": true 80 | } 81 | -------------------------------------------------------------------------------- /test/testUtil.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const myUtil = require("../lib/util"); 3 | const expect = chai.expect; 4 | 5 | describe('Test util.js helpers', function() { 6 | const myUtil = require('../lib/util'); 7 | 8 | describe('Test command split function', function() { 9 | 10 | it('test standard split', function () { 11 | const result = myUtil.split("set key value"); 12 | expect(result).to.deep.equal(['set', 'key', 'value']); 13 | }); 14 | 15 | it('test empty quotes', function () { 16 | const result = myUtil.split("get \"\""); 17 | expect(result).to.deep.equal(['get', ""]); 18 | }); 19 | 20 | it('test quoted key', function () { 21 | let result = myUtil.split('set "key" value'); 22 | expect(result, 'extract key with quotes no space').to.deep.equal(['set', 'key', 'value']); 23 | 24 | result = myUtil.split('set "key b" value'); 25 | expect(result, 'extract key with quotes and space').to.deep.equal(['set', 'key b', 'value']); 26 | }); 27 | 28 | it('test backslash ignores next character', function () { 29 | const result = myUtil.split('set "key\\ name" value'); 30 | expect(result).to.deep.equal(['set', 'key name', 'value']); 31 | }); 32 | 33 | it('test handles single ticks', function () { 34 | let result = myUtil.split("set 'keyname' value"); 35 | expect(result, 'extract key with quotes no space').to.deep.equal(['set', 'keyname', 'value']); 36 | 37 | result = myUtil.split("set 'keyname b' value"); 38 | expect(result, 'extract key with quotes and space').to.deep.equal(['set', 'keyname b', 'value']); 39 | }); 40 | 41 | it('test ignores unterminated strings', function () { 42 | let result = myUtil.split('set "keyname value'); 43 | expect(result, 'ignores unterminated double ticks').to.deep.equal(['set']); 44 | 45 | result = myUtil.split("set 'keyname value"); 46 | expect(result, 'ignores unterminated single ticks').to.deep.equal(['set']); 47 | }); 48 | }); 49 | 50 | describe('Test distinct function', function() { 51 | }); 52 | 53 | describe('Test decodeHTMLEntities function', function() { 54 | }); 55 | 56 | describe('Test encodeHTMLEntities function', function() { 57 | }); 58 | 59 | describe('Test createRedisClient function', function() { 60 | }); 61 | 62 | describe('Test hasDeprecatedConfig function', function() { 63 | }); 64 | 65 | describe('Test getDeprecatedConfig function', function() { 66 | }); 67 | 68 | describe('Test getDeprecatedConfigPath function', function() { 69 | }); 70 | 71 | describe('Test deleteDeprecatedConfig function', function() { 72 | }); 73 | 74 | describe('Test migrateDeprecatedConfig function', function() { 75 | }); 76 | 77 | describe('Test containsConnection function', function() { 78 | }); 79 | 80 | describe('Test findConnection function', function() { 81 | }); 82 | 83 | describe('Test replaceConnection function', function() { 84 | }); 85 | 86 | describe('Test saveConnections function', function() { 87 | }); 88 | 89 | describe('Test convertConnectionInfoForUI function', function() { 90 | }); 91 | 92 | describe('Test saveLocalConfig function', function() { 93 | }); 94 | 95 | describe('Test deleteConfig function', function() { 96 | }); 97 | 98 | describe('Test validateConfig function', function() { 99 | }); 100 | 101 | describe('Test getRedisSentinelGroupName function', function() { 102 | }); 103 | 104 | describe('Test parseRedisSentinel function', function() { 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /web/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /web/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /web/static/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 4 | font-size: 13px; 5 | line-height: 18px; 6 | color: #333333; 7 | background-color: #ffffff; 8 | } 9 | 10 | .navbar-inner { 11 | min-height: 40px; 12 | padding-right: 20px; 13 | padding-left: 20px; 14 | background-color: #2c2c2c; 15 | background-image: -moz-linear-gradient(top, #333333, #222222); 16 | background-image: -ms-linear-gradient(top, #333333, #222222); 17 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); 18 | background-image: -webkit-linear-gradient(top, #333333, #222222); 19 | background-image: -o-linear-gradient(top, #333333, #222222); 20 | background-image: linear-gradient(to top, #333333, #222222); 21 | background-repeat: repeat-x; 22 | -webkit-border-radius: 4px; 23 | -moz-border-radius: 4px; 24 | border-radius: 4px; 25 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); 26 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 27 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 28 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .navbar .brand { 32 | padding: 0 0 0 20px; 33 | } 34 | 35 | #sideBar { 36 | position: fixed; 37 | top: 40px; 38 | left: 5px; 39 | width: 250px; 40 | border-right: 1px solid #DDD; 41 | z-index: 1; 42 | } 43 | 44 | #sidebarResize { 45 | position: absolute; 46 | top: 0; 47 | right: -11px; 48 | height: 100%; 49 | width: 1px; 50 | background-color: transparent; 51 | border-width: 0 10px; 52 | border-style: solid; 53 | border-color: transparent; 54 | cursor: col-resize; 55 | } 56 | 57 | #keyTree { 58 | overflow: scroll; 59 | margin-right: 10px; 60 | } 61 | 62 | #keyTreeActions { 63 | margin-bottom: 5px; 64 | margin-top: 5px; 65 | } 66 | 67 | #keyTreeActions .btn-mini [class^="icon-"], #keyTreeActions .btn-mini [class*=" icon-"] { 68 | margin-top: 0; 69 | vertical-align: top; 70 | } 71 | 72 | #itemTree, 73 | #itemData { 74 | overflow-x: auto; 75 | margin-top: 62px; 76 | margin-right: 12px; 77 | } 78 | 79 | #itemData .btn-mini [class^="icon-"], #itemData .btn-mini [class*=" icon-"] { 80 | margin-top: 1px; 81 | } 82 | 83 | #itemData tbody td { 84 | border-top: 0; 85 | } 86 | 87 | #itemData tbody tr { 88 | border-top: 1px solid #dddddd; 89 | } 90 | 91 | #body { 92 | position: relative; 93 | overflow-y: auto; 94 | overflow-x: hidden; 95 | left: 250px; 96 | margin-left: 30px; 97 | } 98 | 99 | #itemActionsBar { 100 | position: fixed; 101 | top: 0; 102 | left: inherit; 103 | margin-left: 30px; 104 | width: inherit; 105 | margin-top: 30px; 106 | padding-bottom: 10px; 107 | padding-top: 20px; 108 | background-color: #fff; 109 | border-bottom: 1px solid #ddd; 110 | } 111 | 112 | #commandLineContainer { 113 | position: fixed; 114 | bottom: 0; 115 | left: 0; 116 | background-color: #fff; 117 | width: 100%; 118 | z-index: 1039; 119 | } 120 | 121 | #commandLineContainer .btn-group { 122 | white-space: normal; 123 | } 124 | 125 | #commandLineContainer form { 126 | margin: 0; 127 | } 128 | 129 | #commandLineContainer input { 130 | margin: 0; 131 | box-shadow: none; 132 | width: 900px; 133 | } 134 | 135 | #commandLineContainer label { 136 | color: #888; 137 | font-size: 12px; 138 | padding-left: 15px; 139 | font-family: Menlo, "Courier New", monospace; 140 | } 141 | 142 | #commandLineContainer .btn { 143 | font-size: 13px; 144 | } 145 | 146 | .prompt { 147 | color: #888; 148 | font-size: 12px; 149 | padding-left: 15px; 150 | font-family: Menlo, "Courier New", monospace; 151 | } 152 | 153 | #commandLineOutput { 154 | height: 320px; 155 | font-size: 12px; 156 | display: none; 157 | padding-left: 15px; 158 | font-family: Menlo, "Courier New", monospace; 159 | overflow: scroll; 160 | } 161 | 162 | #commandLine input { 163 | outline: none; 164 | border: 0; 165 | color: #333; 166 | font-weight: bold; 167 | font-size: 12px; 168 | font-family: Menlo, "Courier New", monospace; 169 | } 170 | 171 | .autocompletePopup { 172 | padding: 5px; 173 | border: 1px solid #ddd; 174 | background-color: #fff; 175 | } 176 | 177 | .autocompletePopup div { 178 | padding: 5px; 179 | cursor: pointer; 180 | } 181 | 182 | .autocompletePopup .selected { 183 | color: #fff; 184 | background-color: #00f; 185 | } 186 | 187 | .commandLineCommand { 188 | font-weight: bold; 189 | } 190 | 191 | #lockCommandButton { 192 | position: absolute; 193 | right: 20px; 194 | top: 10px; 195 | } 196 | 197 | #pageNav { 198 | display: inline-block; 199 | float: right; 200 | margin-right: 20px; 201 | margin-bottom: 0; 202 | } 203 | 204 | #lockCommandButton.disabled { 205 | opacity: 0.2; 206 | } 207 | 208 | #lockCommandButton.disabled:hover { 209 | opacity: 0.4; 210 | } 211 | 212 | .select-disabled { 213 | -webkit-user-select: none; 214 | -moz-user-select: none; 215 | user-select: none; 216 | } 217 | 218 | .show-vertical-scroll { 219 | cursor: row-resize; 220 | } 221 | 222 | #redisCommandsModal { 223 | overflow: hidden; 224 | } 225 | 226 | #redisCommandsModal .modal-head { 227 | color: white; 228 | position: absolute; 229 | left: 12rem; 230 | top: 6px; 231 | z-index: 99999; 232 | 233 | } 234 | #redisCommandsModal .modal-body { 235 | background-color: transparent; 236 | padding: 0; 237 | overflow: hidden; 238 | } 239 | 240 | #redisCommandsModal iframe { 241 | border-radius: 6px; 242 | border: none; 243 | width: 100%; 244 | height: 400px; 245 | } 246 | 247 | #redisCommandsClose { 248 | color: white; 249 | font-size: unset; 250 | font-weight: normal; 251 | line-height: unset; 252 | opacity: 1; 253 | text-shadow: none; 254 | } 255 | 256 | #redisCommandsClose:hover { 257 | opacity: 0.4; 258 | } 259 | 260 | #redisCommandsExternal { 261 | color: white; 262 | } 263 | 264 | #redisCommandsExternal:hover { 265 | opacity: 0.4; 266 | } 267 | 268 | #addServerContainer { 269 | padding-left: 35px; 270 | } 271 | 272 | #scoreWrap, #fieldWrap, #timestampWrap, #fieldValueWrap { 273 | display: none; 274 | } 275 | 276 | #scoreWrap > input, #fieldWrap > input, #timestampWrap > input, #fieldValueWrap > input, #fieldValueWrap > textarea { 277 | margin-bottom: 10px; 278 | } 279 | 280 | #vakata-contextmenu { 281 | background: white !important; 282 | border-radius: 5px; 283 | } 284 | 285 | #vakata-contextmenu li ins { 286 | margin-top: 2px; 287 | margin-left: 20px; 288 | } 289 | 290 | .btn-mini { 291 | padding: 2px 6px; 292 | font-size: 11px; 293 | line-height: 14px; 294 | } 295 | 296 | .btn-mini .caret { 297 | margin-top: 5px; 298 | opacity: 0.3; 299 | filter: alpha(opacity=30); 300 | } 301 | 302 | #importData { 303 | width: 97%; 304 | height: 300px; 305 | } 306 | 307 | .modal .form-horizontal, .modal .form-vertical { 308 | margin: 0; 309 | } 310 | 311 | .label-big .lead { 312 | margin-bottom: 10px; 313 | margin-top: 10px; 314 | } 315 | 316 | #signinForm { 317 | margin: 0; 318 | } 319 | 320 | #addKeyForm { 321 | margin: 0; 322 | } 323 | 324 | .modal-content-width { 325 | margin-left: auto; 326 | margin-right: auto; 327 | width: calc(100% - 15px); 328 | } 329 | 330 | .control-group input, .control-group select{ 331 | margin-bottom: 0; 332 | } 333 | 334 | .validate-negative, .validate-negative:focus { 335 | background-color: #fff; 336 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05), inset 0 0 6px #ff1a1a; 337 | } 338 | 339 | .validate-positive, .validate-positive:focus { 340 | background-color: #fff; 341 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), inset 0 0 6px #87d738; 342 | } 343 | 344 | .redisDumpSelect { 345 | min-width: 230px; 346 | width: auto; 347 | } 348 | /* without explicit z-index tree and contextmenu marged into one layer, preventing clicks 349 | must be larger than command line z-index 350 | */ 351 | .jstree-contextmenu { 352 | z-index: 1100; 353 | } 354 | 355 | input[type="checkbox"].checkbox-oneline { 356 | margin: 0 0 0 10px; 357 | overflow: hidden; 358 | position: relative; 359 | top: -3px; 360 | vertical-align: bottom; 361 | } 362 | 363 | .small-label.form-horizontal .control-label { 364 | width: 100px 365 | } 366 | 367 | .small-label.form-horizontal .controls { 368 | margin-left: 120px; 369 | } 370 | 371 | #selectServerDbModal { 372 | width: 650px; 373 | } 374 | 375 | #selectServerDbList { 376 | margin-top: 16px; 377 | } 378 | 379 | #selectServerDbList input[type="checkbox"] { 380 | margin-bottom: 7px; 381 | margin-top: 5px; 382 | } 383 | 384 | #selectServerDbList input[type="text"], 385 | #selectServerDbList label { 386 | margin-bottom: 0; 387 | } 388 | 389 | #selectServerDbList table td { 390 | vertical-align: middle; 391 | } 392 | 393 | .binaryView { 394 | font-size: 14px; 395 | line-height: 20px; 396 | margin-top: 16px; 397 | text-align: center; 398 | } 399 | .binaryView span { 400 | cursor: pointer; 401 | float: left; 402 | height: 20px; 403 | margin-left: 2px; 404 | } 405 | .binaryView span:hover { 406 | background: lightgray; 407 | } 408 | .binaryView span.current { 409 | background: #beebff; 410 | } 411 | .binaryView-offset { 412 | float: left; 413 | text-align: left; 414 | width: 70px; 415 | } 416 | .binaryView-hex { 417 | float: left; 418 | margin-left: 20px; 419 | width: 315px; 420 | } 421 | .binaryView-hex span { 422 | width: 20px; 423 | } 424 | .binaryView-char { 425 | float: left; 426 | margin-left: 20px; 427 | width: 165px; 428 | } 429 | .binaryView-char span { 430 | width: 10px; 431 | } 432 | 433 | .icon-empty { 434 | min-width: 14px; 435 | } 436 | 437 | #clusterNodesModal { 438 | width: 80%; 439 | left: 10%; 440 | margin-left: unset; 441 | } 442 | 443 | #clusterNodesTab th, 444 | #clusterNodesTab td { 445 | padding: 0.5rem; 446 | } 447 | 448 | -------------------------------------------------------------------------------- /web/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/favicon.png -------------------------------------------------------------------------------- /web/static/healthcheck: -------------------------------------------------------------------------------- 1 | ok 2 | -------------------------------------------------------------------------------- /web/static/images/RedisCommandLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/RedisCommandLogo.png -------------------------------------------------------------------------------- /web/static/images/icon-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-download.png -------------------------------------------------------------------------------- /web/static/images/icon-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-edit.png -------------------------------------------------------------------------------- /web/static/images/icon-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-info.png -------------------------------------------------------------------------------- /web/static/images/icon-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-plus.png -------------------------------------------------------------------------------- /web/static/images/icon-refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-refresh.png -------------------------------------------------------------------------------- /web/static/images/icon-trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/icon-trash.png -------------------------------------------------------------------------------- /web/static/images/treeBinary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeBinary.png -------------------------------------------------------------------------------- /web/static/images/treeHash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeHash.png -------------------------------------------------------------------------------- /web/static/images/treeJson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeJson.png -------------------------------------------------------------------------------- /web/static/images/treeList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeList.png -------------------------------------------------------------------------------- /web/static/images/treeRoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeRoot.png -------------------------------------------------------------------------------- /web/static/images/treeRootDisconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeRootDisconnect.png -------------------------------------------------------------------------------- /web/static/images/treeSet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeSet.png -------------------------------------------------------------------------------- /web/static/images/treeStream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeStream.png -------------------------------------------------------------------------------- /web/static/images/treeString.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeString.png -------------------------------------------------------------------------------- /web/static/images/treeZSet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeferner/redis-commander/e9851bf314a84d5b10c04d92af5bebd50400c189/web/static/images/treeZSet.png -------------------------------------------------------------------------------- /web/static/scripts/binaryView.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var BinaryView = {}; 4 | 5 | /** converts an given integer to its hex representation. 6 | * hex values shorter than 'length' (e.g. 'a') are prefix with an '0' 7 | * 8 | * @param {number} number value to convert to hex 9 | * @param {number} length number of chars needed, may be prefixed with zero to match length 10 | * @return {string} hex string representation with given length 11 | */ 12 | BinaryView.toHex = function (number, length) { 13 | var s = number.toString(16).toUpperCase(); 14 | while (s.length < length) { 15 | s = '0' + s; 16 | } 17 | return s; 18 | }; 19 | 20 | /** converts an given integer to either a one byte character. 21 | * first numbers (control characters) are return as a '.' to be visible 22 | * 23 | * @param {number} number number to get char from 24 | * @return {string} printable character or '.' for control chars 25 | */ 26 | BinaryView.toChar = function(number) { 27 | return number <= 32 ? '.' : String.fromCharCode(number); 28 | }; 29 | 30 | /** select booth char items (hex and char view) and adds class 'current' to it. 31 | * It searches all 'span' children of the nearest 'binaryView-hex' and 'binaryView-char' 32 | * class elements contained inside the parent 'binaryView'. 33 | * 34 | * @param {Element} e node element to select together with coresponding other 35 | * @param {string} otherClass either 'binaryView-hex' or 'binaryView-char' to select other char representation 36 | * of same index too 37 | */ 38 | BinaryView.selectItem = function(e, otherClass) { 39 | var itemChar = $(e); 40 | var idx = itemChar.parent().children('span').removeClass('current').index(itemChar); 41 | itemChar.addClass('current'); 42 | $(itemChar.closest('.binaryView').find(otherClass + ' > span').removeClass('current').get(idx)).addClass('current') 43 | }; 44 | 45 | /* following two methods are taken from 46 | * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 47 | * 48 | * Base64 / binary data / UTF-8 strings utilities (#1) 49 | * Author: madmurphy 50 | */ 51 | 52 | /** Array of bytes to base64 string decoding 53 | * 54 | * @param {number} nChr character number 55 | * @return {number} uint value 56 | */ 57 | BinaryView.b64ToUint6 = function(nChr) { 58 | return nChr > 64 && nChr < 91 ? 59 | nChr - 65 60 | : nChr > 96 && nChr < 123 ? 61 | nChr - 71 62 | : nChr > 47 && nChr < 58 ? 63 | nChr + 4 64 | : nChr === 43 ? 65 | 62 66 | : nChr === 47 ? 67 | 63 68 | : 69 | 0; 70 | }; 71 | 72 | /** decode base64 string to an array 73 | * 74 | * @param {string} sBase64 base64 encoded string 75 | * @param {number} [nBlockSize] 76 | * @return {Uint8Array} decoded string as uint8 array 77 | */ 78 | BinaryView.base64DecToArr = function(sBase64, nBlockSize) { 79 | var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length; 80 | var nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2; 81 | var aBytes = new Uint8Array(nOutLen); 82 | 83 | for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { 84 | nMod4 = nInIdx & 3; 85 | nUint24 |= this.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; 86 | if (nMod4 === 3 || nInLen - nInIdx === 1) { 87 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { 88 | aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; 89 | } 90 | nUint24 = 0; 91 | } 92 | } 93 | return aBytes; 94 | }; 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /web/static/scripts/jquery.resize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery resize event - v1.1 - 3/14/2010 3 | * http://benalman.com/projects/jquery-resize-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this); -------------------------------------------------------------------------------- /web/static/templates/connectionsBar.ejs: -------------------------------------------------------------------------------- 1 | <% connections.forEach(function(connection, index) {%> 2 | 14 | <% }); %> 15 | -------------------------------------------------------------------------------- /web/static/templates/detectRedisDb.ejs: -------------------------------------------------------------------------------- 1 |
<%= title %>
2 | <%if (infoMessage) { %> 3 |
<%= infoMessage %>
4 | <% } %> 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% dbs.forEach(function(item) { %> 16 | 17 | 18 | 19 | 20 | 21 | <% }) %> 22 | 23 |
DB #Display-NameUsage Information
<%= item.keys %>
24 |
25 | -------------------------------------------------------------------------------- /web/static/templates/editBinary.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 4 | <% } %> 5 |
6 |
7 | 8 | 9 | 10 | 11 |
12 |
13 | <% for (var pos in positions) { %> 14 | <%= positions[pos] %> 15 | <% } %> 16 |
17 |
18 | <% for (var itemHex in value) { 19 | if (value.hasOwnProperty(itemHex)) { %> 20 | <%= BinaryView.toHex(value[itemHex], 2) %> 21 | <% } 22 | } %> 23 |
24 |
25 | <% for (var itemChar in value) { 26 | if (value.hasOwnProperty(itemChar)) { %> 27 | <%= BinaryView.toChar(value[itemChar]) %> 28 | <% } 29 | } %> 30 |
31 |
32 | 33 |
34 | 35 | 37 | -------------------------------------------------------------------------------- /web/static/templates/editBranch.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 4 | 5 | <% } %> 6 |
7 |
8 | 9 |
10 | -------------------------------------------------------------------------------- /web/static/templates/editHash.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 6 | 7 | <% } %> 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% for(var field in data) { %> 24 | 26 | data-deferred-field="<%= field %>" 27 | <% } %> 28 | > 29 | 30 | <% console.log(field);%> 31 | <% console.log(data[field]);%> 32 | 37 | 38 | 39 | <% } %> 40 | 41 |
FieldValue
<%= field %><%_ if (data[field] !== null) { _%> 33 | <%= data[field] -%> 34 | <%_ } else { _%> 35 | This is a large field. Click here to view. 36 | <%_ } _%>
42 |
43 | 70 | -------------------------------------------------------------------------------- /web/static/templates/editList.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 6 | 7 | <% } %> 8 | 9 | 10 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | <% items.forEach(function(item) { %> 43 | 44 | 45 | 46 | 47 | 48 | <% }) %> 49 | 50 |
#Value
<%= item.number %><%= item.value %>
51 |
52 | 68 | -------------------------------------------------------------------------------- /web/static/templates/editReJSON.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 4 | <% } %> 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 | <% if (!redisReadOnly) { %> 23 | 24 | <% } %> 25 |
26 |
27 | 63 | -------------------------------------------------------------------------------- /web/static/templates/editSet.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 6 | 7 | <% } %> 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <% members.forEach(function(member, idx) { %> 21 | 22 | 23 | 24 | 25 | <% }) %> 26 | 27 |
Member
<%= member %>
28 |
29 | 44 | -------------------------------------------------------------------------------- /web/static/templates/editStream.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 6 | 7 | <% } %> 8 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% items.forEach(function(item) { %> 33 | 34 | 35 | 36 | 37 | 38 | 45 | 46 | <% }) %> 47 | 48 |
#TimestampFieldValue
<%= item.number %><%= item.timestamp %><%= item.field %><%= item.value %> 39 | <% if (!redisReadOnly) { %> 40 | 43 | <% } %> 44 |
49 | 64 |
65 | -------------------------------------------------------------------------------- /web/static/templates/editString.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 4 | <% } %> 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 | <% if (!redisReadOnly) { %> 23 | 24 | <% } %> 25 |
26 |
27 | 61 | -------------------------------------------------------------------------------- /web/static/templates/editZSet.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 6 | 7 | <% } %> 8 | 9 | 10 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | <% items.forEach(function(item, idx) { %> 44 | 45 | 46 | 47 | 48 | 49 | 50 | <% }) %> 51 | 52 |
#ScoreValue
<%= item.number %><%= item.score %><%= item.value %>
53 |
54 | 70 | -------------------------------------------------------------------------------- /web/static/templates/serverInfo.ejs: -------------------------------------------------------------------------------- 1 |
2 | <% if (!redisReadOnly) { %> 3 | 4 | <% } %> 5 | 6 | <% if (connectionId.startsWith('C')) { %> 7 | 8 | <% } %> 9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% info.forEach(function(infoItem) { %> 22 | 23 | 24 | 25 | 26 | <% }) %> 27 | 28 |
NameValue
<%= infoItem.key %><%= infoItem.value %>
29 |
30 | -------------------------------------------------------------------------------- /web/views/home/home-sso.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redis Commander: <%= title %> 5 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redis Commander: <%= title %> 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | <%- body %> 37 | 38 | <% if (!redisReadOnly) { %> 39 | <%- include('modals/addListValueModal') %> 40 | <%- include('modals/addSetMemberModal') %> 41 | <%- include('modals/addZSetMemberModal') %> 42 | <%- include('modals/addXSetMemberModal') %> 43 | <%- include('modals/addHashFieldModal') %> 44 | <%- include('modals/editZSetMemberModal') %> 45 | <%- include('modals/editSetMemberModal') %> 46 | <%- include('modals/editListValueModal') %> 47 | <%- include('modals/addKeyModal') %> 48 | <%- include('modals/renameKeyModal') %> 49 | <%- include('modals/editHashFieldModal') %> 50 | <% } %> 51 | <%- include('modals/signinModal') %> 52 | 53 | 54 | -------------------------------------------------------------------------------- /web/views/modals/addHashFieldModal.ejs: -------------------------------------------------------------------------------- 1 | 22 | 27 | -------------------------------------------------------------------------------- /web/views/modals/addKeyModal.ejs: -------------------------------------------------------------------------------- 1 | 60 | 65 | -------------------------------------------------------------------------------- /web/views/modals/addListValueModal.ejs: -------------------------------------------------------------------------------- 1 | 29 | 34 | -------------------------------------------------------------------------------- /web/views/modals/addSetMemberModal.ejs: -------------------------------------------------------------------------------- 1 | 20 | 25 | -------------------------------------------------------------------------------- /web/views/modals/addXSetMemberModal.ejs: -------------------------------------------------------------------------------- 1 | 29 | 35 | -------------------------------------------------------------------------------- /web/views/modals/addZSetMemberModal.ejs: -------------------------------------------------------------------------------- 1 | 22 | 27 | -------------------------------------------------------------------------------- /web/views/modals/editHashFieldModal.ejs: -------------------------------------------------------------------------------- 1 | 24 | 29 | -------------------------------------------------------------------------------- /web/views/modals/editListValueModal.ejs: -------------------------------------------------------------------------------- 1 | 24 | 29 | -------------------------------------------------------------------------------- /web/views/modals/editSetMemberModal.ejs: -------------------------------------------------------------------------------- 1 | 23 | 28 | -------------------------------------------------------------------------------- /web/views/modals/editZSetMemberModal.ejs: -------------------------------------------------------------------------------- 1 | 25 | 30 | -------------------------------------------------------------------------------- /web/views/modals/renameKeyModal.ejs: -------------------------------------------------------------------------------- 1 | 28 | 31 | -------------------------------------------------------------------------------- /web/views/modals/signinModal.ejs: -------------------------------------------------------------------------------- 1 | 21 | 22 | 53 | 54 | -------------------------------------------------------------------------------- /web/views/tools/exportData.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Export

3 |
4 |
5 | 6 |
7 | 15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 30 | 34 | 38 |
39 |
40 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /web/views/tools/importData.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Import

3 |
4 |
5 | 6 |
7 | 15 |
16 |
17 |
18 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | /> 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 | Redis DUMPs can be imported too when the binary data part is base64 encoded. This format can be created by the "Export" 51 | function from the menu (DUMP-BASE64) or with custom export scripts like:

52 | RESTORE <key> <ttl|0> <base64 encoded binary data from DUMP> 53 |
54 |
55 |
56 |
57 | --------------------------------------------------------------------------------