Skip to content

Commit ce2dcc2

Browse files
authored
Merge pull request #942 from Debilski/feature/invalid-team-name
2 parents 8268406 + 9bdf8f7 commit ce2dcc2

6 files changed

Lines changed: 349 additions & 278 deletions

File tree

pelita/scripts/pelita_player.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -205,25 +205,35 @@ def player_handle_request(socket, poller, team, team_name_override=False, silent
205205
raise RuntimeError("Created bad reply message")
206206

207207

208-
def check_team_name(name):
209-
# Team name must be ascii
208+
def sanitize_team_name(string):
209+
"""Strip all non-ascii characters from team name"""
210+
sane = []
211+
# first of all, verify that the whole thing is valid unicode
212+
# this should always be True, but who knows where do they get
213+
# their strings from
210214
try:
211-
name.encode('ascii')
215+
string.encode('utf8')
212216
except UnicodeEncodeError:
213-
raise ValueError('Invalid team name (non ascii): "%s".'%name)
214-
# Team name must be shorter than 25 characters
215-
if len(name) > 25:
216-
raise ValueError('Invalid team name (longer than 25): "%s".'%name)
217-
if len(name) == 0:
218-
raise ValueError('Invalid team name (too short).')
219-
# Check every character and make sure it is either
220-
# a letter or a number. Nothing else is allowed.
221-
for char in name:
222-
if (not char.isalnum()) and (char != ' '):
223-
raise ValueError('Invalid team name (only alphanumeric '
224-
'chars or blanks): "%s"'%name)
225-
if name.isspace():
226-
raise ValueError('Invalid team name (no letters): "%s"'%name)
217+
raise ValueError(f'{string} is not valid Unicode')
218+
for c in string.strip():
219+
if c.isspace():
220+
# convert newlines and other whitespace to blanks
221+
char = ' '
222+
elif int(c.isalnum()):
223+
char = c
224+
else:
225+
# ignore anything else
226+
continue
227+
sane.append(char)
228+
if len(sane) == 25:
229+
# break out of the loop when we have 25 chars
230+
break
231+
232+
name = ''.join(sane)
233+
if name == '':
234+
return '???'
235+
236+
return ''.join(sane)
227237

228238

229239
def load_team(spec):
@@ -246,7 +256,6 @@ def load_team(spec):
246256
print('ERROR: %s' % e, file=sys.stderr)
247257
raise
248258

249-
check_team_name(team.team_name)
250259
return team
251260

252261
def load_team_from_module(path: str):
@@ -308,7 +317,7 @@ def team_from_module(module):
308317
"""
309318
# look for a new-style team
310319
move = module.move
311-
name = module.TEAM_NAME
320+
name = sanitize_team_name(module.TEAM_NAME)
312321

313322
if not callable(move):
314323
raise TypeError("move is not a function")

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ test = "pytest"
5959
[tool.pytest.ini_options]
6060
# addopts = --verbose
6161
python_files = ["test/test_*.py", "contrib/test_*.py"]
62+
markers = [
63+
"cleanup_test_modules: Ensure the given modules are cleaned up after a test",
64+
]
6265

6366
[tool.coverage.run]
6467
relative_files = true

test/fixtures/player_bad_team_name.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

test/test_network.py

Lines changed: 148 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -138,138 +138,181 @@ def stopping(bot, state):
138138
assert res[0] == "success"
139139

140140

141-
@pytest.mark.parametrize("checkpoint", range(12))
142-
def test_client_broken(zmq_context, checkpoint):
143-
# This test runs a test game against a (malicious) server client
144-
# (a malicious subprocess client is harder to test)
145-
# Depending on the checkpoint selected, the broken test client will
146-
# run up to a particular point and then send a malicious message.
141+
def dealer_good(q, *, num_requests, timeout):
142+
zmq_context = zmq.Context()
143+
sock = zmq_context.socket(zmq.DEALER)
144+
poll = zmq.Poller()
147145

148-
# Depending on whether this message occurs in the game setup stage
149-
# or during the game run, this will either set the phase to FAILURE or
150-
# let the good team win. Pelita itself should not break in the process.
146+
port = sock.bind_to_random_port('tcp://127.0.0.1')
147+
q.put(port)
148+
149+
poll.register(sock, zmq.POLLIN)
150+
_available_socks = poll.poll(timeout=timeout)
151+
request = sock.recv_json()
152+
assert request['REQUEST']
153+
sock.send_json({'__status__': 'ok', '__data__': {'team_name': 'good player'}})
154+
155+
_available_socks = poll.poll(timeout=timeout)
156+
set_initial = sock.recv_json(flags=zmq.NOBLOCK)
157+
if set_initial['__action__'] == 'exit':
158+
return
159+
assert set_initial['__action__'] == "set_initial"
160+
sock.send_json({'__uuid__': set_initial['__uuid__'], '__return__': None})
161+
162+
for _i in range(num_requests):
163+
_available_socks = poll.poll(timeout=timeout)
164+
game_state = sock.recv_json(flags=zmq.NOBLOCK)
165+
msg_id = game_state['__uuid__']
151166

152-
timeout = 3000
167+
action = game_state['__action__']
168+
if action == 'exit':
169+
return
170+
assert set_initial['__action__'] == "set_initial"
153171

154-
q1 = queue.Queue()
155-
q2 = queue.Queue()
172+
current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']]
173+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': current_pos}})
156174

157-
def dealer_good(q):
158-
zmq_context = zmq.Context()
159-
sock = zmq_context.socket(zmq.DEALER)
160-
poll = zmq.Poller()
175+
_available_socks = poll.poll(timeout=timeout)
176+
exit_state = sock.recv_json(flags=zmq.NOBLOCK)
161177

162-
port = sock.bind_to_random_port('tcp://127.0.0.1')
163-
q.put(port)
178+
assert exit_state['__action__'] == 'exit'
164179

165-
poll.register(sock, zmq.POLLIN)
166-
_available_socks = poll.poll(timeout=timeout)
167-
request = sock.recv_json()
168-
assert request['REQUEST']
169-
sock.send_json({'__status__': 'ok', '__data__': {'team_name': 'good player'}})
180+
def dealer_bad(q, *, team_name=None, num_requests, checkpoint, timeout):
181+
zmq_context = zmq.Context()
182+
sock = zmq_context.socket(zmq.DEALER)
183+
poll = zmq.Poller()
170184

185+
port = sock.bind_to_random_port('tcp://127.0.0.1')
186+
q.put(port)
187+
188+
poll.register(sock, zmq.POLLIN)
189+
# we set our recv to raise, if there is no message (zmq.NOBLOCK),
190+
# so we do not need to care to check whether something is in the _available_socks
191+
_available_socks = poll.poll(timeout=timeout)
192+
193+
request = sock.recv_json(flags=zmq.NOBLOCK)
194+
assert request['REQUEST']
195+
if checkpoint == 1:
196+
sock.send_string("")
197+
return
198+
elif checkpoint == 2:
199+
sock.send_json({'__status__': 'ok'})
200+
return
201+
else:
202+
if team_name is None:
203+
team_name = f'bad <{checkpoint}>'
204+
sock.send_json({'__status__': 'ok', '__data__': {'team_name': team_name}})
205+
206+
_available_socks = poll.poll(timeout=timeout)
207+
208+
set_initial = sock.recv_json(flags=zmq.NOBLOCK)
209+
210+
if checkpoint == 3:
211+
sock.send_string("")
212+
return
213+
elif checkpoint == 4:
214+
sock.send_json({'__uuid__': 'ok'})
215+
return
216+
else:
217+
sock.send_json({'__uuid__': set_initial['__uuid__'], '__data__': None})
218+
219+
for _i in range(num_requests):
171220
_available_socks = poll.poll(timeout=timeout)
172-
set_initial = sock.recv_json(flags=zmq.NOBLOCK)
173-
if set_initial['__action__'] == 'exit':
221+
game_state = sock.recv_json(flags=zmq.NOBLOCK)
222+
msg_id = game_state['__uuid__']
223+
224+
action = game_state['__action__']
225+
if action == 'exit':
174226
return
175-
assert set_initial['__action__'] == "set_initial"
176-
sock.send_json({'__uuid__': set_initial['__uuid__'], '__return__': None})
177227

178-
for _i in range(8):
179-
_available_socks = poll.poll(timeout=timeout)
180-
game_state = sock.recv_json(flags=zmq.NOBLOCK)
228+
current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']]
229+
if checkpoint == 5:
230+
sock.send_string("No json")
231+
return
232+
elif checkpoint == 6:
233+
# This is an acceptable message that will never match a request
234+
# We can send the correct message afterwards and the match continues
235+
sock.send_json({'__uuid__': "Bad", '__return__': "Nothing"})
236+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': current_pos}})
237+
elif checkpoint == 7:
238+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': [0, 0]}})
239+
return
240+
elif checkpoint == 8:
241+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': "NOTHING"}})
242+
return
243+
elif checkpoint == 9:
244+
# cannot become a tuple
245+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': 12345}})
246+
return
247+
elif checkpoint == 10:
248+
sock.send_json({'__uuid__': msg_id, '__return__': "NOT A DICT"})
249+
return
250+
else:
251+
sock.send_json({'__uuid__': msg_id, '__return__': {'move': current_pos}})
181252

182-
action = game_state['__action__']
183-
if action == 'exit':
184-
return
185-
assert set_initial['__action__'] == "set_initial"
186253

187-
current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']]
188-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': current_pos}})
254+
def test_bad_team_name_is_currently_not_tested_in_backend(zmq_context):
255+
timeout = 3000
189256

190-
_available_socks = poll.poll(timeout=timeout)
191-
exit_state = sock.recv_json(flags=zmq.NOBLOCK)
257+
q1 = queue.Queue()
258+
q2 = queue.Queue()
192259

193-
assert exit_state['__action__'] == 'exit'
260+
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
261+
players = []
262+
players.append(executor.submit(dealer_good, q1, num_requests=8, timeout=timeout))
263+
players.append(executor.submit(dealer_bad, q2, team_name="Long bad team name 123456789 123456789!!", checkpoint=0, num_requests=8, timeout=timeout))
194264

195-
def dealer_bad(q):
196-
zmq_context = zmq.Context()
197-
sock = zmq_context.socket(zmq.DEALER)
198-
poll = zmq.Poller()
265+
port1 = q1.get()
266+
port2 = q2.get()
199267

200-
port = sock.bind_to_random_port('tcp://127.0.0.1')
201-
q.put(port)
268+
layout = {'walls': ((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 0), (1, 5), (2, 0), (2, 5), (3, 0), (3, 2), (3, 3), (3, 5), (4, 0), (4, 2), (4, 3), (4, 5), (5, 0), (5, 5), (6, 0), (6, 5), (7, 0), (7, 1), (7, 2), (7, 3), (7, 4), (7, 5)), 'food': [(1, 1), (1, 2), (2, 1), (2, 2), (5, 3), (5, 4), (6, 3), (6, 4)], 'bots': [(1, 3), (6, 2), (1, 4), (6, 1)], 'shape': (8, 6)}
202269

203-
poll.register(sock, zmq.POLLIN)
204-
# we set our recv to raise, if there is no message (zmq.NOBLOCK),
205-
# so we do not need to care to check whether something is in the _available_socks
206-
_available_socks = poll.poll(timeout=timeout)
270+
game_state = setup_game([
271+
f'pelita://127.0.0.1:{port1}/PLAYER1',
272+
f'pelita://127.0.0.1:{port2}/PLAYER2'
273+
],
274+
layout_dict=layout,
275+
max_rounds=2,
276+
timeout_length=1,
277+
)
207278

208-
request = sock.recv_json(flags=zmq.NOBLOCK)
209-
assert request['REQUEST']
210-
if checkpoint == 1:
211-
sock.send_string("")
212-
return
213-
elif checkpoint == 2:
214-
sock.send_json({'__status__': 'ok'})
215-
return
216-
else:
217-
sock.send_json({'__status__': 'ok', '__data__': {'team_name': f'bad <{checkpoint}>'}})
279+
# check that the game_state ends in the expected phase
280+
# assert game_state['game_phase'] == 'FAILURE'
218281

219-
_available_socks = poll.poll(timeout=timeout)
282+
while game_state['game_phase'] == 'RUNNING':
283+
game_state = play_turn(game_state)
220284

221-
set_initial = sock.recv_json(flags=zmq.NOBLOCK)
285+
assert game_state['team_names'] == ['good player', 'Long bad team name 123456789 123456789!!']
222286

223-
if checkpoint == 3:
224-
sock.send_string("")
225-
return
226-
elif checkpoint == 4:
227-
sock.send_json({'__uuid__': 'ok'})
228-
return
229-
else:
230-
sock.send_json({'__uuid__': set_initial['__uuid__'], '__data__': None})
231-
232-
for _i in range(8):
233-
_available_socks = poll.poll(timeout=timeout)
234-
game_state = sock.recv_json(flags=zmq.NOBLOCK)
235-
236-
action = game_state['__action__']
237-
if action == 'exit':
238-
return
239-
240-
current_pos = game_state['__data__']['game_state']['team']['bot_positions'][game_state['__data__']['game_state']['bot_turn']]
241-
if checkpoint == 5:
242-
sock.send_string("No json")
243-
return
244-
elif checkpoint == 6:
245-
# This is an acceptable message that will never match a request
246-
# We can send the correct message afterwards and the match continues
247-
sock.send_json({'__uuid__': "Bad", '__return__': "Nothing"})
248-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': current_pos}})
249-
elif checkpoint == 7:
250-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': [0, 0]}})
251-
return
252-
elif checkpoint == 8:
253-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': "NOTHING"}})
254-
return
255-
elif checkpoint == 9:
256-
# cannot become a tuple
257-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': 12345}})
258-
return
259-
elif checkpoint == 10:
260-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': "NOT A DICT"})
261-
return
262-
else:
263-
sock.send_json({'__uuid__': game_state['__uuid__'], '__return__': {'move': current_pos}})
287+
# check that no player had an uncaught exception
288+
for player in concurrent.futures.as_completed(players):
289+
assert player.exception() is None, traceback.print_exception(player.exception(), limit=None, file=None, chain=True)
290+
291+
292+
@pytest.mark.parametrize("checkpoint", range(12))
293+
def test_client_broken(zmq_context, checkpoint):
294+
# This test runs a test game against a (malicious) server client
295+
# (a malicious subprocess client is harder to test)
296+
# Depending on the checkpoint selected, the broken test client will
297+
# run up to a particular point and then send a malicious message.
298+
299+
# Depending on whether this message occurs in the game setup stage
300+
# or during the game run, this will either set the phase to FAILURE or
301+
# let the good team win. Pelita itself should not break in the process.
302+
303+
timeout = 3000
304+
305+
q1 = queue.Queue()
306+
q2 = queue.Queue()
264307

265308
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
266309
players = []
267-
players.append(executor.submit(dealer_good, q1))
310+
players.append(executor.submit(dealer_good, q1, num_requests=8, timeout=timeout))
268311

269312
if checkpoint == 0:
270-
players.append(executor.submit(dealer_good, q2))
313+
players.append(executor.submit(dealer_good, q2, num_requests=8, timeout=timeout))
271314
else:
272-
players.append(executor.submit(dealer_bad, q2))
315+
players.append(executor.submit(dealer_bad, q2, checkpoint=checkpoint, num_requests=8, timeout=timeout))
273316

274317
port1 = q1.get()
275318
port2 = q2.get()

0 commit comments

Comments
 (0)