Scoring infrastructure

This commit is contained in:
Neale Pickett 2009-08-20 12:51:47 -06:00
commit 530a209082
3 changed files with 271 additions and 0 deletions

189
points.py Executable file
View File

@ -0,0 +1,189 @@
#! /usr/bin/env python3
import socket
import hmac
import struct
import io
##
## Authentication
##
key = b'mullosks peck my galloping genitals'
def digest(data):
return hmac.new(key, data).digest()
def sign(data):
return data + digest(data)
def check_sig(data):
base, mac = data[:-16], data[-16:]
if mac == digest(base):
return base
else:
raise ValueError('Invalid message digest')
##
## Marshalling
##
def unpack(fmt, buf):
"""Unpack buf based on fmt, return the rest as a buffer."""
size = struct.calcsize(fmt)
vals = struct.unpack(fmt, buf[:size])
return vals + (buf[size:],)
def packstr(s):
b = bytes(s, 'utf-8')
return struct.pack('!H', len(b)) + b
def unpackstr(b):
l, b = unpack('!H', b)
s, b = b[:l], b[l:]
return str(s, 'utf-8'), b
##
## Request
##
def encode_request(when, cat, team, score):
base = (struct.pack('!I', when) +
packstr(cat) +
packstr(team) +
struct.pack('!i', score))
return sign(base)
def decode_request(b):
base = check_sig(b)
when, base = unpack('!I', base)
cat, base = unpackstr(base)
team, base = unpackstr(base)
score, base = unpack('!i', base)
assert not base
return (when, cat, team, score)
##
## Response
##
def encode_response(when, txt):
base = (struct.pack('!I', when) +
packstr(txt))
return sign(base)
def decode_response(b):
base = check_sig(b)
when, base = unpack('!I', base)
txt, base = unpackstr(base)
assert not base
return (when, txt)
##
## Storage
##
class Storage:
def __init__(self, fn):
self.points_by_team = {}
self.points_by_cat = {}
self.log = []
self.events = set()
self.f = io.BytesIO()
# Read stored scores
try:
f = open(fn, 'rb')
while True:
l = f.read(4)
if not l:
return
(l,) = struct.unpack('!I', l)
b = f.read(l)
req = decode_request(b)
self.add(req)
f.close()
except IOError:
pass
self.f = open(fn, 'ab')
def __contains__(self, req):
return req in self.events
def add(self, req):
if req in self.events:
return
when, cat, team, score = req
if team not in self.points_by_team:
self.points_by_team[team] = 0
self.points_by_team[team] += score
if cat not in self.points_by_cat:
self.points_by_cat[cat] = 0
self.points_by_cat[cat] += score
self.log.append(req)
self.events.add(req)
b = encode_request(*req)
l = struct.pack('!I', len(b))
self.f.write(l)
self.f.write(b)
def categories(self):
return sorted(self.points_by_cat)
def teams(self):
return sorted(self.points_by_team)
def cat_points(self, cat):
return self.points_by_cat[cat]
def team_points(self, team):
return self.points_by_team[team]
##
## Testing
##
def test():
import time
import os
now = int(time.time())
req = (now, 'category 5', 'foobers in heat', 43)
assert decode_request(encode_request(*req)) == req
rsp = (now, 'hello world')
assert decode_response(encode_response(*rsp)) == rsp
try:
os.unlink('test.dat')
except OSError:
pass
s = Storage('test.dat')
s.add((now, 'cat1', 'zebras', 20))
s.add((now, 'cat1', 'aardvarks', 10))
s.add((now, 'merf', 'aardvarks', 50))
assert s.teams() == ['aardvarks', 'zebras']
assert s.categories() == ['cat1', 'merf']
assert s.team_points('aardvarks') == 60
assert s.cat_points('cat1') == 30
del s
s = Storage('test.dat')
assert s.teams() == ['aardvarks', 'zebras']
print('all tests pass; output file is test.dat')
if __name__ == '__main__':
test()

38
pointscli.py Normal file
View File

@ -0,0 +1,38 @@
import optparse
import select
import points
import socket
import time
def main():
p = optparse.OptionParser(usage='%prog CATEGORY TEAM SCORE')
p.add_option('-s', '--host', dest='host', default='localhost',
help='Host to connect to')
opts, args = p.parse_args()
try:
cat, team, score = args
score = int(score)
except ValueError:
return p.print_usage()
now = int(time.time())
req = points.encode_request(now, cat, team, score)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
s.sendto(req, (opts.host, 9999))
r, w, x = select.select([s], [], [], 0.2)
if r:
b = s.recv(500)
when, txt = points.decode_response(b)
assert when == now
if txt == 'OK':
return
print(txt)
raise ValueError(txt)
if __name__ == '__main__':
main()

44
pointsd.py Executable file
View File

@ -0,0 +1,44 @@
#! /usr/bin/env python3
import socketserver
import struct
import points
import time
acked = points.Storage('scores.dat')
class MyHandler(socketserver.BaseRequestHandler):
def respond(self, when, txt):
peer = self.request[1]
resp = points.encode_response(when, txt)
peer.sendto(resp, self.client_address)
def handle(self):
global acked
now = int(time.time())
data = self.request[0]
peer = self.request[1]
try:
req = points.decode_request(data)
except ValueError as e:
return self.respond(now, str(e))
when, cat, team, score = req
# Replays can happen legitimately.
if not req in acked:
if not (now - 2 < when < now):
resp = points.encode_response(when, 'Your clock is off')
peer.sendto(resp, self.client_address)
return
acked.add(req)
resp = points.encode_response(when, 'OK')
peer.sendto(resp, self.client_address)
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
server = socketserver.UDPServer((HOST, PORT), MyHandler)
server.serve_forever()