From 26fa7718787b85c21d556ab65df24514d9b4f5f2 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 27 Aug 2019 23:19:37 +0200 Subject: [PATCH] cleanup, re-add arg parsing --- Pipfile | 2 +- Pipfile.lock | 116 +++++++++++++++++------ challenge.py | 23 ++++- challenges.py | 83 +++++++++++++++++ ungleich_game_db.py => db.py | 7 +- server.py | 175 ++++++++++++----------------------- 6 files changed, 258 insertions(+), 148 deletions(-) create mode 100644 challenges.py rename ungleich_game_db.py => db.py (91%) diff --git a/Pipfile b/Pipfile index 0326fdf..e6694e2 100644 --- a/Pipfile +++ b/Pipfile @@ -8,8 +8,8 @@ flask = "*" flask-jsonpify = "*" flask-sqlalchemy = "*" flask-restful = "*" -psycopg2 = "*" python-etcd = "*" +etcd3 = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 496aad8..ac47687 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3a4b0e16a97795721cc169fa6f26982a8bd080805660c08c26052712cd8814e4" + "sha256": "c3b848f915e1b6150fc00881b6eb5a6ea53674b251cb31c9b758fd4077cd5347" }, "pipfile-spec": 6, "requires": { @@ -18,10 +18,10 @@ "default": { "aniso8601": { "hashes": [ - "sha256:b8a6a9b24611fc50cf2d9b45d371bfdc4fd0581d1cc52254f5502130a776d4af", - "sha256:bb167645c79f7a438f9dfab6161af9bed75508c645b1f07d1158240841d22673" + "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e", + "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b" ], - "version": "==6.0.0" + "version": "==7.0.0" }, "click": { "hashes": [ @@ -37,13 +37,20 @@ ], "version": "==1.16.0" }, - "flask": { + "etcd3": { "hashes": [ - "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3", - "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61" + "sha256:25a524b9f032c6631ff0097532907dea81243eaa63c3744510fd1598cc4e0e87" ], "index": "pypi", - "version": "==1.0.3" + "version": "==0.10.0" + }, + "flask": { + "hashes": [ + "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", + "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + ], + "index": "pypi", + "version": "==1.1.1" }, "flask-jsonpify": { "hashes": [ @@ -68,6 +75,43 @@ "index": "pypi", "version": "==2.4.0" }, + "grpcio": { + "hashes": [ + "sha256:1303578092f1f6e4bfbc354c04ac422856c393723d3ffa032fff0f7cb5cfd693", + "sha256:229c6b313cd82bec8f979b059d87f03cc1a48939b543fe170b5a9c5cf6a6bc69", + "sha256:3cd3d99a8b5568d0d186f9520c16121a0f2a4bcad8e2b9884b76fb88a85a7774", + "sha256:41cfb222db358227521f9638a6fbc397f310042a4db5539a19dea01547c621cd", + "sha256:43330501660f636fd6547d1e196e395cd1e2c2ae57d62219d6184a668ffebda0", + "sha256:45d7a2bd8b4f25a013296683f4140d636cdbb507d94a382ea5029a21e76b1648", + "sha256:47dc935658a13b25108823dabd010194ddea9610357c5c1ef1ad7b3f5157ebee", + "sha256:480aa7e2b56238badce0b9413a96d5b4c90c3bfbd79eba5a0501e92328d9669e", + "sha256:4a0934c8b0f97e1d8c18e76c45afc0d02d33ab03125258179f2ac6c7a13f3626", + "sha256:5624dab19e950f99e560400c59d87b685809e4cfcb2c724103f1ab14c06071f7", + "sha256:60515b1405bb3dadc55e6ca99429072dad3e736afcf5048db5452df5572231ff", + "sha256:610f97ebae742a57d336a69b09a9c7d7de1f62aa54aaa8adc635b38f55ba4382", + "sha256:64ea189b2b0859d1f7b411a09185028744d494ef09029630200cc892e366f169", + "sha256:686090c6c1e09e4f49585b8508d0a31d58bc3895e4049ea55b197d1381e9f70f", + "sha256:7745c365195bb0605e3d47b480a2a4d1baa8a41a5fd0a20de5fa48900e2c886a", + "sha256:79491e0d2b77a1c438116bf9e5f9e2e04e78b78524615e2ce453eff62db59a09", + "sha256:825177dd4c601c487836b7d6b4ba268db59787157911c623ba59a7c03c8d3adc", + "sha256:8a060e1f72fb94eee8a035ed29f1201ce903ad14cbe27bda56b4a22a8abda045", + "sha256:90168cc6353e2766e47b650c963f21cfff294654b10b3a14c67e26a4e3683634", + "sha256:94b7742734bceeff6d8db5edb31ac844cb68fc7f13617eca859ff1b78bb20ba1", + "sha256:962aebf2dd01bbb2cdb64580e61760f1afc470781f9ecd5fe8f3d8dcd8cf4556", + "sha256:9c8d9eacdce840b72eee7924c752c31b675f8aec74790e08cff184a4ea8aa9c1", + "sha256:af5b929debc336f6bab9b0da6915f9ee5e41444012aed6a79a3c7e80d7662fdf", + "sha256:b9cdb87fc77e9a3eabdc42a512368538d648fa0760ad30cf97788076985c790a", + "sha256:c5e6380b90b389454669dc67d0a39fb4dc166416e01308fcddd694236b8329ef", + "sha256:d60c90fe2bfbee735397bf75a2f2c4e70c5deab51cd40c6e4fa98fae018c8db6", + "sha256:d8582c8b1b1063249da1588854251d8a91df1e210a328aeb0ece39da2b2b763b", + "sha256:ddbf86ba3aa0ad8fed2867910d2913ee237d55920b55f1d619049b3399f04efc", + "sha256:e46bc0664c5c8a0545857aa7a096289f8db148e7f9cca2d0b760113e8994bddc", + "sha256:f6437f70ec7fed0ca3a0eef1146591bb754b418bb6c6b21db74f0333d624e135", + "sha256:f71693c3396530c6b00773b029ea85e59272557e9bd6077195a6593e4229892a", + "sha256:f79f7455f8fbd43e8e9d61914ecf7f48ba1c8e271801996fef8d6a8f3cc9f39f" + ], + "version": "==1.23.0" + }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", @@ -115,22 +159,27 @@ ], "version": "==1.1.1" }, - "psycopg2": { + "protobuf": { "hashes": [ - "sha256:00cfecb3f3db6eb76dcc763e71777da56d12b6d61db6a2c6ccbbb0bff5421f8f", - "sha256:076501fc24ae13b2609ba2303d88d4db79072562f0b8cc87ec1667dedff99dc1", - "sha256:4e2b34e4c0ddfeddf770d7df93e269700b080a4d2ec514fec668d71895f56782", - "sha256:5cacf21b6f813c239f100ef78a4132056f93a5940219ec25d2ef833cbeb05588", - "sha256:61f58e9ecb9e4dc7e30be56b562f8fc10ae3addcfcef51b588eed10a5a66100d", - "sha256:8954ff6e47247bdd134db602fcadfc21662835bd92ce0760f3842eacfeb6e0f3", - "sha256:b6e8c854cdc623028e558a409b06ea2f16d13438335941c7765d0a42b5bedd33", - "sha256:baca21c0f7344576346e260454d0007313ccca8c170684707a63946b27a56c8f", - "sha256:bb1735378770fb95dbe392d29e71405d45c8bdcfa064f916504833a92ab03c55", - "sha256:de3d3c46c1ee18f996db42d1eb44cf1565cc9e38fb1dbd9b773ff6b3fa8035d7", - "sha256:dee885602bb200bdcb1d30f6da6c7bb207360bc786d0a364fe1540dd14af0bab" + "sha256:00a1b0b352dc7c809749526d1688a64b62ea400c5b05416f93cfb1b11a036295", + "sha256:01acbca2d2c8c3f7f235f1842440adbe01bbc379fa1cbdd80753801432b3fae9", + "sha256:0a795bca65987b62d6b8a2d934aa317fd1a4d06a6dd4df36312f5b0ade44a8d9", + "sha256:0ec035114213b6d6e7713987a759d762dd94e9f82284515b3b7331f34bfaec7f", + "sha256:31b18e1434b4907cb0113e7a372cd4d92c047ce7ba0fa7ea66a404d6388ed2c1", + "sha256:32a3abf79b0bef073c70656e86d5bd68a28a1fbb138429912c4fc07b9d426b07", + "sha256:55f85b7808766e5e3f526818f5e2aeb5ba2edcc45bcccede46a3ccc19b569cb0", + "sha256:64ab9bc971989cbdd648c102a96253fdf0202b0c38f15bd34759a8707bdd5f64", + "sha256:64cf847e843a465b6c1ba90fb6c7f7844d54dbe9eb731e86a60981d03f5b2e6e", + "sha256:917c8662b585470e8fd42f052661fc66d59fccaae450a60044307dcbf82a3335", + "sha256:afed9003d7f2be2c3df20f64220c30faec441073731511728a2cb4cab4cd46a6", + "sha256:b883d7eb129b1b57c5128146bc7c2d1f15de457e96a549827fbee6f26eeedc46", + "sha256:bf8e05d638b585d1752c5a84247134a0350d3a8b73d3632489a014a9f6f1e758", + "sha256:d831b047bd69becaf64019a47179eb22118a50dd008340655266a906c69c6417", + "sha256:de2760583ed28749ff885789c1cbc6c9c06d6de92fc825740ab99deb2f25ea4d", + "sha256:eabc4cf1bc19689af8022ba52fd668564a8d96e0d08f3b4732d26a64255216a4", + "sha256:fcff6086c86fb1628d94ea455c7b9de898afc50378042927a59df8065a79a549" ], - "index": "pypi", - "version": "==2.8.2" + "version": "==3.9.1" }, "python-etcd": { "hashes": [ @@ -141,10 +190,10 @@ }, "pytz": { "hashes": [ - "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", - "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", + "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" ], - "version": "==2019.1" + "version": "==2019.2" }, "six": { "hashes": [ @@ -155,9 +204,16 @@ }, "sqlalchemy": { "hashes": [ - "sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319" + "sha256:0459bf0ea6478f3e904de074d65769a11d74cdc34438ab3159250c96d089aef0" ], - "version": "==1.3.3" + "version": "==1.3.7" + }, + "tenacity": { + "hashes": [ + "sha256:6a7511a59145c2e319b7d04ddd93c12d48cc3d3c8fa42c2846d33a620ee91f57", + "sha256:a4eb168dbf55ed2cae27e7c6b2bd48ab54dabaf294177d998330cf59f294c112" + ], + "version": "==5.1.1" }, "urllib3": { "hashes": [ @@ -168,10 +224,10 @@ }, "werkzeug": { "hashes": [ - "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", - "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6" + "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4", + "sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6" ], - "version": "==0.15.4" + "version": "==0.15.5" } }, "develop": {} diff --git a/challenge.py b/challenge.py index 72ec29b..2c8400c 100644 --- a/challenge.py +++ b/challenge.py @@ -1,7 +1,10 @@ +from flask import request, Response, abort +from flask_restful import reqparse + def require_args(*args): parser = reqparse.RequestParser() for arg in args: - parser.add_argument(arg, required=True) + parser.add_argument(arg, required=True, help="{} required".format(arg)) return parser.parse_args() @@ -20,10 +23,13 @@ class Challenge(object): def game(self): if request.method == 'GET': return self.describe() + if request.method == 'HEAD': + return self.describe() if request.method == 'POST': return self.solve() def describe(self): + """ Describe what to do to solve this challenge""" return self.description def save_points(self, user): @@ -35,3 +41,18 @@ class Challenge(object): def solve(self): """ Needs to be implemented per challenge """ pass + + + def error(self, msg=""): + """ Abort with an error """ + return Response(status=400, response=msg) + + def get_args(self, *args): + res = {} + f = request.form + for arg in args: + if arg not in f: + abort(Response("Missing argument: {}\n".format(arg))) + + res[arg] = f[arg] + return res diff --git a/challenges.py b/challenges.py new file mode 100644 index 0000000..9e2753c --- /dev/null +++ b/challenges.py @@ -0,0 +1,83 @@ +import challenge +import ipaddress + +from flask import request + +class RegisterNet(challenge.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 = self.get_args("user", "network") + + network = args['network'] + user = args['user'] + + try: + net = ipaddress.IPv6Network(network) + except Exception as e: + return self.error("Cannot register network {}: {}".format(network, e)) + + if not net.prefixlen == 64: + return self.error("{} 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, that's a good start!\n".format(network) + +class IPv6Address(challenge.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 = self.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""") diff --git a/ungleich_game_db.py b/db.py similarity index 91% rename from ungleich_game_db.py rename to db.py index 01e77ae..147444e 100644 --- a/ungleich_game_db.py +++ b/db.py @@ -1,7 +1,8 @@ import etcd +import etcd3 from flask import abort -class etcdWrapper(object): +class DB(object): """ Generalises some etcd actions """ def __init__(self, client, base): @@ -25,3 +26,7 @@ class etcdWrapper(object): def set_user_key(self, user, key, value): path = "{}/user/{}/{}".format(self.base, user, key) self.client.write(path, value) + + + def get_users(self): + pass diff --git a/server.py b/server.py index 648e267..8779ed8 100644 --- a/server.py +++ b/server.py @@ -1,103 +1,64 @@ #!/usr/bin/env python3 +# The server part: briding http to logic + import sys import etcd import json import datetime import inspect -from ungleich_game_db import * -from challenge import Challenge - from flask import Flask, abort, request, Response from flask_restful import reqparse +from db import DB +from challenge import Challenge +import challenges -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. +INDEX_MESSAGE = """ +Welcome to the ungleich game server! -Submit your network with the "network" parameter. +This server is still in development and is running on Nico's +notebook. In case it is off-line, ping @nico on +https://chat.ungleich.ch. + +To play: + +curl http://{hostname}/challenge + +To see the high score: + +curl http://{hostname}/points + +The code for this game can be found on https://code.ungleich.ch/nico/ungleich-game """ - 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)) +POINT_MESSAGE = """ +Point list (aka high score) +--------------------------- +{} - 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!\n".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 +CHALLENGE_MESSAGE = """ +The following challenges are available on this server: {} -and POST to this address, when it should be reachable by ping6. +To play, first just curl the URL and if you want to submit solutions, +post them like this: + +curl -d user=nico -d network=2a0a:e5c1:101::/64 http://.../challenge/... """ - 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.wrapper = DB(etcdclient, self.etcbase) self.app = Flask(name) @@ -110,51 +71,50 @@ class Game(object): self.userbase = "{}/user".format(self.etcbase) - # Automate this - challenges = [ RegisterNet, IPv6Address ] - challenge_instances = [] + self.__init_challenges() + def __init_challenges(self): + # Create list of challenges 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.list_challenges = [] + self.challenge_instances = [] 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) + for name, obj in inspect.getmembers(challenges): + if inspect.isclass(obj): + c = obj(self.wrapper) - self.app.add_url_rule(path, name, c.game, methods=['GET', 'POST']) + self.challenge_instances.append(c) + self.list_challenges.append(obj) - for provider in challenge.provides: - if not provider in self.providers: - self.providers[provider] = [] + self.challenge_names.append(name) + path = "/challenge/{}".format(name) - self.providers[provider].append(name) + self.app.add_url_rule(path, name, c.game, methods=['GET', 'POST']) + + for provider in c.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 challenge in self.challenge_instances: for requirement in challenge.requires: if not requirement in self.providers: - raise Exception("Unplayable server/game: {}".format(type(challenge).__name__)) + raise Exception("Unplayable challenge: {}".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)) - + c = [ "{} ({})".format(name, "{}/{}".format(base, name)) for name in self.challenge_names ] + return CHALLENGE_MESSAGE.format("\n".join(c)) def get_points(self): @@ -182,18 +142,7 @@ class Game(object): 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) + return INDEX_MESSAGE.format(hostname=request.headers['Host']) def points(self, username=None): point_list = self.get_points() @@ -212,11 +161,7 @@ https://code.ungleich.ch/nico/ungleich-game for k, v in point_list.items(): res.append("{} has {} points".format(k, v)) - return """ -Point list (aka high score) ---------------------------- -{} -""".format("\n".join(res)) + return POINT_MESSAGE.format("\n".join(res)) def register(self): args = require_args("user") @@ -234,5 +179,5 @@ Point list (aka high score) if __name__ == '__main__': - g = Game(__name__, etcd.Client(port=2379)) - g.app.run(host="::", port='5002') + g = Game(__name__, etcd.Client(port=2379, host='[::1]')) + g.app.run(host="::", port='5002', debug=False)