changeset 291:f7261dd970da

- replace ssl client certs with cookies - remove unused ssh code - add /set?fake=1 test mode
author Matt Johnston <matt@ucc.asn.au>
date Sat, 06 Jul 2019 16:32:16 +0800
parents 35ae717d48f0
children d15dda1b1f76
files web/config.py web/log.py web/secure.py web/templog.py web/views/set.tpl
diffstat 5 files changed, 59 insertions(+), 64 deletions(-) [+]
line wrap: on
line diff
--- a/web/config.py	Sat Jul 06 15:02:47 2019 +0800
+++ b/web/config.py	Sat Jul 06 16:32:16 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
--- a/web/log.py	Sat Jul 06 15:02:47 2019 +0800
+++ b/web/log.py	Sat Jul 06 16:32:16 2019 +0800
@@ -299,6 +299,13 @@
     '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 """
@@ -329,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	Sat Jul 06 15:02:47 2019 +0800
+++ b/web/secure.py	Sat Jul 06 16:32:16 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	Sat Jul 06 15:02:47 2019 +0800
+++ b/web/templog.py	Sat Jul 06 16:32:16 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,9 +92,13 @@
 
 @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')
-    inline_data = log.get_params()
+    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')
@@ -102,7 +106,9 @@
     return bottle.template('set', 
         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.
--- a/web/views/set.tpl	Sat Jul 06 15:02:47 2019 +0800
+++ b/web/views/set.tpl	Sat Jul 06 16:32:16 2019 +0800
@@ -21,6 +21,10 @@
     font-family: sans-serif;
 }
 
+a {
+    color: #000;
+}
+
 input {
     border: 2px solid transparent;
     border-radius: 4px;
@@ -82,6 +86,10 @@
     //vertical-align: center;
 }
 
+#mailauth {
+    display: none;
+}
+
 </style>
 <title>Set templog</title>
 </head>
@@ -234,6 +242,7 @@
     if (!allowed) {
         $("#savebutton").attr("disabled", true);
         $('#status').text("No cert")
+        $('#mailauth').show();
     }
 
     $("#savebutton").click(function() {
@@ -319,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>