From f51b5f4c73f5276b148751019e14433599064a45 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 26 May 2019 21:19:58 +0200 Subject: [PATCH] Cleanup, begin challenge class --- README.md | 112 ++++++---- check-cli.py => archive/check-cli.py | 0 check.py => archive/check.py | 0 game-sql.py => archive/game-sql.py | 0 game.org => archive/game.org | 0 archive/server.py | 62 ++++++ test-etcd.py => archive/test-etcd.py | 0 ungleich.py => archive/ungleich.py | 0 ungleichapi.py => archive/ungleichapi.py | 0 game-etcd.py | 207 ------------------- server.py | 251 +++++++++++++++++++---- 11 files changed, 340 insertions(+), 292 deletions(-) rename check-cli.py => archive/check-cli.py (100%) rename check.py => archive/check.py (100%) rename game-sql.py => archive/game-sql.py (100%) rename game.org => archive/game.org (100%) create mode 100644 archive/server.py rename test-etcd.py => archive/test-etcd.py (100%) rename ungleich.py => archive/ungleich.py (100%) rename ungleichapi.py => archive/ungleichapi.py (100%) delete mode 100644 game-etcd.py diff --git a/README.md b/README.md index b89865e..734affc 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,86 @@ -## Notes to myself - -* Notes in ungleich-marketing.org / Quiz - -## Objective - -This codebase is for preparing the ungleich game, which heavily relies -on checking other people's VMs. - -The (not so) hidden objective is to create the base for "cmon", -our new monitoring system that does not suck. - -### Planned features (monitoring) - -- full parallel execution -- history support -> possible grafana interface -- easy to create and extend checks -- Requirements: python3 + binaries for certain checks -- Minimal core logic - checks can check "anything" - -### Planned features (game) - -- Allow registration -- Allow deep (i.e. functionality based) checks of services -- Define points (or monitoring severity) - - +## Welcome to the ungleich-game, a geek game engine! +ungleich-game is supposed to be an easy-to-use, easy-to-play and +easy-to-extend game framework for geeks. The project name is +ungleich-game, as it has its roots at ungleich - the project name +might change later. ## How to play -### Monitoring +* Select a game server +* Register +* List challenges - have fun! -Test base: +### How to play on Nico's notebook + +1. Register ``` -python check-cli.py +curl -d user=nico http://nico.ungleich.cloud:5002/register ``` -### Game (not implemented) +2. Get challenges -ungleich register --name your-user-name --email your@email ---first-name Yourfirstname --last-name YourLastName +``` +curl http://nico.ungleich.cloud:5002/challenge +``` -ungleich play-game --game register --ip -ungleich play-game --game dns-forward --ip +3. Get a challenge description -## Documentation +``` +curl http://nico.ungleich.cloud:5002/challenge/registernet +``` -- Raise CheckException on parameter wrong +4. Solve a challenge -## How to write a check +``` +curl -d user=nico -d 2a0a:e5c0:101::/64 http://nico.ungleich.cloud:5002/challenge/registernet +``` -- Your command must return with exit code = 0 -- Output will be saved by cmon, but not interpreted +5. Get high score -## TODOs +``` +curl http://nico.ungleich.cloud:5002/highscore +``` -- last result: select checkname where result = true -- last success: select checkname where result = true + +## Overview - Game flow + +* Users register at a game server +* Users play by getting challenges from the game server +* Users can see their or all high scores on the main page + +## Overview - Development Flow + +[not yet fully implemented] + +The idea is that there are challenges and each challenge offers: + +* A description +* Some dependencies (on something another challenge can provide) +* A score ("how difficult it is") + +### How to add challenges + +* Create challenges-.py and add challenges in there +* Do some magic so all challenges are imported by server + +## Overview - Security + +None at the moment. + + +## Tech stack + +The base for building games is: + +* Python3 - The programmming language +* Flask - web frontend +* etcd - storing data, games, etcd. + +## Things to solve + +* Enhance the Challenge class - maybe make it easier for challenges to abort +* Enhance the Challenge class - abstract away writing information? +* Implement dependencies / providers for challenges +* Add an easy to use CLI (Steven might like click) diff --git a/check-cli.py b/archive/check-cli.py similarity index 100% rename from check-cli.py rename to archive/check-cli.py diff --git a/check.py b/archive/check.py similarity index 100% rename from check.py rename to archive/check.py diff --git a/game-sql.py b/archive/game-sql.py similarity index 100% rename from game-sql.py rename to archive/game-sql.py diff --git a/game.org b/archive/game.org similarity index 100% rename from game.org rename to archive/game.org diff --git a/archive/server.py b/archive/server.py new file mode 100644 index 0000000..7fcfaff --- /dev/null +++ b/archive/server.py @@ -0,0 +1,62 @@ +from flask import Flask, request +from flask_restful import Resource, Api +from sqlalchemy import create_engine +from json import dumps +from flask.json import jsonify + +db_connect = create_engine('sqlite:///chinook.db') +app = Flask(__name__) +api = Api(app) + +class Employees(Resource): + def get(self): + conn = db_connect.connect() # connect to database + query = conn.execute("select * from employees") # This line performs query and returns json result + return {'employees': [i[0] for i in query.cursor.fetchall()]} # Fetches first column that is Employee ID + +class Tracks(Resource): + def get(self): + conn = db_connect.connect() + query = conn.execute("select trackid, name, composer, unitprice from tracks;") + result = {'data': [dict(zip(tuple (query.keys()) ,i)) for i in query.cursor]} + return jsonify(result) + +class Employees_Name(Resource): + def get(self, employee_id): + conn = db_connect.connect() + query = conn.execute("select * from employees where EmployeeId =%d " %int(employee_id)) + result = {'data': [dict(zip(tuple (query.keys()) ,i)) for i in query.cursor]} + return jsonify(result) + + +@app.route("/") +def hello(): + return """ +
+
+
+Join the game
+
+- Create an on account.ungleich.ch
+ -> creates ldap account
+
+Creating the game:
+
+- Create IPv6 only VM + http proxy
+- Intro on main page
+- Score list below
+- Users can be clicked
+
+
+ +""" + + +api.add_resource(Employees, '/employees') # Route_1 +api.add_resource(Employees_Name, '/employees/') # Route_3 +#api.add_resource(Tracks, '/tracks') # Route_2 + + + +if __name__ == '__main__': + app.run(port='5002') diff --git a/test-etcd.py b/archive/test-etcd.py similarity index 100% rename from test-etcd.py rename to archive/test-etcd.py diff --git a/ungleich.py b/archive/ungleich.py similarity index 100% rename from ungleich.py rename to archive/ungleich.py diff --git a/ungleichapi.py b/archive/ungleichapi.py similarity index 100% rename from ungleichapi.py rename to archive/ungleichapi.py diff --git a/game-etcd.py b/game-etcd.py deleted file mode 100644 index e9ac214..0000000 --- a/game-etcd.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 - -USERLENGTH = 50 - -import ipaddress -import random -import sys -import etcd -import ungleichapi -import json -import datetime - -from flask import Flask, abort, request, Response -from flask_restful import Resource, Api -from flask_restful import reqparse - -app = Flask(__name__) -api = Api(app) - - -def get_random_ip(network): - net = ipaddress.IPv6Network(network) - addr_offset = random.randrange(2**64) - addr = net[0] + addr_offset - - return addr - -class Level(Resource): - points = 0 - - def test(self): - pass - -class Ping6(Level): - points = 10 - - def test(self): - - """ - ping6 -c3 - """ - -class Game(object): - def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"): - self.client = etcdclient - self.app = Flask(name) - self.app.add_url_rule('/', 'highscore', self.highscore) - self.app.add_url_rule('/highscore', 'highscore', self.highscore) - self.app.add_url_rule("/register", 'register', self.register, methods=['POST']) - - # etcd paths are below here - self.etcbase = etcbase - self.userbase = "{}/user".format(self.etcbase) - - - 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 - - def get_highscore(self, username=None): - """ Returns a dict['username'] = points """ - - all_users = {} - highscore = {} - - print("getting high") - - if username: - path = "{}/{}".format(self.userbase, username) - user = self.read_etcd(path) - if user: - all_users[username] = user - else: - path = "{}/".format(self.userbase) - users = self.read_etcd(path, recursive=True) - print("reading from {}".format(path)) - if users: - for child in users.children: - print("adding user {} {} = {}".format(child, child.key, child.value)) - all_users[child.key] = child.value - - for k, v in all_users.items(): - # Ignore all kind of errors - just add the ones that work - try: - highscore[k] = json.loads(v)['points'] - print("f?") - except Exception as e: - print(e) - - return highscore - - def highscore(self): - point_list = self.get_highscore() - 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 Response("\n".join(res)) - - def require_args(self, *args): - parser = reqparse.RequestParser() - for arg in args: - parser.add_argument(arg, required=True) - return parser.parse_args() - - def register(self): - args = self.require_args("network", "user") - - # Needs to be fixed with ungleich-otp - username=args['user'] - - try: - net = ipaddress.IPv6Network(args['network']) - network = args['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)) - - self.client.write("/ungleichgame/v1/{}/network".format(username), network) - data = self.client.read("/ungleichgame/v1/{}/network".format(username)) - - return json.dumps("All good, go to /level/1 to start with level 1! - {}".format(data.value)) - - - -@app.route("/level/1", methods=['GET', 'POST']) # post for username -def get_ip_address(): - 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(""" -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__': - 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(port='5002') - - # app.run(port='5002') diff --git a/server.py b/server.py index 7fcfaff..e34bf66 100644 --- a/server.py +++ b/server.py @@ -1,62 +1,229 @@ -from flask import Flask, request -from flask_restful import Resource, Api -from sqlalchemy import create_engine -from json import dumps -from flask.json import jsonify +#!/usr/bin/env python3 -db_connect = create_engine('sqlite:///chinook.db') -app = Flask(__name__) -api = Api(app) +USERLENGTH = 50 -class Employees(Resource): - def get(self): - conn = db_connect.connect() # connect to database - query = conn.execute("select * from employees") # This line performs query and returns json result - return {'employees': [i[0] for i in query.cursor.fetchall()]} # Fetches first column that is Employee ID +import ipaddress +import random +import sys +import etcd +import ungleichapi +import json +import datetime -class Tracks(Resource): - def get(self): - conn = db_connect.connect() - query = conn.execute("select trackid, name, composer, unitprice from tracks;") - result = {'data': [dict(zip(tuple (query.keys()) ,i)) for i in query.cursor]} - return jsonify(result) +from flask import Flask, abort, request, Response +from flask_restful import reqparse -class Employees_Name(Resource): - def get(self, employee_id): - conn = db_connect.connect() - query = conn.execute("select * from employees where EmployeeId =%d " %int(employee_id)) - result = {'data': [dict(zip(tuple (query.keys()) ,i)) for i in query.cursor]} - return jsonify(result) +# app = Flask(__name__) +def get_random_ip(network): + net = ipaddress.IPv6Network(network) + addr_offset = random.randrange(2**64) + addr = net[0] + addr_offset -@app.route("/") -def hello(): - return """ -
+    return addr
 
+class Challenge(object):
+    """ A sample challenge -- inherit this and overwrite accordingly """
 
-Join the game
+    points = 0
+    provides = []
+    requires = []
 
-- Create an on account.ungleich.ch
- -> creates ldap account
+    def __init__(self, etcdclient):
+        self.client = etcdclient
 
-Creating the game:
+    def require_args(self, *args):
+        parser = reqparse.RequestParser()
+        for arg in args:
+            parser.add_argument(arg, required=True)
+        return parser.parse_args()
 
-- Create IPv6 only VM + http proxy
-- Intro on main page
-- Score list below
-- Users can be clicked
+    def describe(self):
+        return self.description
 
-
+ def save_points(self, user): + """ should be called when the challenge was solved successfully""" + path = "/ungleichgame/v1/{}/challenges/{}/points".format(user, self.__name__) + self.client.write(path, 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): + self.require_args("user", "network") + + try: + net = ipaddress.IPv6Network(args['network']) + network = args['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.client.write("/ungleichgame/v1/{}/network".format(user), network) + self.save_points(args['user']) + + return json.dumps("All good, go to /level/1 to start with level 1! - {}".format(data.value)) -api.add_resource(Employees, '/employees') # Route_1 -api.add_resource(Employees_Name, '/employees/') # Route_3 -#api.add_resource(Tracks, '/tracks') # Route_2 +class Game(object): + def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"): + self.client = etcdclient + self.app = Flask(name) + + self.app.add_url_rule('/', 'highscore', self.highscore) + self.app.add_url_rule('/highscore', 'highscore', self.highscore) + + # etcd paths are below here + self.etcbase = etcbase + self.userbase = "{}/user".format(self.etcbase) + + # Automate this + challenges = [ RegisterNet ] + + for challenge in challenges: + c = challenge(self.client) + name = c.__name__ + path = "/challenge/{}".format(name) + + self.app.add_url_rule(path, name, c.describe, methods=['GET']) + self.app.add_url_rule(path, name, c.solve, methods=['POST']) + + + 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 + + def get_highscore(self, username=None): + """ Returns a dict['username'] = points """ + + all_users = {} + highscore = {} + + print("getting high") + + if username: + path = "{}/{}".format(self.userbase, username) + user = self.read_etcd(path) + if user: + all_users[username] = user + else: + path = "{}/".format(self.userbase) + users = self.read_etcd(path, recursive=True) + print("reading from {}".format(path)) + if users: + for child in users.children: + print("adding user {} {} = {}".format(child, child.key, child.value)) + all_users[child.key] = child.value + + for k, v in all_users.items(): + # Ignore all kind of errors - just add the ones that work + try: + highscore[k] = json.loads(v)['points'] + print("f?") + except Exception as e: + print(e) + + return highscore + + def highscore(self): + point_list = self.get_highscore() + 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 Response("\n".join(res)) + + +# 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__': - app.run(port='5002') + # 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')