ungleich-game/server.py

256 lines
7.3 KiB
Python
Raw Normal View History

2019-05-26 19:19:58 +00:00
#!/usr/bin/env python3
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
USERLENGTH = 50
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
import ipaddress
import random
import sys
import etcd
import json
import datetime
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
from flask import Flask, abort, request, Response
from flask_restful import reqparse
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
# app = Flask(__name__)
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
def get_random_ip(network):
net = ipaddress.IPv6Network(network)
addr_offset = random.randrange(2**64)
addr = net[0] + addr_offset
2019-04-14 16:57:39 +00:00
2019-05-26 19:19:58 +00:00
return addr
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
class Challenge(object):
""" A sample challenge -- inherit this and overwrite accordingly """
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
points = 0
provides = []
requires = []
2019-05-26 20:04:59 +00:00
description = None
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
def __init__(self, etcdclient):
self.client = etcdclient
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
def require_args(self, *args):
parser = reqparse.RequestParser()
for arg in args:
parser.add_argument(arg, required=True)
return parser.parse_args()
2019-05-11 22:18:03 +00:00
2019-05-26 20:04:59 +00:00
def game(self):
if request.method == 'GET':
return self.describe()
if request.method == 'POST':
return self.solve()
2019-05-26 19:19:58 +00:00
def describe(self):
return self.description
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
def save_points(self, user):
""" should be called when the challenge was solved successfully"""
2019-05-11 22:18:03 +00:00
2019-05-26 20:04:59 +00:00
key = "points/{}".format(user, type(self).__name__)
self.set_user_key(user, key, self.points)
def set_user_key(self, user, key, value):
path = "/ungleichgame/v1/user/{}/{}".format(user, key)
self.client.write(path, value)
2019-05-26 19:19:58 +00:00
def solve(self):
""" Needs to be implemented per challenge """
pass
class RegisterNet(Challenge):
points = 10
provides = [ "network" ]
description = """
Register a /64 IPv6 network that you fully control.
Many other challenges depend on this. You will need to
be able to configure IPv6 addresses in this networks
and to setup services listening on these IPv6 addresses.
Submit your network with the "network" parameter.
2019-05-11 22:18:03 +00:00
"""
2019-05-26 19:19:58 +00:00
def solve(self):
2019-05-26 20:04:59 +00:00
args = self.require_args("user", "network")
network = args['network']
user = args['user']
2019-05-26 19:19:58 +00:00
try:
2019-05-26 20:04:59 +00:00
net = ipaddress.IPv6Network(network)
2019-05-26 19:19:58 +00:00
except Exception as e:
return Response(status=400, response="Cannot register network {}: {}".format(network, e))
if not net.prefixlen == 64:
return Response(status=400, response="{} mask is not /64 - please use a /64 network".format(net))
# Save network
2019-05-26 20:04:59 +00:00
self.set_user_key(user, "network", network)
self.save_points(user)
2019-05-26 19:19:58 +00:00
2019-05-26 20:04:59 +00:00
return "Network {} registered, have fun with the next challenge!".format(network)
2019-05-26 19:19:58 +00:00
class Game(object):
def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"):
self.client = etcdclient
self.app = Flask(name)
2019-05-26 20:04:59 +00:00
self.app.add_url_rule('/', 'index', self.index)
self.app.add_url_rule('/points', 'points', self.points)
2019-05-26 19:19:58 +00:00
# etcd paths are below here
self.etcbase = etcbase
self.userbase = "{}/user".format(self.etcbase)
# Automate this
challenges = [ RegisterNet ]
2019-05-26 20:04:59 +00:00
self.app.add_url_rule('/challenge', 'list_of_challenges', self.list_of_challenges)
self.app.add_url_rule('/challenge/', 'list_of_challenges', self.list_of_challenges)
self.challenge_names = []
2019-05-26 19:19:58 +00:00
for challenge in challenges:
c = challenge(self.client)
2019-05-26 20:04:59 +00:00
name = type(c).__name__
self.challenge_names.append(name)
2019-05-26 19:19:58 +00:00
path = "/challenge/{}".format(name)
2019-05-26 20:04:59 +00:00
self.app.add_url_rule(path, name, c.game, methods=['GET', 'POST'])
def list_of_challenges(self):
return """The following challenges are available on this server:
2019-05-26 20:15:13 +00:00
2019-05-26 20:04:59 +00:00
{}
2019-05-26 20:15:13 +00:00
2019-05-26 20:04:59 +00:00
""".format("\n".join(self.challenge_names))
2019-05-11 22:18:03 +00:00
2019-05-26 19:19:58 +00:00
def read_etcd(self, path, recursive=False):
try:
data = self.client.read(path, recursive=recursive)
except etcd.EtcdKeyNotFound:
return None
except Exception:
abort(Response(status=400, response="Error connecting to etcd"))
return data
2019-05-11 22:18:03 +00:00
2019-05-26 20:04:59 +00:00
def get_points(self):
2019-05-26 19:19:58 +00:00
""" Returns a dict['username'] = points """
2019-05-26 20:04:59 +00:00
user_points = {}
path = "{}/".format(self.userbase)
users = self.client.get(path)
if users:
print(users)
for user in users.children:
username= user.key # needs to be FIXED
user_points[username] = 0
point_path = "{}/points".format(user.key)
points = self.read_etcd(point_path, recursive=True)
for challenge in points.children:
user_points[username] += int(challenge.value)
return user_points
def index(self):
points = self.points()
return """Welcome to the game server!
Current point list is:
2019-05-26 20:05:47 +00:00
2019-05-26 20:04:59 +00:00
{}
For more information visit
https://code.ungleich.ch/nico/ungleich-game
""".format(points)
def points(self):
point_list = self.get_points()
2019-05-26 19:19:58 +00:00
res = []
if not point_list:
return Response("No winners yet!")
for k, v in point_list.items():
2019-05-26 20:04:59 +00:00
res.append("{} has {} points".format(k, v))
2019-05-26 19:19:58 +00:00
2019-05-26 20:04:59 +00:00
return "\n".join(res)
2019-05-26 19:19:58 +00:00
# def get_ip_address():
# args = self.require_args("network", "user")
# # Needs to be fixed with ungleich-otp
# username=args['user']
# if request.method == 'GET':
# return Response("""
# This is an easy level - just register any /64 network
# that you fully control. After submission the game server will generate
# a random IPv6 address in this network.
# """)
# client = etcd.Client(port=2379)
# try:
# data = client.read("/ungleichgame/v1/{}/network".format(username))
# # FIXME: differentiate keynotfound and other errors
# except Exception as e:
# return Response(status=400, response="Cannot read your network, try registering first (error: {})".format(e))
# return Response("data={}".format(data.value))
# address = get_random_ip(data.value)
# # FIXME: catch errors
# client.write("/ungleichgame/v1/{}/address".format(username), address)
# return Response("Your IPv6 address for this game is {}. Make it pingable and post to /level/1/result".format(address))
# @app.route("/level/2", methods=['GET', 'POST']) # post for username
# def pingme():
# parser = reqparse.RequestParser()
# parser.add_argument('user', required=True)
# args = parser.parse_args()
# # Needs to be fixed with ungleich-otp
# username=args['user']
# if request.method == 'GET':
# return Response("""
# Proof that you can really control the network that you submitted:
# - Setup the IPv6 address to be ping6 able globally
# - POST to this address when it is configured
# """)
# if request.method == 'POST':
# try:
# data = client.read("/ungleichgame/v1/{}/address".format(username), address)
# except Exception as e:
# return Response(status=400,
# response="""
# You need to register a network before trying to be reachable.
# Please go back to Level 1 for registering your network.
# """)
# return Response("something good")
2019-04-14 16:57:39 +00:00
if __name__ == '__main__':
2019-05-26 19:19:58 +00:00
# net_base = "2a0a:e5c1:{:x}::/64"
# net_offset = random.randrange(0xffff)
# net = ipaddress.IPv6Network(net_base.format(net_offset))
# username = 'nico{}'.format(net_offset)
# print("{} has {}".format(username, net))
g = Game(__name__, etcd.Client(port=2379))
g.app.run(host="::", port='5002')