diff --git a/archive/oldserver.py b/archive/oldserver.py new file mode 100644 index 0000000..38bc4d0 --- /dev/null +++ b/archive/oldserver.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 + +USERLENGTH = 50 + +import ipaddress +import random +import sys +import etcd +import json +import datetime + +from flask import Flask, abort, request, Response +from flask_restful import reqparse + +def get_random_ip(network): + net = ipaddress.IPv6Network(network) + addr_offset = random.randrange(2**64) + addr = net[0] + addr_offset + + return addr + +def require_args(*args): + parser = reqparse.RequestParser() + for arg in args: + parser.add_argument(arg, required=True) + return parser.parse_args() + +class etcdWrapper(object): + """ Generalises some etcd actions """ + + def __init__(self, client, base): + self.client = client + self.base = base + + def read_key_or_none(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 + + def get_user_key_or_none(self, user, key): + path = "{}/user/{}/{}".format(self.base, user, key) + return self.read_key_or_none(path) + + def set_user_key(self, user, key, value): + path = "{}/user/{}/{}".format(self.base, user, key) + self.client.write(path, value) + + +class Challenge(object): + """ A sample challenge -- inherit this and overwrite accordingly """ + + points = 0 + provides = [] + requires = [] + description = None + dependencies_provided_by = {} + + def __init__(self, wrapper): + self.db = wrapper + + def game(self): + if request.method == 'GET': + return self.describe() + if request.method == 'POST': + return self.solve() + + def describe(self): + return self.description + + def save_points(self, user): + """ should be called when the challenge was solved successfully""" + + key = "points/{}".format(user, type(self).__name__) + self.db.set_user_key(user, key, self.points) + + 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. +""" + def solve(self): + args = require_args("user", "network") + network = args['network'] + user = args['user'] + + try: + net = ipaddress.IPv6Network(network) + 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 + self.db.set_user_key(user, "network", network) + self.save_points(user) + + return "Network {} registered, have fun with the next challenge!".format(network) + +class IPv6Address(Challenge): + points = 20 + requires = [ "network" ] + + description = """ +You have setup your network, great! +Now it is time to show that you are really controlling your network! + +Setup the IPv6 address + +{} + +and POST to this address, when it should be reachable by ping6. +""" + + def describe(self): + args = require_args("user") + user = args['user'] + key = "network" + + network = self.db.get_user_key_or_none(user, key) + if not network: + return Response(status=400, response=""" +Register a network before trying to be reachable. Possible challenges that +provide the network are: + +{} +""".format("\n".join(self.dependencies_provided_by['network']))) + + + key = "address" + address = self.db.get_user_key_or_none(user, key) + if not address: + address = get_random_ip(network.value) + self.db.set_user_key(user, key, address) + else: + address = address.value + + return self.description.format(address) + + def solve(self): + args = require_args("user") + user = args['user'] + + return Response(status=400, response=""" +Not yet implemented""") + + +class Game(object): + def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"): + self.client = etcdclient + self.etcbase = etcbase + + self.wrapper = etcdWrapper(etcdclient, self.etcbase) + + self.app = Flask(name) + + self.app.add_url_rule('/', 'index', self.index) + self.app.add_url_rule('/points', 'points', self.points) + self.app.add_url_rule('/register', 'register', self.register, methods=['POST']) + + # etcd paths are below here + + self.userbase = "{}/user".format(self.etcbase) + + # Automate this + challenges = [ RegisterNet, IPv6Address ] + challenge_instances = [] + + 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.providers = {} + self.challenge_names = [] + for challenge in challenges: + c = challenge(self.wrapper) + challenge_instances.append(c) + + name = type(c).__name__ + self.challenge_names.append(name) + path = "/challenge/{}".format(name) + + self.app.add_url_rule(path, name, c.game, methods=['GET', 'POST']) + + for provider in challenge.provides: + if not provider in self.providers: + self.providers[provider] = [] + + self.providers[provider].append(name) + + # Update challenges with provider information + for challenge in challenge_instances: + for requirement in challenge.requires: + if not requirement in self.providers: + raise Exception("Unplayable server/game: {}".format(type(challenge).__name__)) + + challenge.dependencies_provided_by[requirement] = self.providers[requirement] + + + + def list_of_challenges(self): + base = request.base_url + challenges = [ "{} ({})".format(name, "{}/{}".format(base, name)) for name in self.challenge_names ] + + return """The following challenges are available on this server: + +{} + +""".format("\n".join(challenges)) + + + + def get_points(self): + """ Returns a dict['username'] = points """ + + user_points = {} + + path = "{}/".format(self.userbase) + users = self.wrapper.read_key_or_none(path) + + if users: + for user in users.children: + username = user.key.replace(path,"") + user_points[username] = 0 + + point_path = "{}/points".format(user.key) + points = self.wrapper.read_key_or_none(point_path, recursive=True) + + if not points: + continue + + 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: + +{} + +For more information visit + +https://code.ungleich.ch/nico/ungleich-game +""".format(points) + + def points(self): + point_list = self.get_points() + res = [] + if not point_list: + return Response("No winners yet!") + + for k, v in point_list.items(): + res.append("{} has {} points".format(k, v)) + + return """ +Point list (aka high score) +--------------------------- +{} +""".format("\n".join(res)) + + def register(self): + args = require_args("user") + path = "{}/{}/registered_at".format(self.userbase, args['user']) + value = str(datetime.datetime.now()) + + cur = self.read_etcd(path) + + if cur: + value = cur.value + else: + self.client.write(path, value) + + return "Registered at: {}\n".format(value) + +# 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") + + +if __name__ == '__main__': + g = Game(__name__, etcd.Client(port=2379)) + g.app.run(host="::", port=5002)