update readme, format code

This commit is contained in:
Jeffrey Paul 2020-03-11 09:13:50 -07:00
parent 8c23c702d7
commit cdf6a17055
6 changed files with 79 additions and 71 deletions

View File

@ -3,8 +3,6 @@ name = "pypi"
url = "https://pypi.org/simple" url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages]
[packages] [packages]
bottle = "*" bottle = "*"
pprint = "*" pprint = "*"

View File

@ -35,13 +35,16 @@ will get redirected to the new target.
# todo # todo
* fix CSRF * fix CSRF bug
* fix FIXMEs * fix FIXMEs
* fix logging output * fix logging output (that is, make it log anything)
* put git short id into version string * put git short id into version string
* make sure cookie expiration is correct * make sure cookie expiration is correct
* sessions maybe * sessions maybe
* configuration in db to support password changes * 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 # screenshots
@ -63,6 +66,20 @@ will get redirected to the new target.
WTFPL 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 # author
sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)> sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>

View File

@ -4,15 +4,10 @@ from setuptools import setup, find_packages
# and i have no idea why so there is a bin in bin/ # and i have no idea why so there is a bin in bin/
setup( setup(
name='tvid', name="tvid",
version='1.0.0', version="1.0.0",
packages=find_packages(), packages=find_packages(),
license='WTFPL', license="WTFPL",
long_description=open('README.md').read(), long_description=open("README.md").read(),
entry_points = { entry_points={"console_scripts": ["tvidd = tvid.tvid:serve"],},
'console_scripts': [
'tvidd = tvid.tvid:serve'
],
},
) )

View File

@ -1,2 +1 @@
from .server import serve from .server import serve

View File

@ -8,9 +8,10 @@ Base = SQLBASE
# yet # yet
# class Settings(Base): # class Settings(Base):
class TV(Base): class TV(Base):
__tablename__ = 'tvs' __tablename__ = "tvs"
id = Column(Integer, Sequence('id_seq'), primary_key=True) id = Column(Integer, Sequence("id_seq"), primary_key=True)
displayid = Column(String(20)) displayid = Column(String(20))
lastSeen = Column(DateTime) lastSeen = Column(DateTime)
target = Column(String(255)) target = Column(String(255))

View File

@ -21,25 +21,27 @@ import random
import string import string
from datetime import datetime, timedelta from datetime import datetime, timedelta
VERSION = '1.0.0' VERSION = "1.0.0"
PORT = os.environ.get('PORT', 8080) PORT = os.environ.get("PORT", 8080)
DEBUG = os.environ.get('DEBUG', False) DEBUG = os.environ.get("DEBUG", False)
SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite') SQLITE_FILENAME = os.environ.get("SQLITE_FILENAME", "/data/db.sqlite")
DATABASE_URL = os.environ.get('DATABASE_URL','sqlite:///' + SQLITE_FILENAME) DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///" + SQLITE_FILENAME)
ADMIN_PSK = os.environ.get('ADMIN_PSK','hunter2') ADMIN_PSK = os.environ.get("ADMIN_PSK", "hunter2")
# sorry for global # sorry for global
SQLBASE = declarative_base() SQLBASE = declarative_base()
# FIXME make this skip letters not in base58 # FIXME make this skip letters not in base58
def randomUpper(count): 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 # FIXME maybe make the IDs longer
def genRandomTVID(): def genRandomTVID():
tvid = str(randomUpper(3) + "-" + randomUpper(3)) tvid = str(randomUpper(3) + "-" + randomUpper(3))
return tvid return tvid
def serve(): def serve():
app = bottle.Bottle() app = bottle.Bottle()
@ -51,40 +53,40 @@ def serve():
plugin = sqlalchemy.Plugin( plugin = sqlalchemy.Plugin(
engine, engine,
SQLBASE.metadata, SQLBASE.metadata,
keyword='db', keyword="db",
create=True, create=True,
commit=True, commit=True,
use_kwargs=False use_kwargs=False,
) )
app.install(plugin) app.install(plugin)
# here we cookie them if they have no cookie, then redirect them to the # here we cookie them if they have no cookie, then redirect them to the
# cookie'd value (whether preexisting or new). # cookie'd value (whether preexisting or new).
@app.get('/') @app.get("/")
def indexpage(): def indexpage():
c = request.get_cookie("displayid") c = request.get_cookie("displayid")
if c: if c:
# redirect # redirect
return redirect('/tv/' + c) return redirect("/tv/" + c)
else: else:
newid = genRandomTVID() newid = genRandomTVID()
response.set_cookie("displayid", newid) response.set_cookie("displayid", newid)
return redirect('/tv/' + newid) return redirect("/tv/" + newid)
@app.get('/style.css') @app.get("/style.css")
def stylesheet(): def stylesheet():
response.content_type = 'text/css' response.content_type = "text/css"
return template('style') return template("style")
# here we check to see if they have a redirect URL in the db. if they do # 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, # we send them there. if they don't, we display their ID really big,
# reloading the page once per hour. # reloading the page once per hour.
@app.get('/tv/<displayid>') @app.get("/tv/<displayid>")
def tvpage(db, displayid=None): def tvpage(db, displayid=None):
# FIXME check for cookie, this is broken # FIXME check for cookie, this is broken
if displayid is None: if displayid is None:
return template('nocookie') return template("nocookie")
# check db for tv id # check db for tv id
tv = db.query(TV).filter_by(displayid=displayid).first() tv = db.query(TV).filter_by(displayid=displayid).first()
@ -94,7 +96,7 @@ def serve():
if tv.target: if tv.target:
return redirect(tv.target) return redirect(tv.target)
else: else:
return template('displayid', id=displayid, version=VERSION) return template("displayid", id=displayid, version=VERSION)
else: else:
# otherwise, just show their display ID bigly and keep them # otherwise, just show their display ID bigly and keep them
# bouncing periodically until some admin configures them # bouncing periodically until some admin configures them
@ -102,53 +104,52 @@ def serve():
# update lastseen here: # update lastseen here:
newtv = TV(displayid=displayid, lastSeen=datetime.now()) newtv = TV(displayid=displayid, lastSeen=datetime.now())
db.add(newtv) db.add(newtv)
return template('displayid', id=displayid, version=VERSION) return template("displayid", id=displayid, version=VERSION)
@app.get("/admin/edit/<displayid>")
@app.get('/admin/edit/<displayid>')
def displayeditform(db, displayid=None): def displayeditform(db, displayid=None):
c = request.get_cookie("psk") c = request.get_cookie("psk")
if not c: if not c:
return redirect('/login') return redirect("/login")
if c != ADMIN_PSK: if c != ADMIN_PSK:
return redirect('/logout') return redirect("/logout")
if not displayid: if not displayid:
return redirect('/admin') return redirect("/admin")
tv = db.query(TV).filter_by(displayid=displayid).first() tv = db.query(TV).filter_by(displayid=displayid).first()
if tv is None: if tv is None:
return redirect('/admin') return redirect("/admin")
return template('displayeditform', tv=tv, version=VERSION) return template("displayeditform", tv=tv, version=VERSION)
@app.post('/admin/edit') @app.post("/admin/edit")
def displayedithandler(db): def displayedithandler(db):
# FIXME SECURITY csrf issue # FIXME SECURITY csrf issue
c = request.get_cookie("psk") c = request.get_cookie("psk")
if not c: if not c:
return redirect('/login') return redirect("/login")
if c != ADMIN_PSK: if c != ADMIN_PSK:
return redirect('/logout') return redirect("/logout")
displayid = request.forms.get('displayid') displayid = request.forms.get("displayid")
tv = db.query(TV).filter_by(displayid=displayid).first() tv = db.query(TV).filter_by(displayid=displayid).first()
if tv is None: if tv is None:
return redirect('/admin') return redirect("/admin")
# FIXME make sure this is a valid URL # FIXME make sure this is a valid URL
tv.target = request.forms.get('target') tv.target = request.forms.get("target")
tv.memo = request.forms.get('formmemo') tv.memo = request.forms.get("formmemo")
db.add(tv) db.add(tv)
db.commit() db.commit()
return redirect('/admin') return redirect("/admin")
# here we display the administration list of TVs if logged in # here we display the administration list of TVs if logged in
# if logged out then redirect to /login # if logged out then redirect to /login
# FIXME make this use sessions instead of just storing PSK in a cookie # FIXME make this use sessions instead of just storing PSK in a cookie
# https://bottlepy.org/docs/dev/recipes.html # https://bottlepy.org/docs/dev/recipes.html
@app.get('/admin') @app.get("/admin")
def adminpage(db): def adminpage(db):
c = request.get_cookie("psk") c = request.get_cookie("psk")
if not c: if not c:
return redirect('/login') return redirect("/login")
if c != ADMIN_PSK: if c != ADMIN_PSK:
return redirect('/logout') return redirect("/logout")
# first, cleanup db of old entries: # first, cleanup db of old entries:
week_ago = datetime.now() - timedelta(days=7) week_ago = datetime.now() - timedelta(days=7)
@ -156,36 +157,33 @@ def serve():
db.commit() db.commit()
tvs = db.query(TV).order_by(TV.lastSeen.desc()) tvs = db.query(TV).order_by(TV.lastSeen.desc())
response.headers['Cache-Control'] = 'no-cache' response.headers["Cache-Control"] = "no-cache"
return template('adminpanel', tvs=tvs, version=VERSION) return template("adminpanel", tvs=tvs, version=VERSION)
# here we ask for a password: # here we ask for a password:
@app.get('/login') @app.get("/login")
def loginform(): def loginform():
msg = request.GET.msg 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(): def checklogin():
attemptedPass = request.forms.get('password') attemptedPass = request.forms.get("password")
if not attemptedPass: if not attemptedPass:
return redirect( return redirect(
'/login?msg=' + "/login?msg=" + urllib.parse.quote_plus(u"Incorrect password.")
urllib.parse.quote_plus(u"Incorrect password.")
) )
if attemptedPass != ADMIN_PSK: if attemptedPass != ADMIN_PSK:
return redirect( return redirect(
'/login?msg=' + "/login?msg=" + urllib.parse.quote_plus(u"Incorrect password.")
urllib.parse.quote_plus(u"Incorrect password.")
) )
# password is right, cookie them: # password is right, cookie them:
response.set_cookie("psk", attemptedPass) response.set_cookie("psk", attemptedPass)
return redirect('/admin') return redirect("/admin")
@app.get('/logout') @app.get("/logout")
def logout(): def logout():
response.set_cookie("psk", "") 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)