cleanup, re-add arg parsing

This commit is contained in:
Nico Schottelius 2019-08-27 23:19:37 +02:00
parent 044801328d
commit 26fa771878
6 changed files with 258 additions and 148 deletions

View file

@ -8,8 +8,8 @@ flask = "*"
flask-jsonpify = "*" flask-jsonpify = "*"
flask-sqlalchemy = "*" flask-sqlalchemy = "*"
flask-restful = "*" flask-restful = "*"
psycopg2 = "*"
python-etcd = "*" python-etcd = "*"
etcd3 = "*"
[dev-packages] [dev-packages]

116
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "3a4b0e16a97795721cc169fa6f26982a8bd080805660c08c26052712cd8814e4" "sha256": "c3b848f915e1b6150fc00881b6eb5a6ea53674b251cb31c9b758fd4077cd5347"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -18,10 +18,10 @@
"default": { "default": {
"aniso8601": { "aniso8601": {
"hashes": [ "hashes": [
"sha256:b8a6a9b24611fc50cf2d9b45d371bfdc4fd0581d1cc52254f5502130a776d4af", "sha256:513d2b6637b7853806ae79ffaca6f3e8754bdd547048f5ccc1420aec4b714f1e",
"sha256:bb167645c79f7a438f9dfab6161af9bed75508c645b1f07d1158240841d22673" "sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b"
], ],
"version": "==6.0.0" "version": "==7.0.0"
}, },
"click": { "click": {
"hashes": [ "hashes": [
@ -37,13 +37,20 @@
], ],
"version": "==1.16.0" "version": "==1.16.0"
}, },
"flask": { "etcd3": {
"hashes": [ "hashes": [
"sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3", "sha256:25a524b9f032c6631ff0097532907dea81243eaa63c3744510fd1598cc4e0e87"
"sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.3" "version": "==0.10.0"
},
"flask": {
"hashes": [
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
],
"index": "pypi",
"version": "==1.1.1"
}, },
"flask-jsonpify": { "flask-jsonpify": {
"hashes": [ "hashes": [
@ -68,6 +75,43 @@
"index": "pypi", "index": "pypi",
"version": "==2.4.0" "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": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
@ -115,22 +159,27 @@
], ],
"version": "==1.1.1" "version": "==1.1.1"
}, },
"psycopg2": { "protobuf": {
"hashes": [ "hashes": [
"sha256:00cfecb3f3db6eb76dcc763e71777da56d12b6d61db6a2c6ccbbb0bff5421f8f", "sha256:00a1b0b352dc7c809749526d1688a64b62ea400c5b05416f93cfb1b11a036295",
"sha256:076501fc24ae13b2609ba2303d88d4db79072562f0b8cc87ec1667dedff99dc1", "sha256:01acbca2d2c8c3f7f235f1842440adbe01bbc379fa1cbdd80753801432b3fae9",
"sha256:4e2b34e4c0ddfeddf770d7df93e269700b080a4d2ec514fec668d71895f56782", "sha256:0a795bca65987b62d6b8a2d934aa317fd1a4d06a6dd4df36312f5b0ade44a8d9",
"sha256:5cacf21b6f813c239f100ef78a4132056f93a5940219ec25d2ef833cbeb05588", "sha256:0ec035114213b6d6e7713987a759d762dd94e9f82284515b3b7331f34bfaec7f",
"sha256:61f58e9ecb9e4dc7e30be56b562f8fc10ae3addcfcef51b588eed10a5a66100d", "sha256:31b18e1434b4907cb0113e7a372cd4d92c047ce7ba0fa7ea66a404d6388ed2c1",
"sha256:8954ff6e47247bdd134db602fcadfc21662835bd92ce0760f3842eacfeb6e0f3", "sha256:32a3abf79b0bef073c70656e86d5bd68a28a1fbb138429912c4fc07b9d426b07",
"sha256:b6e8c854cdc623028e558a409b06ea2f16d13438335941c7765d0a42b5bedd33", "sha256:55f85b7808766e5e3f526818f5e2aeb5ba2edcc45bcccede46a3ccc19b569cb0",
"sha256:baca21c0f7344576346e260454d0007313ccca8c170684707a63946b27a56c8f", "sha256:64ab9bc971989cbdd648c102a96253fdf0202b0c38f15bd34759a8707bdd5f64",
"sha256:bb1735378770fb95dbe392d29e71405d45c8bdcfa064f916504833a92ab03c55", "sha256:64cf847e843a465b6c1ba90fb6c7f7844d54dbe9eb731e86a60981d03f5b2e6e",
"sha256:de3d3c46c1ee18f996db42d1eb44cf1565cc9e38fb1dbd9b773ff6b3fa8035d7", "sha256:917c8662b585470e8fd42f052661fc66d59fccaae450a60044307dcbf82a3335",
"sha256:dee885602bb200bdcb1d30f6da6c7bb207360bc786d0a364fe1540dd14af0bab" "sha256:afed9003d7f2be2c3df20f64220c30faec441073731511728a2cb4cab4cd46a6",
"sha256:b883d7eb129b1b57c5128146bc7c2d1f15de457e96a549827fbee6f26eeedc46",
"sha256:bf8e05d638b585d1752c5a84247134a0350d3a8b73d3632489a014a9f6f1e758",
"sha256:d831b047bd69becaf64019a47179eb22118a50dd008340655266a906c69c6417",
"sha256:de2760583ed28749ff885789c1cbc6c9c06d6de92fc825740ab99deb2f25ea4d",
"sha256:eabc4cf1bc19689af8022ba52fd668564a8d96e0d08f3b4732d26a64255216a4",
"sha256:fcff6086c86fb1628d94ea455c7b9de898afc50378042927a59df8065a79a549"
], ],
"index": "pypi", "version": "==3.9.1"
"version": "==2.8.2"
}, },
"python-etcd": { "python-etcd": {
"hashes": [ "hashes": [
@ -141,10 +190,10 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
"sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
], ],
"version": "==2019.1" "version": "==2019.2"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -155,9 +204,16 @@
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
"sha256:91c54ca8345008fceaec987e10924bf07dcab36c442925357e5a467b36a38319" "sha256:0459bf0ea6478f3e904de074d65769a11d74cdc34438ab3159250c96d089aef0"
], ],
"version": "==1.3.3" "version": "==1.3.7"
},
"tenacity": {
"hashes": [
"sha256:6a7511a59145c2e319b7d04ddd93c12d48cc3d3c8fa42c2846d33a620ee91f57",
"sha256:a4eb168dbf55ed2cae27e7c6b2bd48ab54dabaf294177d998330cf59f294c112"
],
"version": "==5.1.1"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
@ -168,10 +224,10 @@
}, },
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c", "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4",
"sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6" "sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6"
], ],
"version": "==0.15.4" "version": "==0.15.5"
} }
}, },
"develop": {} "develop": {}

View file

@ -1,7 +1,10 @@
from flask import request, Response, abort
from flask_restful import reqparse
def require_args(*args): def require_args(*args):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
for arg in args: for arg in args:
parser.add_argument(arg, required=True) parser.add_argument(arg, required=True, help="{} required".format(arg))
return parser.parse_args() return parser.parse_args()
@ -20,10 +23,13 @@ class Challenge(object):
def game(self): def game(self):
if request.method == 'GET': if request.method == 'GET':
return self.describe() return self.describe()
if request.method == 'HEAD':
return self.describe()
if request.method == 'POST': if request.method == 'POST':
return self.solve() return self.solve()
def describe(self): def describe(self):
""" Describe what to do to solve this challenge"""
return self.description return self.description
def save_points(self, user): def save_points(self, user):
@ -35,3 +41,18 @@ class Challenge(object):
def solve(self): def solve(self):
""" Needs to be implemented per challenge """ """ Needs to be implemented per challenge """
pass 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

83
challenges.py Normal file
View file

@ -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""")

View file

@ -1,7 +1,8 @@
import etcd import etcd
import etcd3
from flask import abort from flask import abort
class etcdWrapper(object): class DB(object):
""" Generalises some etcd actions """ """ Generalises some etcd actions """
def __init__(self, client, base): def __init__(self, client, base):
@ -25,3 +26,7 @@ class etcdWrapper(object):
def set_user_key(self, user, key, value): def set_user_key(self, user, key, value):
path = "{}/user/{}/{}".format(self.base, user, key) path = "{}/user/{}/{}".format(self.base, user, key)
self.client.write(path, value) self.client.write(path, value)
def get_users(self):
pass

175
server.py
View file

@ -1,103 +1,64 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# The server part: briding http to logic
import sys import sys
import etcd import etcd
import json import json
import datetime import datetime
import inspect import inspect
from ungleich_game_db import *
from challenge import Challenge
from flask import Flask, abort, request, Response from flask import Flask, abort, request, Response
from flask_restful import reqparse from flask_restful import reqparse
from db import DB
from challenge import Challenge
import challenges
class RegisterNet(Challenge):
points = 10
provides = [ "network" ]
description = """ INDEX_MESSAGE = """
Register a /64 IPv6 network that you fully control. Welcome to the ungleich game server!
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. 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: POINT_MESSAGE = """
net = ipaddress.IPv6Network(network) Point list (aka high score)
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 CHALLENGE_MESSAGE = """
self.db.set_user_key(user, "network", network) The following challenges are available on this server:
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
{} {}
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): class Game(object):
def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"): def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"):
self.client = etcdclient self.client = etcdclient
self.etcbase = etcbase self.etcbase = etcbase
self.wrapper = etcdWrapper(etcdclient, self.etcbase) self.wrapper = DB(etcdclient, self.etcbase)
self.app = Flask(name) self.app = Flask(name)
@ -110,51 +71,50 @@ class Game(object):
self.userbase = "{}/user".format(self.etcbase) self.userbase = "{}/user".format(self.etcbase)
# Automate this self.__init_challenges()
challenges = [ RegisterNet, IPv6Address ]
challenge_instances = []
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.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.providers = {}
self.list_challenges = []
self.challenge_instances = []
self.challenge_names = [] self.challenge_names = []
for challenge in challenges:
c = challenge(self.wrapper)
challenge_instances.append(c)
name = type(c).__name__ for name, obj in inspect.getmembers(challenges):
self.challenge_names.append(name) if inspect.isclass(obj):
path = "/challenge/{}".format(name) 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: self.challenge_names.append(name)
if not provider in self.providers: path = "/challenge/{}".format(name)
self.providers[provider] = []
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 # Update challenges with provider information
for challenge in challenge_instances: for challenge in self.challenge_instances:
for requirement in challenge.requires: for requirement in challenge.requires:
if not requirement in self.providers: 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] challenge.dependencies_provided_by[requirement] = self.providers[requirement]
def list_of_challenges(self): def list_of_challenges(self):
base = request.base_url base = request.base_url
challenges = [ "{} ({})".format(name, "{}/{}".format(base, name)) for name in self.challenge_names ] c = [ "{} ({})".format(name, "{}/{}".format(base, name)) for name in self.challenge_names ]
return CHALLENGE_MESSAGE.format("\n".join(c))
return """The following challenges are available on this server:
{}
""".format("\n".join(challenges))
def get_points(self): def get_points(self):
@ -182,18 +142,7 @@ class Game(object):
return user_points return user_points
def index(self): def index(self):
points = self.points() return INDEX_MESSAGE.format(hostname=request.headers['Host'])
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, username=None): def points(self, username=None):
point_list = self.get_points() point_list = self.get_points()
@ -212,11 +161,7 @@ https://code.ungleich.ch/nico/ungleich-game
for k, v in point_list.items(): for k, v in point_list.items():
res.append("{} has {} points".format(k, v)) res.append("{} has {} points".format(k, v))
return """ return POINT_MESSAGE.format("\n".join(res))
Point list (aka high score)
---------------------------
{}
""".format("\n".join(res))
def register(self): def register(self):
args = require_args("user") args = require_args("user")
@ -234,5 +179,5 @@ Point list (aka high score)
if __name__ == '__main__': if __name__ == '__main__':
g = Game(__name__, etcd.Client(port=2379)) g = Game(__name__, etcd.Client(port=2379, host='[::1]'))
g.app.run(host="::", port='5002') g.app.run(host="::", port='5002', debug=False)