Skip to content
This repository has been archived by the owner on Aug 22, 2023. It is now read-only.

Added a lot of modernizations #44

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
FROM python:2.7-alpine
FROM python:3.9-alpine

MAINTAINER Marian Steinbach <[email protected]>
MAINTAINER Jonathan Kelley <[email protected]>

ENV DEBIAN_FRONTEND noninteractive

ADD requirements.txt /
RUN pip install -r /requirements.txt
#ADD requirements.txt /
#RUN pip install -r /requirements.txt
ADD . /app/

WORKDIR /app

RUN python3 setup.py install

EXPOSE 5001
ENTRYPOINT ["python", "-u", "/app/runserver.py"]
ENTRYPOINT ["rebrow"]
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
The MIT License (MIT)

Copyright (c) 2014 Marian Steinbach
Copyright (c) 2021 Jonathan Kelley

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include rebrow/templates/*
recursive-include rebrow/static *.css *.map *.svg *.ttf *.eot *.js
54 changes: 43 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
rebrow - Python-Flask-based Browser for Redis Content
rebrow-modernized - Python-Flask-based Browser for Redis Content
=====================================================

Built for the developer who needs to look into a Redis store.
Allows for inspection and deletion of keys and follows PubSub messages. Also displays
## Fork Info

I forked this because the upstream seems abandoned.
I've added some features for my workplace at [LogDNA](https://logdna.com/).

Here's the features added in this fork:

* Upgrade for Python3 (unicode support) ✅
* Move the code into Flask blueprint pattern. ✅
* Add docker-compose file with a Redis instance for testing. ✅
* Bump flask dependency from 1.0 to 1.1.0 ✅
* Add (*optional*) password support for Redis instances (thanks to [kveroneau](https://github.com/kveroneau)) ✅

## Project Information

Built for the Python developer who needs to look into a Redis store. Allows for inspection and deletion of keys and follows PubSub messages. Also displays
some runtime and configuration information.

## Features
## Primary Features

* Web based
* Runs in Python 2.7
* Runs in Python 3.9.1
* Lightweight requirements
* Search for keys using patterns
* Delete single keys
Expand All @@ -19,30 +33,48 @@ some runtime and configuration information.

Execute this:

git clone https://github.com/marians/rebrow.git
git clone https://github.com/jondkelley/rebrow-modernized.git
cd rebrow
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
python runserver.py
python3 setup.py install
rebrow &

Then open [127.0.0.1:5001](http://127.0.0.1:5001).

## Running in docker-compose

If you have docker-compose installed, you can simply run

```
docker-compose build
docker-compose up
```

Then open [127.0.0.1:5001](http://127.0.0.1:5001).

A redis server is started in tandem with hostname `redis` for your convienence.

## Running as Docker container

If you run redis in a Docker container, the recommended way is to run rebrow in it's own Docker container, too.

You can use the ready-made public image [marian/rebrow](https://registry.hub.docker.com/u/marian/rebrow/).
You can use the ready-made public image [jondkelley/rebrow](https://registry.hub.docker.com/r/jondkelley/rebrow).

Alternatively, the provided `Dockerfile` can be used to create the according image. The `Makefile` contains example commands to build the image and run a container from the image.

When running the image, make sure to get your links right. For example, if your redis server is running in a container named `myredis`, start your rebrow container like this:

```
docker run --rm -ti -p 5001:5001 --link myredis:myredis marian/rebrow
docker run --rm -ti -p 5001:5001 --link myredis:myredis jondkelley/rebrow:latest
```

Then access rebrow via `http://<your-docker-ip>:5001/` and set the host name in the login screen to `myredis`.
Then access rebrow via `http://<your-docker-ip>:5001/` and set the host name in the login screen to `redis` or your Redis instance if it's something else..

## Contributers

* 2014 Marian Steinbach
* 2021 Jonathan Kelley

## License

Expand Down
Empty file added data/appendonly.aof
Empty file.
31 changes: 31 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: '2'

services:
redis:
image: redis:alpine
container_name: redis_db
command: redis-server --appendonly yes
ports:
- 6379:6379
volumes:
- ./data:/data
restart: always
networks:
- redis_net

rebrow:
depends_on:
- redis
build: .
container_name: rebrow
ports:
- 5001:5001
links:
- redis:redis
restart: always
networks:
- redis_net

networks:
redis_net:
driver: bridge
Empty file added rebrow/__init__.py
Empty file.
Empty file added rebrow/blueprints/__init__.py
Empty file.
233 changes: 233 additions & 0 deletions rebrow/blueprints/rebrow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
from datetime import datetime, timedelta
from flask import Blueprint, request
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, Markup, Response, json
from flask import current_app as app
from flask import render_template, redirect, url_for
from json import loads as json_loads
from rebrow.sharedlib.metadata import serverinfo_meta
from redis.exceptions import ConnectionError
from redis.sentinel import Sentinel
import base64
import os
import redis
import time

rebrow = Blueprint('rebrow', __name__)

def get_redis(host, port, db, password, sentinel):
"""
get redis instance
"""
if sentinel == 'on':
_sentinel = Sentinel([(host, port)], socket_timeout=0.1)
return sentinel.master_for(
'mymaster', db=db, password=password, socket_timeout=0.1)
else:
if password == "":
return redis.StrictRedis(host=host, port=port, db=db)
else:
return redis.StrictRedis(host=host, port=port, db=db, password=password)

@rebrow.route("/", methods=['GET', 'POST'])
def login():
"""
Start page
"""
if request.method == 'POST':
host = request.form["host"]
port = int(request.form["port"])
port = int(request.form["port"])
db = int(request.form["db"])
db = int(request.form["db"])
url = url_for("rebrow.server_db", host=host, port=port, db=db)
sentinel = request.form.get("sentinel")
if sentinel is None:
sentinel = 'off'
password = request.form["password"]
url = url_for("rebrow.server_db", host=host, port=port, db=db, password=password, sentinel=sentinel)
return redirect(url)
else:
s = time.time()
return render_template('login.html',
duration=time.time()-s)


@rebrow.route("/<host>:<int:port>/<int:db>/")
def server_db(host, port, db):
"""
List all databases and show info on server
"""
s = time.time()
try:
password = request.args.get('password', default = '', type=str)
sentinel = request.args.get('sentinel', default = 'off', type=str)
r = get_redis(host, port, db, password, sentinel)
info = r.info("all")
dbsize = r.dbsize()
return render_template('server.html',
host=host,
port=port,
db=db,
password=password,
info=info,
dbsize=dbsize,
serverinfo_meta=serverinfo_meta,
duration=time.time()-s)
except ConnectionError as e:
flash(f'ConnectionError: {e}', category="error")
return redirect(url_for("rebrow.login"))
except Exception as e:
flash(f'Exception: {e}', category="error")
return redirect(url_for("rebrow.login"))


@rebrow.route("/<host>:<int:port>/<int:db>/keys/", methods=['GET', 'POST'])
def keys(host, port, db):
"""
List keys for one database
"""
s = time.time()
try:
password = request.args.get('password', default = '', type=str)
sentinel = request.args.get('sentinel', default = 'off', type=str)
r = get_redis(host, port, db, password, sentinel)
if request.method == "POST":
action = request.form["action"]
app.logger.debug(action)
if action == "delkey":
if request.form["key"] is not None:
result = r.delete(request.form["key"])
if result == 1:
flash("Key %s has been deleted." %
request.form["key"], category="info")
else:
flash("Key %s could not be deleted." %
request.form["key"], category="error")
return redirect(request.url)
else:
offset = int(request.args.get("offset", "0"))
perpage = int(request.args.get("perpage", "10"))
pattern = request.args.get('pattern', '*')
dbsize = r.dbsize()
keys = sorted(r.keys(pattern))
limited_keys = keys[offset:(perpage+offset)]
types = {}
for key in limited_keys:
types[key] = r.type(key).decode()
return render_template('keys.html',
host=host,
port=port,
db=db,
password=password,
dbsize=dbsize,
keys=[k.decode() for k in limited_keys],
types=[t.decode() for t in types],
offset=offset,
perpage=perpage,
pattern=pattern,
num_keys=len(keys),
duration=time.time()-s)
except ConnectionError as e:
flash(f'ConnectionError: {e}', category="error")
return redirect(url_for("rebrow.login"))
except Exception as e:
flash(f'Exception: {e}', category="error")
return redirect(url_for("rebrow.login"))


@rebrow.route("/<host>:<int:port>/<int:db>/keys/<key>/")
def key(host, port, db, key):
"""
Show a specific key.
key is expected to be URL-safe base64 encoded
"""
key = base64.urlsafe_b64decode(key.encode("utf8"))
s = time.time()
try:
password = request.args.get('password', default = '', type=str)
sentinel = request.args.get('sentinel', default = 'off', type=str)
r = get_redis(host, port, db, password, sentinel)
dump = r.dump(key)
if dump is None:
abort(404)
# if t is None:
# abort(404)
size = len(dump)
del dump
t = r.type(key)
ttl = r.pttl(key)
if t == b"string":
val = r.get(key).decode("utf-8", "replace")
try:
val = json.dumps(json_loads(val), indent=3)
except ValueError:
pass
elif t == b"list":
val = r.lrange(key, 0, -1)
elif t == b"hash":
val = r.hgetall(key)
elif t == b"set":
val = r.smembers(key)
elif t == b"zset":
val = r.zrange(key, 0, -1, withscores=True)
return render_template('key.html',
host=host,
port=port,
db=db,
password=password,
key=key.decode(),
value=val,
type=t.decode(),
size=size,
ttl=ttl / 1000.0,
now=datetime.utcnow(),
expiration=datetime.utcnow() + timedelta(seconds=ttl / 1000.0),
duration=time.time()-s)
except ConnectionError as e:
flash(f'ConnectionError: {e}', category="error")
return redirect(url_for("rebrow.login"))
except Exception as e:
flash(f'Exception: {e}', category="error")
return redirect(url_for("rebrow.login"))


@rebrow.route("/<host>:<int:port>/<int:db>/pubsub/")
def pubsub(host, port, db):
"""
List PubSub channels
"""
s = time.time()
password = request.args.get('password', default = '', type=str)
sentinel = request.args.get('sentinel', default = 'off', type=str)
return render_template('pubsub.html',
host=host,
port=port,
db=db,
password=password,
sentinel=sentinel,
duration=time.time()-s)


def pubsub_event_stream(host, port, db, password, sentinel, pattern):
r = get_redis(host, port, db, password, sentinel)
p = r.pubsub()
p.psubscribe(pattern)
for message in p.listen():
if message["type"] != "psubscribe" and message["data"] != "1":
yield 'data: %s\n\n' % json.dumps(message)


@rebrow.route("/<host>:<int:port>/<int:db>/pubsub/api/")
def pubsub_ajax(host, port, db):
try:
password = request.args.get('password', default = '', type=str)
sentinel = request.args.get('sentinel', default = 'off', type=str)

return Response(pubsub_event_stream(host, port, db, password, sentinel, pattern="*"),
mimetype="text/event-stream")
except ConnectionError as e:
flash(f'ConnectionError: {e}', category="error")
return redirect(url_for("rebrow.login"))
except Exception as e:
flash(f'Exception: {e}')
return redirect(url_for("rebrow.login"))
Loading