From cdf6a17055899f8f2e0f85a987eb9d11a5c99c34 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 11 Mar 2020 09:13:50 -0700 Subject: [PATCH] update readme, format code --- Pipfile | 2 - README.md | 21 +++++++++- setup.py | 15 +++---- tvid/__init__.py | 1 - tvid/db.py | 7 ++-- tvid/server.py | 104 +++++++++++++++++++++++------------------------ 6 files changed, 79 insertions(+), 71 deletions(-) diff --git a/Pipfile b/Pipfile index e482e03..b236af4 100644 --- a/Pipfile +++ b/Pipfile @@ -3,8 +3,6 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true -[dev-packages] - [packages] bottle = "*" pprint = "*" diff --git a/README.md b/README.md index 363506a..3d2deaf 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,16 @@ will get redirected to the new target. # todo -* fix CSRF +* fix CSRF bug * fix FIXMEs -* fix logging output +* fix logging output (that is, make it log anything) * put git short id into version string * make sure cookie expiration is correct * sessions maybe * configuration in db to support password changes +* perhaps load the target in a fullsize iframe so that the target can be + changed by the js on server update instead of having to reboot the display + box # screenshots @@ -63,6 +66,20 @@ will get redirected to the new target. WTFPL +# status + +This is not enterprise-production-grade software yet. + +It has a CSRF bug (low severity) and it stores your unhashed auth password +in a cookie. I didn't even check how long the per-device cookie expirations +are, so it might have a big painful bug there too. I made it to scratch an +itch and I am running it in production; I may improve it further but it +works for my purposes (being able to flash a half-dozen display driver +raspberry pis with the same image) as-is. + +Patches and improvements and bug reports are welcome. Send me an email at +the address below. + # author sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)> diff --git a/setup.py b/setup.py index b6f0be1..4a1173c 100644 --- a/setup.py +++ b/setup.py @@ -4,15 +4,10 @@ from setuptools import setup, find_packages # and i have no idea why so there is a bin in bin/ setup( - name='tvid', - version='1.0.0', + name="tvid", + version="1.0.0", packages=find_packages(), - license='WTFPL', - long_description=open('README.md').read(), - entry_points = { - 'console_scripts': [ - 'tvidd = tvid.tvid:serve' - ], - }, + license="WTFPL", + long_description=open("README.md").read(), + entry_points={"console_scripts": ["tvidd = tvid.tvid:serve"],}, ) - diff --git a/tvid/__init__.py b/tvid/__init__.py index 6640c20..af97ca1 100644 --- a/tvid/__init__.py +++ b/tvid/__init__.py @@ -1,2 +1 @@ from .server import serve - diff --git a/tvid/db.py b/tvid/db.py index a71a7aa..27ffe48 100644 --- a/tvid/db.py +++ b/tvid/db.py @@ -6,11 +6,12 @@ from .server import SQLBASE Base = SQLBASE # YAGNI just set admin pw in an env var for now and dont support changing it # yet -#class Settings(Base): +# class Settings(Base): + class TV(Base): - __tablename__ = 'tvs' - id = Column(Integer, Sequence('id_seq'), primary_key=True) + __tablename__ = "tvs" + id = Column(Integer, Sequence("id_seq"), primary_key=True) displayid = Column(String(20)) lastSeen = Column(DateTime) target = Column(String(255)) diff --git a/tvid/server.py b/tvid/server.py index cf41ef8..06d6da7 100644 --- a/tvid/server.py +++ b/tvid/server.py @@ -21,25 +21,27 @@ import random import string from datetime import datetime, timedelta -VERSION = '1.0.0' -PORT = os.environ.get('PORT', 8080) -DEBUG = os.environ.get('DEBUG', False) -SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite') -DATABASE_URL = os.environ.get('DATABASE_URL','sqlite:///' + SQLITE_FILENAME) -ADMIN_PSK = os.environ.get('ADMIN_PSK','hunter2') +VERSION = "1.0.0" +PORT = os.environ.get("PORT", 8080) +DEBUG = os.environ.get("DEBUG", False) +SQLITE_FILENAME = os.environ.get("SQLITE_FILENAME", "/data/db.sqlite") +DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///" + SQLITE_FILENAME) +ADMIN_PSK = os.environ.get("ADMIN_PSK", "hunter2") # sorry for global SQLBASE = declarative_base() # FIXME make this skip letters not in base58 def randomUpper(count): - return ''.join(random.choice(string.ascii_uppercase) for _ in range(count)) + return "".join(random.choice(string.ascii_uppercase) for _ in range(count)) + # FIXME maybe make the IDs longer def genRandomTVID(): tvid = str(randomUpper(3) + "-" + randomUpper(3)) return tvid + def serve(): app = bottle.Bottle() @@ -51,40 +53,40 @@ def serve(): plugin = sqlalchemy.Plugin( engine, SQLBASE.metadata, - keyword='db', + keyword="db", create=True, commit=True, - use_kwargs=False + use_kwargs=False, ) app.install(plugin) # here we cookie them if they have no cookie, then redirect them to the # cookie'd value (whether preexisting or new). - @app.get('/') + @app.get("/") def indexpage(): c = request.get_cookie("displayid") if c: # redirect - return redirect('/tv/' + c) + return redirect("/tv/" + c) else: newid = genRandomTVID() response.set_cookie("displayid", newid) - return redirect('/tv/' + newid) + return redirect("/tv/" + newid) - @app.get('/style.css') + @app.get("/style.css") def stylesheet(): - response.content_type = 'text/css' - return template('style') + response.content_type = "text/css" + return template("style") # here we check to see if they have a redirect URL in the db. if they do # we send them there. if they don't, we display their ID really big, # reloading the page once per hour. - @app.get('/tv/') + @app.get("/tv/") def tvpage(db, displayid=None): # FIXME check for cookie, this is broken if displayid is None: - return template('nocookie') + return template("nocookie") # check db for tv id tv = db.query(TV).filter_by(displayid=displayid).first() @@ -94,61 +96,60 @@ def serve(): if tv.target: return redirect(tv.target) else: - return template('displayid', id=displayid, version=VERSION) + return template("displayid", id=displayid, version=VERSION) else: # otherwise, just show their display ID bigly and keep them # bouncing periodically until some admin configures them # a target: # update lastseen here: - newtv = TV(displayid=displayid,lastSeen=datetime.now()) + newtv = TV(displayid=displayid, lastSeen=datetime.now()) db.add(newtv) - return template('displayid', id=displayid, version=VERSION) + return template("displayid", id=displayid, version=VERSION) - - @app.get('/admin/edit/') + @app.get("/admin/edit/") def displayeditform(db, displayid=None): c = request.get_cookie("psk") if not c: - return redirect('/login') + return redirect("/login") if c != ADMIN_PSK: - return redirect('/logout') + return redirect("/logout") if not displayid: - return redirect('/admin') + return redirect("/admin") tv = db.query(TV).filter_by(displayid=displayid).first() if tv is None: - return redirect('/admin') - return template('displayeditform', tv=tv, version=VERSION) + return redirect("/admin") + return template("displayeditform", tv=tv, version=VERSION) - @app.post('/admin/edit') + @app.post("/admin/edit") def displayedithandler(db): # FIXME SECURITY csrf issue c = request.get_cookie("psk") if not c: - return redirect('/login') + return redirect("/login") if c != ADMIN_PSK: - return redirect('/logout') - displayid = request.forms.get('displayid') + return redirect("/logout") + displayid = request.forms.get("displayid") tv = db.query(TV).filter_by(displayid=displayid).first() if tv is None: - return redirect('/admin') + return redirect("/admin") # FIXME make sure this is a valid URL - tv.target = request.forms.get('target') - tv.memo = request.forms.get('formmemo') + tv.target = request.forms.get("target") + tv.memo = request.forms.get("formmemo") db.add(tv) db.commit() - return redirect('/admin') + return redirect("/admin") # here we display the administration list of TVs if logged in # if logged out then redirect to /login # FIXME make this use sessions instead of just storing PSK in a cookie # https://bottlepy.org/docs/dev/recipes.html - @app.get('/admin') + @app.get("/admin") def adminpage(db): c = request.get_cookie("psk") if not c: - return redirect('/login') + return redirect("/login") if c != ADMIN_PSK: - return redirect('/logout') + return redirect("/logout") # first, cleanup db of old entries: week_ago = datetime.now() - timedelta(days=7) @@ -156,36 +157,33 @@ def serve(): db.commit() tvs = db.query(TV).order_by(TV.lastSeen.desc()) - response.headers['Cache-Control'] = 'no-cache' - return template('adminpanel', tvs=tvs, version=VERSION) - + response.headers["Cache-Control"] = "no-cache" + return template("adminpanel", tvs=tvs, version=VERSION) # here we ask for a password: - @app.get('/login') + @app.get("/login") def loginform(): msg = request.GET.msg - return template('loginform', version=VERSION, msg=msg) + return template("loginform", version=VERSION, msg=msg) - @app.post('/checklogin') + @app.post("/checklogin") def checklogin(): - attemptedPass = request.forms.get('password') + attemptedPass = request.forms.get("password") if not attemptedPass: return redirect( - '/login?msg=' + - urllib.parse.quote_plus(u"Incorrect password.") + "/login?msg=" + urllib.parse.quote_plus(u"Incorrect password.") ) if attemptedPass != ADMIN_PSK: return redirect( - '/login?msg=' + - urllib.parse.quote_plus(u"Incorrect password.") + "/login?msg=" + urllib.parse.quote_plus(u"Incorrect password.") ) # password is right, cookie them: response.set_cookie("psk", attemptedPass) - return redirect('/admin') + return redirect("/admin") - @app.get('/logout') + @app.get("/logout") def logout(): response.set_cookie("psk", "") - return redirect('/login') + return redirect("/login") - app.run(host='0.0.0.0', port=PORT, debug=DEBUG) + app.run(host="0.0.0.0", port=PORT, debug=DEBUG)