update readme, format code
This commit is contained in:
parent
8c23c702d7
commit
cdf6a17055
2
Pipfile
2
Pipfile
@ -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 = "*"
|
||||||
|
21
README.md
21
README.md
@ -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)>
|
||||||
|
15
setup.py
15
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/
|
# 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'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,2 +1 @@
|
|||||||
from .server import serve
|
from .server import serve
|
||||||
|
|
||||||
|
@ -6,11 +6,12 @@ from .server import SQLBASE
|
|||||||
Base = SQLBASE
|
Base = SQLBASE
|
||||||
# YAGNI just set admin pw in an env var for now and dont support changing it
|
# YAGNI just set admin pw in an env var for now and dont support changing it
|
||||||
# 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))
|
||||||
|
104
tvid/server.py
104
tvid/server.py
@ -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,61 +96,60 @@ 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
|
||||||
# a target:
|
# a target:
|
||||||
# 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user