view web/templog.py @ 586:87c20b8c5472 default master

port to python3
author Matt Johnston <matt@ucc.asn.au>
date Mon, 09 Sep 2019 22:24:10 +0800
parents 8441916e3095
children
line wrap: on
line source

#!/usr/bin/env python2.7

import binascii
import json
import hmac
import zlib
from datetime import datetime, timedelta
import time
import urllib.request, urllib.parse, urllib.error
import sys
import traceback
import hashlib

import bottle
from bottle import route, request, response

import config
import log
import secure
import atomicfile

DATE_FORMAT = '%Y%m%d-%H.%M'
ZOOM_SCALE = 2.0

class TemplogBottle(bottle.Bottle):
    def run(*args, **argm):
        argm['server'] = 'gevent'
        super(TemplogBottle, self).run(*args, **argm)

bottle.default_app.push(TemplogBottle())

secure.setup_csrf()

@route('/update', method='post')
def update():
    js_enc = request.forms.data.strip().encode()
    mac = request.forms.hmac

    h = hmac.new(config.HMAC_KEY.encode(), js_enc, hashlib.sha256).hexdigest()
    if h != mac:
        raise bottle.HTTPError(code = 403, output = "Bad key")

    js = zlib.decompress(binascii.a2b_base64(js_enc))

    params = json.loads(js.decode())

    log.parse(params)

    return "OK"

def make_graph(length, end):
    length_minutes = int(length)
    end = datetime.strptime(end, DATE_FORMAT)
    start = end - timedelta(minutes=length_minutes)

    start_epoch = time.mktime(start.timetuple())
    return log.graph_png(start_epoch, length_minutes * 60)

def encode_data(data, mimetype):
    return 'data:%s;base64,%s' % (mimetype, binascii.b2a_base64(data).decode().rstrip())

@route('/graph.png')
def graph():
    response.set_header('Content-Type', 'image/png')
    minutes, endstr = get_request_zoom()
    return make_graph(minutes, endstr)

@route('/set/update', method='post')
def set_update():
    if not secure.check_cookie(config.ALLOWED_USERS):
        # the "Save" button should be disabled if the cert wasn't
        # good
        response.status = 403
        return "No cert, dodginess"

    post_json = json.loads(request.forms.data)

    csrf_blob = post_json['csrf_blob']

    if not secure.check_csrf_blob(csrf_blob):
        response.status = 403
        return "Bad csrf"

    ret = log.update_params(post_json['params'])
    if not ret is True:
        response.status = 409 # Conflict
        return ret
        
    return "Good"

@route('/set')
def set():
    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 = inline_data,
        csrf_blob = secure.get_csrf_blob(),
        allowed = allowed,
        cookie_hash = cookie_hash,
        email = urllib.parse.quote(config.EMAIL))

def get_request_zoom():
    """ returns (length, end) tuple.
    length is in minutes, end is a DATE_FORMAT string """
    minutes = int(request.query.get('length', 26*60))

    if 'end' in request.query:
        end = datetime.strptime(request.query.end, DATE_FORMAT)
    else:
        end = datetime.now()

    if 'zoom' in request.query:
        orig_start = end - timedelta(minutes=minutes)
        orig_end = end
        scale = float(request.query.scaledwidth) / config.GRAPH_WIDTH
        xpos = int(request.query.x) / scale
        xpos -= config.GRAPH_LEFT_MARGIN * config.ZOOM

        if xpos >= 0 and xpos < config.GRAPH_WIDTH * config.ZOOM:
            click_time = orig_start \
                + timedelta(minutes=(float(xpos) / (config.GRAPH_WIDTH * config.ZOOM)) * minutes)
            minutes = int(minutes / ZOOM_SCALE)

            end = click_time + timedelta(minutes=minutes//2)
        else:
            # zoom out
            minutes = int(minutes*ZOOM_SCALE)
            end += timedelta(minutes=minutes//2)

    if end > datetime.now():
        end = datetime.now()

    endstr = end.strftime(DATE_FORMAT)
    return (minutes, endstr)

@route('/')
def top():
    minutes, endstr = get_request_zoom()

    request.query.replace('length', minutes)
    request.query.replace('end', endstr)

    urlparams = urllib.parse.urlencode(request.query)
    graphdata = encode_data(make_graph(minutes, endstr), 'image/png')
    return bottle.template('top', urlparams=urlparams,
                    end = endstr,
                    length = minutes,
                    graphwidth = config.GRAPH_WIDTH,
                    graphdata = graphdata)

@route('/debug')
def debuglog():
    response.set_header('Content-Type', 'text/plain')
    return log.tail_debug_log()

@route('/env')
def env():
    response.set_header('Content-Type', 'text/plain')
    #return '\n'.join(traceback.format_stack())
    return '\n'.join(("%s %s" % k) for k in  request.environ.items())
    #return str(request.environ)
    #yield "\n"
    #var_lookup = environ['mod_ssl.var_lookup']
    #return var_lookup("SSL_SERVER_I_DN_O")

@route('/h')
def headers():
    response.set_header('Content-Type', 'text/plain')
    return '\n'.join("%s: %s" % x for x in request.headers.items())

@route('/get_settings')
def get_settings():
    response.set_header('Cache-Control', 'no-cache')
    req_etag = request.headers.get('etag', None)
    if req_etag:
        # wait for it to change
        # XXX this is meant to return True if it has been woken up
        # but it isn't working. Instead compare epochtag below.
        log.fridge_settings.wait(req_etag, timeout=config.LONG_POLL_TIMEOUT)

    contents, epoch_tag = log.fridge_settings.get()
    if epoch_tag == req_etag:
        response.status = 304
        return "Nothing happened"

    response.set_header('Content-Type', 'application/json')
    return json.dumps({'params': contents, 'epoch_tag': epoch_tag})

@bottle.get('/<filename:re:.*\.js>')
def javascripts(filename):
    response.set_header('Cache-Control', "public, max-age=1296000")
    return bottle.static_file(filename, root='static')


def main():
    """ for standalone testing """
    #bottle.debug(True)
    #bottle.run(reloader=True)
    bottle.run(server='cgi', reloader=True)
    #bottle.run(port=9999, reloader=True)

if __name__ == '__main__':
    main()