client -> server messages are now objects; better name validation; fixed concurrency bug

This commit is contained in:
troido 2019-09-26 18:30:14 +02:00
parent aa9f1913c8
commit 54c7238f2d
6 changed files with 215 additions and 87 deletions

View File

@ -17,7 +17,6 @@ class CommandHandler:
self.client = client
self.commands = {
"send": self.send,
"input": self.input,
"move": self.move,
"say": self.say,
@ -65,11 +64,8 @@ class CommandHandler:
# Commands
def send(self, data):
self.client.send(data)
def input(self, action):
self.send(["input", action])
self.client.sendInput(action)
def move(self, direction):
self.input(["move", direction])
@ -81,7 +77,7 @@ class CommandHandler:
self.input(["pick", option])
def chat(self, text):
self.send(["chat", text])
self.client.sendChat( text)
def log(self, text):

View File

@ -13,6 +13,7 @@ from queue import Queue
import ratuil.inputs
from .inputhandler import InputHandler
from asciifarm.common import messages
class Client:
@ -33,15 +34,24 @@ class Client:
self.queue = Queue()
def send(self, data):
text = json.dumps(data)
self.connection.send(text)
def sendMessage(self, message):
self.connection.send(json.dumps(message.to_json()))
def sendInput(self, inp):
message = messages.InputMessage(inp)
self.sendMessage(message)
def sendChat(self, text):
try:
self.sendMessage(messages.ChatMessage(text))
except messages.InvalidMessageError as e:
self.log(e.description)
def start(self):
self.sendMessage(messages.NameMessage(self.name))
threading.Thread(target=self.listen, daemon=True).start()
threading.Thread(target=self.getInput, daemon=True).start()
self.connection.send(json.dumps(["name", self.name]))
self.command_loop()
def listen(self):
@ -79,48 +89,53 @@ class Client:
self.close("error: name is already taken")
return
if error == "invalidname":
self.close("Invalid name error: "+ msg[2:])
self.close("Invalid name error: "+ str(msg[2:]))
return
self.log(" ".join(msg[1:]))
if msgType == 'field':
field = msg[1]
fieldWidth = field['width']
fieldHeight = field['height']
self.display.resizeField((fieldWidth, fieldHeight))
fieldCells = field['field']
mapping = field['mapping']
self.display.drawFieldCells(
(
tuple(reversed(divmod(i, fieldWidth))),
mapping[spr]
)
for i, spr in enumerate(fieldCells))
if msgType == 'changecells' and len(msg[1]):
self.display.drawFieldCells(msg[1])
if msgType == "playerpos":
self.display.setFieldCenter(msg[1])
if msgType == "health":
health, maxHealth = msg[1]
self.display.setHealth(health, maxHealth)
if maxHealth is None:
self.log("You have died. Restart the client to respawn")
if msgType == "inventory":
self.display.setInventory(msg[1])
if msgType == "equipment":
self.display.setEquipment(msg[1])
if msgType == "ground":
self.display.setGround(msg[1])
if msgType == "message":
self.log(*msg[1:])
if msgType == "options":
if msg[1] != None:
description, options = msg[1]
self.log(description)
for option in options:
self.log(option)
if msgType == "world":
for msg in msg[1]:
msgType = msg[0]
if msgType == 'field':
field = msg[1]
fieldWidth = field['width']
fieldHeight = field['height']
self.display.resizeField((fieldWidth, fieldHeight))
fieldCells = field['field']
mapping = field['mapping']
self.display.drawFieldCells(
(
tuple(reversed(divmod(i, fieldWidth))),
mapping[spr]
)
for i, spr in enumerate(fieldCells))
if msgType == 'changecells' and len(msg[1]):
self.display.drawFieldCells(msg[1])
if msgType == "playerpos":
self.display.setFieldCenter(msg[1])
if msgType == "health":
health, maxHealth = msg[1]
self.display.setHealth(health, maxHealth)
if maxHealth is None:
self.log("You have died. Restart the client to respawn")
if msgType == "inventory":
self.display.setInventory(msg[1])
if msgType == "equipment":
self.display.setEquipment(msg[1])
if msgType == "ground":
self.display.setGround(msg[1])
if msgType == "message":
self.log(*msg[1:])
if msgType == "options":
if msg[1] != None:
description, options = msg[1]
self.log(description)
for option in options:
self.log(option)
def log(self, text, type=None):

View File

@ -23,7 +23,7 @@ def parse_args(argv):
Kill the goblins and plant the seeds.
~troido""", formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-n', '--name', help='Your player name (must be unique!). Defaults to username on inet sockets and tildename on (unix socket (including abstract)', default=None)
parser.add_argument('-n', '--name', help='Your player name (must be unique!). Defaults to username on inet sockets and tildename on unix socket (including abstract). Apart from the tilde in a tildename all characters must be unicode letters, numbers or connection puctuation. The maximum size of a name is 256 bytes when encoded as utf8', default=None)
parser.add_argument("-a", "--address", help="The address of the socket. When the socket type is 'abstract' this is just a name. When it is 'unix' this is a filename. When it is 'inet' is should be in the format 'address:port', eg 'localhost:8080'. Defaults depends on the socket type")
parser.add_argument("-s", "--socket", help="the socket type. 'unix' is unix domain sockets, 'abstract' is abstract unix domain sockets and 'inet' is inet sockets. ", choices=["abstract", "unix", "inet"], default="abstract")
parser.add_argument('-k', '--keybindings', help='The file with the keybinding configuration. This file is a JSON file.', default="default")

View File

@ -0,0 +1,98 @@
import re
import unicodedata
class InvalidMessageError(Exception):
errType = "invalidmessage"
description = ""
def __init__(self, description="", errType=None):
self.description = description
if errType is not None:
self.errType = errType
class InvalidNameError(InvalidMessageError):
errType = "invalidname"
class Message:
@classmethod
def msgType(cls):
return cls.typename
def to_json(self):
raise NotImplementedError
@classmethod
def from_json(cls, jsonobj):
raise NotImplementedError
class ClientToServerMessage(Message):
def body(self):
raise NotImplementedError
def to_json(self):
return [self.typename, self.body()]
@classmethod
def from_json(cls, jsonlist):
assert len(jsonlist) == 2, InvalidMessageError
typename, body = jsonlist
assert typename == cls.msgType(), InvalidMessageError
return cls(body)
class NameMessage(ClientToServerMessage):
typename = "name"
nameRegex = re.compile("(~|\w)\w*")
categories = {"Lu", "Ll", "Lt", "Lm", "Lo", "Nd", "Nl", "No", "Pc"}
def __init__(self, name):
assert isinstance(name, str), InvalidNameError("name must be a string")
assert (len(name) > 0), InvalidNameError("name needs at least one character")
assert (len(bytes(name, "utf-8")) <= 256), InvalidNameError("name may not be longer than 256 utf8 bytes")
for char in name if name[0] != "~" else name[1:]:
category = unicodedata.category(char)
assert category in self.categories, InvalidNameError("all name caracters must be in these unicode categories: " + "|".join(self.categories) + " (except the tilde in a tildename)")
#assert (name.rfind("~") < 1), InvalidNameError("tilde character may only occur at start of name")
#assert (self.nameRegex.match(name) is not None), InvalidNameError("name must match the following regex: {}".format(self.nameRegex.pattern))
self.name = name
def body(self):
return self.name
class InputMessage(ClientToServerMessage):
typename = "input"
def __init__(self, inp):
self.inp = inp
def body(self):
return self.inp
class ChatMessage(ClientToServerMessage):
typename = "chat"
def __init__(self, text):
assert isinstance(text, str), InvalidMessageError("chat message must be a string")
assert text.isprintable(), InvalidMessageError("chat messages may only contain printable unicode characters")
self.text = text
def body(self):
return self.text
messages = {message.msgType(): message for message in [
NameMessage,
InputMessage,
ChatMessage
]}

View File

@ -11,6 +11,8 @@ from . import view
from . import socketserver as server
from . import player
from asciifarm.common import messages
import re
nameRegex = re.compile("(~|\w)\w*")
@ -37,7 +39,7 @@ class GameServer:
data = view.playerView(name)
if data is None:
continue
databytes = bytes(json.dumps(data), 'utf-8')
databytes = bytes(json.dumps(["world", data]), 'utf-8')
self.serv.send(connection, databytes)
@ -56,48 +58,65 @@ class GameServer:
return
msg = data
msgType = msg[0]
if msgType == "name":
name = msg[1]
if len(name) < 1:
self.error(n, "invalidname", "name needs at least one character")
return
if len(bytes(name, "utf-8")) > 256:
self.error(n, "invalidname", "name may not be longer than 256 utf8 bytes")
return
if nameRegex.match(name) is None:
self.error(n, "invalidname", "Name must match the following regex: {}".format(nameRegex.pattern))
return
if name[0] == "~" and name[1:] != self.serv.getUsername(n):
self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
return
if name in self.players:
self.error(n, "nametaken", "another connection to this player already exists")
return
self.connections[n] = name
self.players[name] = n
self.messages.put(("join", name))
print("new player: "+name)
self.broadcast("{} has connected".format(name), "connect")
elif msgType == "input":
if n in self.connections:
self.messages.put(("input", self.connections[n], msg[1]))
elif msgType == "chat":
if n in self.connections:
name = self.connections[n]
if not msg[1].isprintable():
self.error("invalidmessage", "Chat message may only contain printable unicode characters")
message = name + ": " + msg[1]
print(message)
self.broadcast(message, "chat")
if not isinstance(msgType, str):
self.error(n, "invalidmessage", "Message type must be a string")
return
msgClass = messages.messages.get(msgType)
if msgClass is None:
self.error(n, "invalidmessage", "Unknown message type '{}'".format(msgType))
return
try:
message = msgClass.from_json(msg)
except messages.InvalidMessageError as e:
self.error(n, e.errType, e.description)
return
self.handleMessage(n, message)
except Exception as e:
print(e)
self.error(n, "invalidmessage", "An unknown error occured in handling the message")
def handleMessage(self, n, message):
# I wish I had type overloading
if isinstance(message, messages.NameMessage):
self.handleNameMessage(n, message)
elif isinstance(message, messages.InputMessage):
self.handleInputMessage(n, message)
elif isinstance(message, messages.ChatMessage):
self.handleChatMessage(n, message)
else:
self.error(n, "invalidmessage", "unknown message '{}'".format(message.__class__))
def error(self, n, errtype, *data):
self.serv.send(n, bytes(json.dumps(["error", errtype]+list(data)), "utf-8"))
def handleNameMessage(self, n, message):
name = message.name
if name[0] == "~" and name[1:] != self.serv.getUsername(n):
self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
return
if name in self.players:
self.error(n, "nametaken", "another connection to this player already exists")
return
self.connections[n] = name
self.players[name] = n
self.messages.put(("join", name))
print("new player: "+name)
self.broadcast("{} has connected".format(name), "connect")
def handleInputMessage(self, n, message):
if n in self.connections:
self.messages.put(("input", self.connections[n], message.inp))
def handleChatMessage(self, n, msg):#if n in self.connections:
name = self.connections[n]
message = name + ": " + msg.text
print(message)
self.broadcast(message, "chat")
def error(self, n, errtype, description=""):
self.serv.send(n, bytes(json.dumps(["error", errtype, description]), "utf-8"))
def close(self, connection):
if connection in self.connections:

View File

@ -133,7 +133,7 @@ class Server:
header = length.to_bytes(4, byteorder="big")
try:
connection.sendall(header + msg)
except BrokenPipeError:
except (BrokenPipeError, OSError):
self.closeConnection(connection)
def broadcast(self, msg):