Cleanup, begin challenge class

This commit is contained in:
Nico Schottelius 2019-05-26 21:19:58 +02:00
parent 063da32ae9
commit f51b5f4c73
11 changed files with 340 additions and 292 deletions

112
README.md
View file

@ -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-<YOURNAME>.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)

62
archive/server.py Normal file
View file

@ -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 """
<pre>
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
</pre>
"""
api.add_resource(Employees, '/employees') # Route_1
api.add_resource(Employees_Name, '/employees/<employee_id>') # Route_3
#api.add_resource(Tracks, '/tracks') # Route_2
if __name__ == '__main__':
app.run(port='5002')

View file

@ -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("<p>{} has {} points</p>".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')

251
server.py
View file

@ -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 """
<pre>
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
</pre>
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/<employee_id>') # 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("<p>{} has {} points</p>".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')