Skip to content

Commit ad1d0f4

Browse files
committed
Update
1 parent caad02f commit ad1d0f4

File tree

11 files changed

+232
-63
lines changed

11 files changed

+232
-63
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ A lightweight server and framework for turn-based multiplayer games.
1010

1111
![](.github/game_server.svg)
1212

13+
Only basic programming skills are required to implement clients or to add new games to the server.
14+
1315
## Overview
1416

1517
### Features
@@ -36,7 +38,7 @@ This server was developed for use in a university programming course, where stud
3638

3739
## Operating the server
3840

39-
To run the server in a network, edit IP and port in the configuration file (`server/config.py`). If you intend to run the server as a systemd service, you can use the provided unit file as a starting point. Server and API are implemented in plain Python. Only modules from the standard library are used. This makes the server easy to handle.
41+
To run the server in a network, edit IP and port in the configuration file (`server/config.py`). TLS can also be enabled. If you intend to run the server as a systemd service, you can use the provided unit file. Server and API are implemented in plain Python. Only modules from the standard library are used. This makes the server easy to handle.
4042

4143
## Implementing clients
4244

@@ -46,7 +48,8 @@ Module `game_server_api` provides an API for communicating with the server. It a
4648
- submit moves
4749
- retrieve the game state
4850
- passively observe another player
49-
- start a new game within the current session
51+
- restart a game within the current session
52+
- enable TLS
5053

5154
Here is a short demo of the API usage:
5255

client/cert.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBPDCB76ADAgECAhR80LMQEzjOb5Z3qp3KxSSAEESfPjAFBgMrZXAwFDESMBAG
3+
A1UEAwwJbG9jYWxob3N0MB4XDTI2MDMxNzIxMTgyN1oXDTM2MDMxNDIxMTgyN1ow
4+
FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEA2IItFX1uAVrek+ek0YMQ
5+
h+dxc8AwPrRU79NmeIZuqjCjUzBRMB0GA1UdDgQWBBT6fAh+V0x78fkUZgNEfaeq
6+
XNCCrDAfBgNVHSMEGDAWgBT6fAh+V0x78fkUZgNEfaeqXNCCrDAPBgNVHRMBAf8E
7+
BTADAQH/MAUGAytlcANBAMrDCYdBbubPvBusl2zV5DmUSD1GkM+S/Lkmy3cTXghN
8+
LwuiwCMepXO7f047/AN7AC21IlgyqnO8U7k6cUCF/Qk=
9+
-----END CERTIFICATE-----

client/echo_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
from game_server_api import GameServerAPI, GameServerError, IllegalMove
1414

1515
game = GameServerAPI(server='127.0.0.1', port=4711, game='Echo', session='mygame', players=1)
16+
game.enable_tls(cert='cert.pem')
17+
# game.enable_tls()
18+
# TODO TLS rausnehmen
1619

1720
my_id = game.join()
1821
state = game.state()

client/game_server_api.py

Lines changed: 113 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@
2626
- submit moves to the server
2727
- retrieve the game state
2828
- passively observe a specific player
29-
- restart a game without starting a new session
29+
- restart a game within the current session
30+
- enable TLS
3031
"""
3132

3233
import json
34+
import os
3335
import socket
36+
import ssl
3437
import traceback
3538

36-
class GameServerError(Exception):
37-
pass
38-
39-
class IllegalMove(Exception):
40-
pass
39+
class GameServerError(Exception): pass
40+
class IllegalMove(Exception): pass
4141

4242
class GameServerAPI:
4343
"""
@@ -103,6 +103,10 @@ def __init__(self, server, port, game, session='auto', players=None, name=''):
103103
# tcp connections:
104104
self._buffer_size = 4096 # bytes, corresponds to server-side buffer size value
105105
self._request_size_max = int(1e6) # bytes, updated after joining a game
106+
107+
# tls:
108+
self._tls_enabled = False
109+
self._tls_cert = None
106110

107111
def join(self):
108112
"""
@@ -300,6 +304,21 @@ def restart(self):
300304

301305
if err: raise GameServerError(err)
302306

307+
def enable_tls(self, cert=''):
308+
"""
309+
Calling this function enables TLS encryption. By providing a
310+
certificate, authentication of the server is performed.
311+
312+
The server must have TLS enabled.
313+
314+
Parameters:
315+
cert (str): certificate (optional)
316+
"""
317+
assert type(cert) == str, self._error('cert')
318+
319+
self._tls_enabled = True
320+
self._tls_cert = self._abs_path(cert)
321+
303322
def _send(self, data):
304323
"""
305324
Send data to the server and receive its response.
@@ -328,47 +347,94 @@ def _send(self, data):
328347

329348
# create a socket:
330349
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sd:
331-
try:
332-
# connect to server:
333-
sd.settimeout(5)
334-
sd.connect((self._server, self._port))
335-
sd.settimeout(None) # let server handle timeouts
336-
except:
337-
return self._api_error(f'unable to connect to {self._server}:{self._port}')
338-
339-
try:
340-
# send data to server:
341-
sd.sendall(request)
342-
343-
# receive data from server:
344-
response = bytearray()
345-
346-
while True:
347-
data = sd.recv(self._buffer_size)
348-
if not data: break
349-
response += data
350-
351-
if not response: raise self._NoResponse
352-
response = json.loads(response.decode())
353-
354-
# return data:
355-
if response['status'] != 'ok': # server responded with an error
356-
return None, response['message'], response['status']
357-
358-
return response['data'], None, None
359-
360-
except socket.timeout:
361-
return self._api_error('connection timed out')
362-
except self._NoResponse:
363-
return self._api_error('empty or no response received from server')
364-
except (ConnectionResetError, BrokenPipeError):
365-
return self._api_error('connection closed by server')
366-
except UnicodeDecodeError:
367-
return self._api_error('could not decode binary data received from server')
368-
except json.decoder.JSONDecodeError:
369-
return self._api_error('corrupt json received from server')
370-
except:
371-
return self._api_error('unexpected exception:\n' + traceback.format_exc())
350+
with self._secure_socket(sd) as sd:
351+
try:
352+
# connect to server:
353+
sd.settimeout(5)
354+
sd.connect((self._server, self._port))
355+
sd.settimeout(None) # let server handle timeouts
356+
except IndexError:
357+
return self._api_error(f'unable to connect to {self._server}:{self._port}')
358+
359+
try:
360+
# send data to server:
361+
sd.sendall(request)
362+
363+
# receive data from server:
364+
response = bytearray()
365+
366+
while True:
367+
data = sd.recv(self._buffer_size)
368+
if not data: break
369+
response += data
370+
371+
if not response: raise self._NoResponse
372+
response = json.loads(response.decode())
373+
374+
# return data:
375+
if response['status'] != 'ok': # server responded with an error
376+
return None, response['message'], response['status']
377+
378+
return response['data'], None, None
379+
380+
except socket.timeout:
381+
return self._api_error('connection timed out')
382+
except self._NoResponse:
383+
return self._api_error('empty or no response received from server')
384+
except (ConnectionResetError, BrokenPipeError):
385+
return self._api_error('connection closed by server')
386+
except UnicodeDecodeError:
387+
return self._api_error('could not decode binary data received from server')
388+
except json.decoder.JSONDecodeError:
389+
return self._api_error('corrupt json received from server')
390+
except:
391+
return self._api_error('unexpected exception:\n' + traceback.format_exc())
392+
393+
def _secure_socket(self, socket):
394+
"""
395+
This function wraps the socket and returns an SSL socket. TLS must be
396+
enabled by calling API function enable_tls. Otherwise, the passed socket
397+
is returned unmodified. If a certificate was passed to function
398+
enable_tls, authentication of the server is enabled. Without a
399+
certificate, TLS is used for encryption only.
400+
401+
Parameters:
402+
socket (socket): a regular socket
403+
404+
Returns:
405+
socket or SSLSocket: an SSL socket, if TLS is enabled, a regular socket otherwise
406+
407+
Raises:
408+
# TODO
409+
"""
410+
if self._tls_enabled:
411+
context = ssl.create_default_context()
412+
413+
if self._tls_cert:
414+
context.load_verify_locations(self._tls_cert)
415+
else:
416+
context.check_hostname = False
417+
context.verify_mode = ssl.CERT_NONE
418+
419+
return context.wrap_socket(socket, server_hostname=self._server)
420+
421+
return socket
422+
423+
def _abs_path(self, file_path):
424+
"""
425+
Always returns the absolute path to the file, regardless of where the
426+
file is located or from where the program was called.
427+
428+
Parameters:
429+
file_path (str): path to file, relative or absolute
430+
431+
Returns:
432+
str: absolute path to file
433+
"""
434+
if not file_path or os.path.isabs(file_path):
435+
return file_path
436+
437+
return os.path.join(os.path.abspath(os.path.dirname(__file__)), file_path)
372438

373439
@staticmethod
374440
def _api_error(message):

client/wrong_cert.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBPDCB76ADAgECAhRmDEjN38kb9dePsgI5Z9capYBbnTAFBgMrZXAwFDESMBAG
3+
A1UEAwwJbG9jYWxob3N0MB4XDTI2MDMyMTE1NTM0OFoXDTM2MDMxODE1NTM0OFow
4+
FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAlrlDYa/OIPhQDuZL5Rgd
5+
YBpVmipVvFiIjkmqBgYLcHyjUzBRMB0GA1UdDgQWBBQFoLqqFIwuVdCQik0hUJpF
6+
nFMYHjAfBgNVHSMEGDAWgBQFoLqqFIwuVdCQik0hUJpFnFMYHjAPBgNVHRMBAf8E
7+
BTADAQH/MAUGAytlcANBADShzpnkM3DtXQP5zZqOcylIqE1JDqy8VQlns0gTtvel
8+
4g0BvAMSLZRJJHYbzCb+3+5CjKveY/QVWWZ1CmnzWw4=
9+
-----END CERTIFICATE-----

client/wrong_host.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBNjCB6aADAgECAhRC4l2QYZpbjj2PxkiJ18ZU+IyrIDAFBgMrZXAwETEPMA0G
3+
A1UEAwwGcXdlcnR6MB4XDTI2MDMyMTE1NTQxMloXDTM2MDMxODE1NTQxMlowETEP
4+
MA0GA1UEAwwGcXdlcnR6MCowBQYDK2VwAyEAe/GKS4+p+nJS7yoCPUMQafqDP5Bk
5+
rYUlHYOJ7A6caW+jUzBRMB0GA1UdDgQWBBQymq7ri17CZZOi8vzokKhY78BhGzAf
6+
BgNVHSMEGDAWgBQymq7ri17CZZOi8vzokKhY78BhGzAPBgNVHRMBAf8EBTADAQH/
7+
MAUGAytlcANBAIs148vo8OdMCmUGmqnW5VAuG1TVkidaoX2O//MjSM2j3LuWKEQy
8+
a3wKxXGpS/ZPYtOVWThXZ+fSdslG6hTWrg0=
9+
-----END CERTIFICATE-----

server/cert.pem

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBPDCB76ADAgECAhR80LMQEzjOb5Z3qp3KxSSAEESfPjAFBgMrZXAwFDESMBAG
3+
A1UEAwwJbG9jYWxob3N0MB4XDTI2MDMxNzIxMTgyN1oXDTM2MDMxNDIxMTgyN1ow
4+
FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEA2IItFX1uAVrek+ek0YMQ
5+
h+dxc8AwPrRU79NmeIZuqjCjUzBRMB0GA1UdDgQWBBT6fAh+V0x78fkUZgNEfaeq
6+
XNCCrDAfBgNVHSMEGDAWgBT6fAh+V0x78fkUZgNEfaeqXNCCrDAPBgNVHRMBAf8E
7+
BTADAQH/MAUGAytlcANBAMrDCYdBbubPvBusl2zV5DmUSD1GkM+S/Lkmy3cTXghN
8+
LwuiwCMepXO7f047/AN7AC21IlgyqnO8U7k6cUCF/Qk=
9+
-----END CERTIFICATE-----

server/config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,17 @@
3535
log_framework_actions = True # actions performed by the framework, such as terminating games
3636

3737
# TCP CONNECTIONS:
38-
# pick a higher value for request_size_max if required by a new game;
39-
# it should not be necessary to change buffer_size or connection_timeout
38+
# pick a higher value for request_size_max if required by a new game
4039
request_size_max = int(1e6) # bytes, prevents clients from sending too much data
40+
41+
# it should not be necessary to change buffer_size or connection_timeout
4142
buffer_size = 4096 # bytes, corresponds to client-side buffer size value
4243
connection_timeout = 60 # seconds, timeout for tcp transactions
44+
45+
# TLS:
46+
# to enable TLS, specify certificate and key (the client must enable TLS as well)
47+
tls_cert = ''
48+
tls_key = ''
49+
# TODO entfernen:
50+
tls_cert = 'cert.pem'
51+
tls_key = 'key.pem'

server/game_server.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import json
3030
import socket
31+
import ssl
3132
import threading
3233
import traceback
3334

@@ -57,7 +58,7 @@ def handle_connection(conn, ip, port):
5758
possible, error messages are sent back to the client.
5859
5960
Parameters:
60-
conn (socket): connection socket
61+
conn (socket or SSLSocket): connection socket
6162
ip (str): client IP
6263
port (int): client port
6364
"""
@@ -133,25 +134,57 @@ def handle_connection(conn, ip, port):
133134
conn.close()
134135
log.info('connection closed by server')
135136

137+
def secure_socket(socket):
138+
"""
139+
This function wraps the socket and returns an SSL socket. TLS must be
140+
enabled in the config module. Otherwise, the passed socket is returned
141+
unmodified.
142+
143+
Parameters:
144+
socket (socket): listening socket
145+
146+
Returns:
147+
socket or SSLSocket: an SSL socket, if TLS is enabled, a regular socket otherwise
148+
149+
Raises:
150+
# TODO
151+
"""
152+
if config.tls_cert and config.tls_key:
153+
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
154+
context.minimum_version = ssl.TLSVersion.TLSv1_2
155+
context.load_cert_chain(
156+
certfile=utility.abs_path(config.tls_cert),
157+
keyfile=utility.abs_path(config.tls_key))
158+
159+
print('TLS enabled')
160+
161+
return context.wrap_socket(socket, server_side=True)
162+
163+
return socket
164+
136165
print("""This is free software with ABSOLUTELY NO WARRANTY.
137166
Licensed under the GPL version 3 (see LICENSE).""")
138167

139168
try:
169+
print('Server starting')
170+
140171
# create listening socket:
141172
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sd:
142173
sd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
143174
sd.bind((config.ip, config.port))
144175
sd.listen()
145176

146-
print(f'Listening on {config.ip}:{config.port}')
177+
with secure_socket(sd) as sd:
178+
print(f'Listening on {config.ip}:{config.port}')
147179

148-
while True:
149-
# accept a connection:
150-
conn, client = sd.accept()
151-
ip, port = client
180+
# accept connections and handle them in separate threads:
181+
while True:
182+
conn, client = sd.accept()
183+
ip, port = client
152184

153-
# handle connection in separate thread:
154-
t = threading.Thread(target=handle_connection, args=(conn, ip, port), daemon=True)
155-
t.start()
185+
threading.Thread(
186+
target=handle_connection,
187+
args=(conn, ip, port),
188+
daemon=True).start()
156189
except KeyboardInterrupt:
157-
print('')
190+
print('\nServer shutting down')

server/key.pem

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MC4CAQAwBQYDK2VwBCIEIKAFSUn9/JAK704eqW2hKNpT+NDoMaN7zHu40jMIok6q
3+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)