Compare commits

...

8 commits
0.2 ... master

Author SHA1 Message Date
c7e5cd437b Update README.md 2019-12-01 06:13:16 +01:00
25388b1baf Add new file 2019-12-01 06:11:56 +01:00
3053ba3d31 Update README.md 2019-12-01 05:58:46 +01:00
89037034cf Update README.md 2019-12-01 05:56:47 +01:00
3fdb385ad9 Update server.py 2019-12-01 05:45:34 +01:00
1e19efc15f Add new file 2019-12-01 05:44:49 +01:00
e440b54ed0 Update server.py 2019-11-10 14:45:28 +01:00
cb10b41d40 Update README.md 2019-11-10 14:39:17 +01:00
4 changed files with 574 additions and 167 deletions

View file

@ -15,36 +15,43 @@ the project name might change later.
* Register
* List challenges - have fun!
### How to play (for instance on Nico's notebook)
### How to play (for instance on sxiii's laptop)
* Note: here HTTPie is used; can be replaced by curl or any other http tool
* Note2: example with localhost [::] is ipv6 localhost representation
* Note3: you can ran sample commands as-is in most cases, as they utilize your user name automatically
0. Try to access the game server without any argument
```
http [::]:5002
```
1. Register: send a POST request with your username
```
curl -d user=nico http://nico.ungleich.cloud:5002/register
http POST [::]:5002/register user=$USER
```
2. Get challenges
```
curl http://nico.ungleich.cloud:5002/challenge
http [::]:5002/challenge
```
3. Get a challenge description
```
curl http://nico.ungleich.cloud:5002/challenge/registernet
http [::]:5002/challenge/RegisterNet
```
4. Solve a challenge
```
curl -d user=nico -d 2a0a:e5c0:101::/64 http://nico.ungleich.cloud:5002/challenge/RegisterNet
http POST [::]:5002/challenge/RegisterNet 'user=$USER' 'network=2a0a:e5c0:101::/64'
```
5. Get high score
```
curl http://nico.ungleich.cloud:5002/highscore
http POST [::]:5002/points 'user=$USER'
```
@ -76,6 +83,8 @@ Run
python server.py
```
### Testing game
If you want to automatically test the game server, run the ./test_game.sh script included in this directory.
## Overview - Security
@ -90,6 +99,15 @@ The base for building games is:
* Flask - web frontend
* etcd - storing data, games, etcd.
## Requirements
Tested packages @ Ubuntu 18.04.3
* etcd3 (important, version 3)
* python3-etcd
* python3-flask
* python3-flask-restful
## Things to solve
* Enhance the Challenge class - maybe make it easier for challenges to abort

353
archive/oldserver.py Normal file
View file

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

288
server.py
View file

@ -2,54 +2,39 @@
USERLENGTH = 50
import ipaddress
import random
import sys
import etcd
import json
import datetime
# Important imports
from flask import Flask, abort, request, Response
import ipaddress, random, sys, etcd3, json, datetime, os
from flask import Flask, abort, request, Response, jsonify
from flask_restful import reqparse
from etcd3_wrapper import Etcd3Wrapper
# Generate random IPv6 address
def get_random_ip(network):
net = ipaddress.IPv6Network(network)
addr_offset = random.randrange(2**64)
addr = net[0] + addr_offset
return addr
# Check our own IPv6 address by using ipv6-test API
# This might be used to check internet connectivity
# def check_internet():
# import urllib.request, urllib.error, urllib.parse
# url = 'http://v6.ipv6-test.com/api/myip.php'
# response = urllib.request.urlopen(url)
# webContent = response.read()
# return webContent
# RESTful flask arguments parsing
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)
# Classes
class Challenge(object):
""" A sample challenge -- inherit this and overwrite accordingly """
@ -76,16 +61,22 @@ class Challenge(object):
""" 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)
#self.db.set_user_key(user, key, self.points)
self.wrapper.put('/users' + user + '/points', self.points)
def solve(self):
""" Needs to be implemented per challenge """
pass
# Challenge description works
class RegisterNet(Challenge):
#self.etcbase = etcbase
#self.wrapper = Etcd3Wrapper()
#self.app = Flask(name)
wrapper = Etcd3Wrapper()
points = 10
provides = [ "network" ]
description = """
Register a /64 IPv6 network that you fully control.
Many other challenges depend on this. You will need to
@ -95,6 +86,7 @@ and to setup services listening on these IPv6 addresses.
Submit your network with the "network" parameter.
"""
def solve(self):
#self.wrapper = Etcd3Wrapper()
args = require_args("user", "network")
network = args['network']
user = args['user']
@ -107,16 +99,25 @@ Submit your network with the "network" parameter.
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)
path = "{}/{}/points".format("/user", user)
cur = self.wrapper.get(path)
if cur == None:
self.wrapper.put('/user/' + user + '/network', network)
points = self.wrapper.get(path)
self.wrapper.put('/user/' + user + '/points', '20') # Adjust points for first challenge here
x = "Network {} registered, have fun with the next challenge!\n".format(network)
else:
x = "Network {} is already registered.\n".format(network)
#self.db.set_user_key(user, "network", network)
#save_points(user)
return x
return "Network {} registered, have fun with the next challenge!".format(network)
######################################
class IPv6Address(Challenge):
wrapper = Etcd3Wrapper()
points = 20
requires = [ "network" ]
description = """
You have setup your network, great!
Now it is time to show that you are really controlling your network!
@ -131,23 +132,22 @@ and POST to this address, when it should be reachable by ping6.
def describe(self):
args = require_args("user")
user = args['user']
key = "network"
#key = "network"
network = self.db.get_user_key_or_none(user, key)
network = self.wrapper.get('/user/' + user + '/network')
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)
#key = "address"
address = self.wrapper.get('/user/' + user + '/ip')
if not address:
address = get_random_ip(network.value)
self.db.set_user_key(user, key, address)
#self.db.set_user_key(user, key, address)
else:
address = address.value
@ -156,35 +156,76 @@ provide the network are:
def solve(self):
args = require_args("user")
user = args['user']
return Response(status=400, response="""
Not yet implemented""")
address = self.wrapper.get('/user/' + user + '/ip')
response = os.system('ping -c 1 -t 3 ' + address.value)
if response == 0:
x = address.value + ' is up!'
else:
x = address.value + ' is down!'
return x
class Game(object):
def __init__(self, name, etcdclient, etcbase="/ungleichgame/v1"):
self.client = etcdclient
def __init__(self, name, etcdclient, etcbase="/"):
self.etcbase = etcbase
self.wrapper = etcdWrapper(etcdclient, self.etcbase)
self.wrapper = Etcd3Wrapper()
self.app = Flask(name)
#self.userbase = "{}/user".format(self.etcbase)
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'])
# Index text works! _
# etcd paths are below here
def index():
try:
from_etcd = self.wrapper.get("/index")
response = from_etcd.key, ' : ', from_etcd.value
except:
response = 'The key is empty.'
return Response(response, 200)
self.app.add_url_rule('/', 'index', index)
self.userbase = "{}/user".format(self.etcbase)
# End of index ^
# The registration works! _
def register():
args = require_args("user")
user = args['user']
value = str(datetime.datetime.now())
netaddr = request.remote_addr
cur = self.wrapper.get('/user/' + user)
if cur == None:
self.wrapper.put('/user/' + user, 'user exist')
self.wrapper.put('/user/' + user + '/registered_at', value)
self.wrapper.put('/user/' + user + '/ip', netaddr)
x = "User @" + user + " successfully registered on " + value + " with IP: " + netaddr + '\n'
else:
time = self.wrapper.get('/user/' + user + '/registered_at')
ip = self.wrapper.get('/user/' + user + '/ip')
x = "User @" + user + " is already registered on " + time.value + ". His IP is: " + ip.value + '\n'
return Response(x,200)
self.app.add_url_rule('/register', 'register', register, methods=['POST'])
# End of registration ^
# Getting point for one user works! _
def points():
args = require_args("user")
user = args['user']
try:
from_etcd = self.wrapper.get("/user/" + user + "/points")
response = from_etcd.key, ' : ', from_etcd.value
except:
response = 'The key is empty.'
return Response(response, 200)
self.app.add_url_rule('/points', 'points', points, methods=['POST'])
# End of getting points ^
# 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:
@ -212,45 +253,46 @@ class Game(object):
challenge.dependencies_provided_by[requirement] = self.providers[requirement]
# List of challenges works _
def list_of_challenges(self):
def list_of_challenges():
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))
self.app.add_url_rule('/challenge', 'list_of_challenges', list_of_challenges)
self.app.add_url_rule('/challenge/', 'list_of_challenges', list_of_challenges)
# End of list of challenges ^
def get_points(self):
def get_points(user):
""" Returns a dict['username'] = points """
user_points = {}
#user_points = {}
path = "{}/".format(self.userbase)
users = self.wrapper.read_key_or_none(path)
#path = "{}/".format(self.userbase)
#users = self.wrapper.read_key_or_none(path)
points = self.wrapper.get('/user/' + user + '/points').value
if users:
for user in users.children:
username = user.key.replace(path,"")
user_points[username] = 0
###if users:
### for user in users:
# username = user.key.replace(path,"")
# user_points[username] = 0
### print("inside users: user " + user)
#point_path = "{}/points".format(user.key)
### points = self.wrapper.get("{}/points".format(user))
### print("Points: " + points)
# if not points:
# continue
point_path = "{}/points".format(user.key)
points = self.wrapper.read_key_or_none(point_path, recursive=True)
# for challenge in points.children:
# user_points[username] += int(challenge.value)
if not points:
continue
# return user_points
for challenge in points.children:
user_points[username] += int(challenge.value)
return user_points
def index(self):
points = self.points()
# def index(self):
# points = self.points()
return """Welcome to the game server!
@ -278,76 +320,8 @@ 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")
# This (below) works :D
if __name__ == '__main__':
g = Game(__name__, etcd.Client(port=2379))
g.app.run(host="::", port='5002')
g = Game(__name__, etcd3.client(port=2379))
g.app.run(host="::", port=5002)

62
test_game.sh Normal file
View file

@ -0,0 +1,62 @@
#!/bin/bash
###################################################################
# Ungleich-game testing script
# NB: For this script to work you need to start server first:
# python3 ./server.py
############################# written by @sxiii on 30.11.2019 #####
sleepdelay="10" # Delay between commands
gameaddr="[::]:5002" # Game server address
user="sxiii-$(date +%HH%MM)" # User to register
network="2a0a:e5c0:101::/64" # Network to register
cmd="http $gameaddr"
echo -e "Getting information about the game:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/register user=$user"
echo -e "Registering a new user called $user:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/register user=$user"
echo -e "Trying to register with the same user again:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http $gameaddr/challenge"
echo -e "Looking which challenges are available:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http $gameaddr/challenge/RegisterNet"
echo -e "Reading challenge #1 description:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/challenge/RegisterNet user=$user network=$network"
echo -e "Registering network (challenge 1):\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/challenge/RegisterNet user=$user network=$network"
echo -e "Trying to register network again:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/points user=$user"
echo -e "Getting points for our player:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http $gameaddr/challenge/IPv6Address"
echo -e "Reading challenge #2 description:\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
cmd="http POST $gameaddr/challenge/IPv6Address user=sxiii"
echo -e "Checking if IP is pingable (challenge 2):\n $ $cmd"
sleep $sleepdelay && eval "$cmd"
sleep $sleepdelay
toilet --rainbow -t "Done! Thank you :)"
#1) Information about the game: ("curl ip/index")
#2) Registration of the new user with his IP and date: ("curl -X POST -d user=sxiii ip/register")
#3) Checking if user registered, it returns user IP (re-run previous command again)
#4) Challenges listing ("curl ip/challenge")
#5) RegisterNet challenge ("curl -X POST -d user=sxiii -d 'network=2a0a:e5c0:101::/64' ip/challenge/RegisterNet")
#6) Checking if challenge already done; it returns error (re-run prev. command)
#7) Getting points for user: ``` curl -X POST -d user=sxiii ip/points ```
#8) Challenge IPv6Address (checking if IP is pingable): ``` curl -X POST -d user=sxiii ip/challenge/IPv6Address ```