changeset 294:6bacd8ca9f8f

merge
author Matt Johnston <matt@ucc.asn.au>
date Sat, 06 Jul 2019 18:30:25 +0800
parents d15dda1b1f76 (diff) 97d99eb42d27 (current diff)
children 8441916e3095
files py/uploader.py web/templog.py
diffstat 8 files changed, 108 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/py/uploader.py	Wed Jun 26 22:52:03 2019 +0800
+++ b/py/uploader.py	Sat Jul 06 18:30:25 2019 +0800
@@ -64,7 +64,7 @@
     @asyncio.coroutine
     def do(self):
         try:
-            readings = self.server.take_readings()
+        readings = self.server.take_readings()
             tosend = self.get_tosend(readings)
             D("tosend >>>%s<<<" % str(tosend))
             nreadings = len(readings)
--- a/web/config.py	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/config.py	Sat Jul 06 18:30:25 2019 +0800
@@ -9,13 +9,12 @@
 
 # local config items
 HMAC_KEY = 'a hmac key' 
-ALLOWED_USERS = [] # list of sha1 hashes of client ssl keys
-SSH_HOST = 'remotehost'
-SSH_KEYFILE = '/home/matt/.ssh/somekey'
-SSH_PROG = 'ssh'
+ALLOWED_USERS = [] # list of hashes allowed, as provided by the Email link
 
 UPDATE_URL = 'http://evil.ucc.asn.au/~matt/templog/update'
 
+EMAIL = "[email protected]"
+
 GRAPH_WIDTH = 600
 GRAPH_HEIGHT = 700
 ZOOM = 1
@@ -24,15 +23,18 @@
 
 LINE_WIDTH = 2
 
-SENSOR_NAMES = {'sensor_28 CE B2 1A 03 00 00 99': "Old Fridge",
+SENSOR_NAMES = {
+    'sensor_28 CE B2 1A 03 00 00 99': "Old Fridge",
     'sensor_28 CC C1 1A 03 00 00 D4': "Old Ambient",
     'sensor_28 49 BC 1A 03 00 00 54': "Old Wort",
     'sensor_voltage': 'Voltage',
     'sensor_fridge_setpoint': 'Setpoint',
     'sensor_fridge_on': 'Cool',
-    'sensor_28-0000042cf4dd': "Wort",
+    'sensor_28-0000042cf4dd': "New Old Wort",
+    'sensor_28-0000042d36cc': "Wort",
     'sensor_28-0000042cccc4': "OldFridge",
-    'sensor_28-0000042c6dbb': "Fridge",
+    'sensor_28-0000042c6dbb': "New Old Fridge",
+    'sensor_28-0000068922df': "Fridge",
     'sensor_internal': "Processor",
     }
 
--- a/web/log.py	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/log.py	Sat Jul 06 18:30:25 2019 +0800
@@ -51,8 +51,6 @@
                 'DS:temp:GAUGE:600:-100:500',
                 'RRA:AVERAGE:0.5:1:1051200']
 
-    print>>sys.stderr, sensor_rrd_path(sensor_id) 
-
     rrdtool.create(sensor_rrd_path(sensor_id), 
                 '--start', 'now-60d',
                 *args)
@@ -106,7 +104,7 @@
             graph_args.append('DEF:raw%(vname)s=%(rrdfile)s:temp:AVERAGE' % locals())
             # limit max temp to 50
             graph_args.append('CDEF:%(vname)s=raw%(vname)s,38,GT,UNKN,raw%(vname)s,%(volts_mult)f,*,%(volts_shift)f,+,IF' % locals())
-            unit = '<span face="Liberation Serif">º</span>C'
+            unit = '<span face="Liberation Serif">°</span>C'
 
         format_last_value = None
         if unit:
@@ -143,8 +141,7 @@
     sensor_lines.sort(key = lambda (legend, line): "Wort" in legend)
     graph_args += (line for (legend, line) in sensor_lines)
 
-    print>>sys.stderr, '\n'.join(graph_args)
-
+    #print>>sys.stderr, '\n'.join(graph_args)
 
     end = int(start+length)
     start = int(start)
@@ -224,8 +221,9 @@
 
 def record_debug(params):
     f = debug_file('a+')
-    f.write('===== %s =====\n' % time.strftime('%a, %d %b %Y %H:%M:%S'))
+    f.write('===== start %s =====\n' % time.strftime('%a, %d %b %Y %H:%M:%S'))
     json.dump(params, f, sort_keys=True, indent=4)
+    f.write('===== end %s =====\n' % time.strftime('%a, %d %b %Y %H:%M:%S'))
     f.flush()
     return f
 
@@ -289,22 +287,34 @@
     debugf.write("Updated sensors in %.2f secs\n" % timedelta)
     debugf.flush()
 
+# types used here define the type of a field
 _FIELD_DEFAULTS = {
     'fridge_setpoint': 16.0,
     'fridge_difference': 0.2,
     'overshoot_delay': 720, # 12 minutes
-    'overshoot_factor': 1, # ºC
+    'overshoot_factor': 1.0, # °C
     'disabled': False,
     'nowort': True,
     'fridge_range_lower': 3,
     'fridge_range_upper': 3,
     }
 
+def fake_params():
+    """ for quicker testing """
+    r = []
+    r.append({'name': 'going', 'value': 'true', 'kind': 'yesno', 'title': 'going'})
+    r.append({'name': 'temperature', 'value': 12.5, 'kind': 'number', 'title': 'temperature', 'digits': 1, 'amount': 0.1, 'unit': '°'})
+    return r
+
 def get_params():
+    """ Can return None if there aren't any parameters yet,
+    otherwise returns the parameter list """
 
     r = []
 
     vals = read_current_params()
+    if not vals:
+        return None
 
     for k, v in _FIELD_DEFAULTS.iteritems():
         n = {'name': k, 'value': type(v)(vals[k])}
@@ -317,7 +327,7 @@
                 n['amount'] = 60
                 n['digits'] = 0;
             else:
-                n['unit'] = 'º'
+                n['unit'] = '°'
                 n['amount'] = 0.1;
                 n['digits'] = 1;
         n['kind'] = kind
@@ -326,26 +336,6 @@
 
     return json.dumps(r, sort_keys=True, indent=4)
 
-def send_params(params):
-    # 'templog_receive' is ignored due to authorized_keys
-    # restrictions. the rpi has authorized_keys with
-    # command="/home/matt/templog/venv/bin/python /home/matt/templog/py/receive.py",no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding ssh-rsa AAAAB3NzaC....
-    args = [config.SSH_PROG, '-i', config.SSH_KEYFILE,
-        config.SSH_HOST, 'templog_receive']
-    try:
-        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-        (out, err) = p.communicate(json.dumps(params))
-    except OSError, e:
-        print>>sys.stderr, e
-        return "Failed update"
-
-    if 'Good Update' in out:
-        return True
-
-    print>>sys.stderr, "Strange return from update:"
-    print>>sys.stderr, out
-    return "Unexpected update result"
-
 def same_type(a, b):
     ta = type(a)
     tb = type(b)
--- a/web/secure.py	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/secure.py	Sat Jul 06 18:30:25 2019 +0800
@@ -11,47 +11,39 @@
 
 import config
 
-__all__ = ["get_csrf_blob", "check_csrf_blob", "setup_csrf", "get_user_hash",
-"check_user_hash"]
+__all__ = [
+    "get_csrf_blob", 
+    "check_csrf_blob", 
+    "setup_csrf", 
+    "check_cookie",
+    "init_cookie",
+]
+
+AUTH_COOKIE = 'templogauth'
+AUTH_COOKIE_LEN = 16
 
 HASH=hashlib.sha1
 
 CLEAN_RE = re.compile('[^a-z0-9A-Z]')
 
-def clean_hash(h):
-    return CLEAN_RE.sub('', h.lower())
-
-def get_user_hash():
-    """
-    Uses the following apache config. 
-    Needs a separate port or IP to no-certificate SSL, SNI isn't good enough.
-
-    <location /~matt/templog/set>
-    Require all granted
-    SSLVerifyClient optional_no_ca
-    SSLVerifyDepth 1
-    SSLOptions +StdEnvVars +ExportCertData +OptRenegotiate
-    </location>
-    """
+def cookie_hash(c):
+    return hashlib.sha256(c).hexdigest()
 
-    verify = bottle.request.environ.get('SSL_CLIENT_VERIFY', '')
-    if not (verify == 'GENEROUS' or verify == 'SUCCESS'):
-        return 'FAILVERIFY'
-    blob = bottle.request.environ.get('SSL_CLIENT_CERT')
-    if not blob:
-        return 'NOCERT'
+def init_cookie():
+    """ Generates a new httponly auth cookie if required. 
+    Returns the hash of the cookie (new or existing)
+    """
+    c = bottle.request.get_cookie(AUTH_COOKIE)
+    if not c:
+        c = binascii.hexlify(os.urandom(AUTH_COOKIE_LEN))
+        bottle.response.set_cookie(AUTH_COOKIE, c, secure=True, httponly=True)
+    return cookie_hash(c)
 
-    b64 = ''.join(l for l in blob.split('\n')
-        if not l.startswith('-'))
-
-    return HASH(binascii.a2b_base64(b64)).hexdigest()
-
-def check_user_hash(allowed_users):
-    current_hash = clean_hash(get_user_hash())
-    for a in allowed_users:
-        if current_hash == clean_hash(a):
-            return True
-    return False
+def check_cookie(allowed_users):
+    c = bottle.request.get_cookie(AUTH_COOKIE)
+    if not c:
+        return False
+    return cookie_hash(c) in allowed_users
 
 def setup_csrf():
     NONCE_SIZE=16
@@ -72,7 +64,7 @@
 
 def get_csrf_blob():
     expiry = int(config.CSRF_TIMEOUT + time.time())
-    content = '%s-%s' % (get_user_hash(), expiry)
+    content = '%s-%s' % (init_cookie(), expiry)
     mac = hmac.new(_csrf_key, content).hexdigest()
     return "%s-%s" % (content, mac)
 
@@ -83,7 +75,7 @@
         return False
 
     user, expiry, mac = toks
-    if user != get_user_hash():
+    if user != init_cookie():
         print>>sys.stderr, "wrong user"
         return False
 
--- a/web/templog.py	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/templog.py	Sat Jul 06 18:30:25 2019 +0800
@@ -69,7 +69,7 @@
 
 @route('/set/update', method='post')
 def set_update():
-    if not secure.check_user_hash(config.ALLOWED_USERS):
+    if not secure.check_cookie(config.ALLOWED_USERS):
         # the "Save" button should be disabled if the cert wasn't
         # good
         response.status = 403
@@ -92,12 +92,23 @@
 
 @route('/set')
 def set():
-    allowed = ["false", "true"][secure.check_user_hash(config.ALLOWED_USERS)]
+    cookie_hash = secure.init_cookie()
+    allowed = ["false", "true"][secure.check_cookie(config.ALLOWED_USERS)]
     response.set_header('Cache-Control', 'no-cache')
+    if request.query.fake:
+        inline_data = log.fake_params()
+    else:
+        inline_data = log.get_params()
+    if not inline_data:
+        response.status = 503 # Service Unavailable
+        return bottle.template('noparamsyet')
+
     return bottle.template('set', 
-        inline_data = log.get_params(), 
+        inline_data = inline_data,
         csrf_blob = secure.get_csrf_blob(),
-        allowed = allowed)
+        allowed = allowed,
+        cookie_hash = cookie_hash,
+        email = urllib.quote(config.EMAIL))
 
 def get_request_zoom():
     """ returns (length, end) tuple.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/noparamsyet.tpl	Sat Jul 06 18:30:25 2019 +0800
@@ -0,0 +1,21 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+<title>Try again soon</title>
+<meta name="viewport" content="width=device-width">
+<style type="text/css">
+    body {
+        font-family: monospace;
+        text-align: center;
+    }
+</style>
+</head>
+<body>
+<p>
+The templog fridge client hasn't sent its current settings to the server yet. 
+</p>
+<p>
+Try again in a minute or two.
+</p>
+</body>
+</html>
--- a/web/views/set.tpl	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/views/set.tpl	Sat Jul 06 18:30:25 2019 +0800
@@ -8,6 +8,7 @@
 <script>
 %include riot.min.js
 </script>
+<meta name="theme-color" content="#fff">
 
 <style type="text/css">
 span.no_selection {
@@ -20,6 +21,10 @@
     font-family: sans-serif;
 }
 
+a {
+    color: #000;
+}
+
 input {
     border: 2px solid transparent;
     border-radius: 4px;
@@ -28,6 +33,8 @@
     padding: 0;
     font-size: 30pt;
     height: 34pt;
+    vertical-align: middle;
+    line-height: 1em;
 }
 
 input[type="button"] {
@@ -36,7 +43,6 @@
     -webkit-appearance: none;
     -moz-appearance: none;
     background:#fff;
-    vertical-align: center;
 }
 
 input[type="submit"] {
@@ -80,6 +86,10 @@
     //vertical-align: center;
 }
 
+#mailauth {
+    display: none;
+}
+
 </style>
 <title>Set templog</title>
 </head>
@@ -232,6 +242,7 @@
     if (!allowed) {
         $("#savebutton").attr("disabled", true);
         $('#status').text("No cert")
+        $('#mailauth').show();
     }
 
     $("#savebutton").click(function() {
@@ -317,6 +328,8 @@
 <span id="savebox">
 <input type="button" id="savebutton" value="Save"/>
 <span id="status"></span>
+<span id="mailauth"> <a href="mailto:{{email}}?Subject=Allow%20Templog&body=Hash%20is%20{{cookie_hash}}">Email</a>
+</span>
 </span>
 
 
--- a/web/views/top.tpl	Wed Jun 26 22:52:03 2019 +0800
+++ b/web/views/top.tpl	Sat Jul 06 18:30:25 2019 +0800
@@ -3,7 +3,8 @@
 <head>
 <title>Wort Temperature Log</title>
 <meta name="viewport" content="width=device-width">
-<style type="text/css"><!--
+<meta name="theme-color" content="#fff">
+<style type="text/css">
 span.no_selection {
     -webkit-user-select: none; // webkit (safari, chrome) browsers
     -moz-user-select: none; // mozilla browsers
@@ -19,7 +20,6 @@
 	width: 100%;
 	max-width: {{graphwidth}}px;
 }
-//-->
 </style>
 <title></title>
 </head>