From 6714408e6c2fad5202a7ca38825f6105116cd8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 12:47:14 +0100 Subject: [PATCH 01/34] Web: Fix string formatting of Scoreboard instances This used to cause an error in the admin interface. --- src/ctf_gameserver/web/scoring/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index 2104ffd..896bb42 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -114,7 +114,7 @@ class Meta: ordering = ('team', '-total', '-attack', '-defense') def __str__(self): - return 'Score for team {:d}'.format(self.team) + return 'Score for team {}'.format(self.team) class GameControl(models.Model): From def6fae76733747afe57a222efc5ab18257c0d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 12:55:24 +0100 Subject: [PATCH 02/34] Web: Prevent cascading deletion of Capture objects When deleting a Team, the Flags protected by it will get deleted through `models.CASCADE`. However, also deleting the associated Captures directly influences the attack score of other Teams, so we shouldn't allow that. This is merely a theoretical case, since deletion should already by prevented by `models.PROTECT` from the Scoreboard object. But for the sake of completeness, let's additionally prevent it where sensible. Improves: https://github.com/fausecteam/ctf-gameserver/issues/12 --- src/ctf_gameserver/web/scoring/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index 896bb42..86ba1a3 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -49,7 +49,7 @@ class Capture(models.Model): Database representation of a capture, i.e. the (successful) submission of a particular flag by one team. """ - flag = models.ForeignKey(Flag, on_delete=models.CASCADE) + flag = models.ForeignKey(Flag, on_delete=models.PROTECT) capturing_team = models.ForeignKey(Team, on_delete=models.CASCADE) tick = models.PositiveSmallIntegerField() timestamp = models.DateTimeField(auto_now_add=True) From 1bc148062aeeacffe16471ee34d32e114dbc7f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 12:59:51 +0100 Subject: [PATCH 03/34] Web: Prevent deletion of Teams after competition has started Theoretically, there can be cases where registration is still open (meaning Teams can be edited) after the competition has begun. This is usually not much of a problem, but deleting a Team in that situation will technically break the Materialized View for the scoreboard and semantically scoring as a whole. Closes: https://github.com/fausecteam/ctf-gameserver/issues/12 --- .../web/registration/templates/edit_team.html | 2 ++ src/ctf_gameserver/web/registration/views.py | 10 +++++++++- src/ctf_gameserver/web/scoring/decorators.py | 17 +++++++++++++++++ src/ctf_gameserver/web/scoring/models.py | 9 +++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/ctf_gameserver/web/registration/templates/edit_team.html b/src/ctf_gameserver/web/registration/templates/edit_team.html index 05a5062..6d1e479 100644 --- a/src/ctf_gameserver/web/registration/templates/edit_team.html +++ b/src/ctf_gameserver/web/registration/templates/edit_team.html @@ -7,7 +7,9 @@

{% block title %}{% trans 'Edit team' %}{% endblock %}

{% trans 'Change password' %} + {% if show_delete_button %} {% trans 'Delete team' %} + {% endif %}
diff --git a/src/ctf_gameserver/web/registration/views.py b/src/ctf_gameserver/web/registration/views.py index 7d4f223..496716e 100644 --- a/src/ctf_gameserver/web/registration/views.py +++ b/src/ctf_gameserver/web/registration/views.py @@ -12,7 +12,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required -from ctf_gameserver.web.scoring.decorators import registration_open_required +from ctf_gameserver.web.scoring.decorators import before_competition_required, registration_open_required import ctf_gameserver.web.scoring.models as scoring_models from . import forms from .models import Team @@ -89,15 +89,23 @@ def edit_team(request): user_form = forms.UserForm(prefix='user', instance=request.user) team_form = forms.TeamForm(prefix='team', instance=team) + game_control = scoring_models.GameControl.get_instance() + # Theoretically, there can be cases where registration is still open (meaning Teams can be edited) after + # the competition has begun; this is usually not much of a problem, but deleting a Team in that situation + # will break scoring + show_delete_button = not game_control.competition_started() + return render(request, 'edit_team.html', { 'team': team, 'user_form': user_form, 'team_form': team_form, + 'show_delete_button': show_delete_button, 'delete_form': None }) @login_required +@before_competition_required @registration_open_required @transaction.atomic def delete_team(request): diff --git a/src/ctf_gameserver/web/scoring/decorators.py b/src/ctf_gameserver/web/scoring/decorators.py index 9fa1728..968db18 100644 --- a/src/ctf_gameserver/web/scoring/decorators.py +++ b/src/ctf_gameserver/web/scoring/decorators.py @@ -43,6 +43,23 @@ def func(request, *args, **kwargs): return func +def before_competition_required(view): + """ + View decorator which prohibits access to the decorated view if the competition has already begun (i.e. + running or over). + """ + + @wraps(view) + def func(request, *args, **kwargs): + if GameControl.get_instance().competition_started(): + messages.error(request, _('Sorry, that is only possible before the competition.')) + return redirect(settings.HOME_URL) + + return view(request, *args, **kwargs) + + return func + + def services_public_required(resp_format): """ View decorator which prohibits access to the decorated view if information about the services is not diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index 86ba1a3..a3251fb 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -171,6 +171,15 @@ def are_services_public(self): return self.services_public <= timezone.now() + def competition_started(self): + """ + Indicates whether the competition has already begun (i.e. running or over). + """ + if self.start is None or self.end is None: + return False + + return self.start <= timezone.now() + def competition_over(self): """ Indicates whether the competition is already over. From 45fd981709c23ec242e9379c61f7746fde313bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 14:20:38 +0100 Subject: [PATCH 04/34] Web: Show team net numbers in (internal) Service History Closes: https://github.com/fausecteam/ctf-gameserver/issues/58 --- src/ctf_gameserver/web/static/service_history.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ctf_gameserver/web/static/service_history.js b/src/ctf_gameserver/web/static/service_history.js index dd93f0d..ff0d64d 100644 --- a/src/ctf_gameserver/web/static/service_history.js +++ b/src/ctf_gameserver/web/static/service_history.js @@ -63,7 +63,8 @@ function buildTable(data) { while (tableHeadRow.firstChild) { tableHeadRow.removeChild(tableHeadRow.firstChild) } - // Leave first column (team names) empty + // Leave first two columns (team numbers & names) empty + tableHeadRow.appendChild(document.createElement('th')) tableHeadRow.appendChild(document.createElement('th')) for (let i = data['min-tick']; i <= data['max-tick']; i++) { let col = document.createElement('th') @@ -84,9 +85,14 @@ function buildTable(data) { let row = document.createElement('tr') let firstCol = document.createElement('td') - firstCol.textContent = team['name'] + firstCol.classList.add('text-muted') + firstCol.textContent = team['net_number'] row.appendChild(firstCol) + let secondCol = document.createElement('td') + secondCol.textContent = team['name'] + row.appendChild(secondCol) + for (let i = 0; i < team['checks'].length; i++) { const check = team['checks'][i] const tick = data['min-tick'] + i From 22effe62fa10519a7fd88fc4e6edfc10b4d5ec27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 14:38:50 +0100 Subject: [PATCH 05/34] Web: Block changes during updates in (internal) Service History Prevent multiple ongoing updates by making all fields read-only and showing a progress spinner during updates. "progress_spinner.gif" is "Indicator Big 2" from http://www.ajaxload.info. As per the site's footer, "[g]enerated gifs can be used, modified and distributed under the terms of the Do What The Fuck You Want To Public License" (http://www.wtfpl.net). Closes: https://github.com/fausecteam/ctf-gameserver/issues/56 --- setup.py | 1 + .../scoring/templates/service_history.html | 7 +++--- .../web/static/progress_spinner.gif | Bin 0 -> 4178 bytes .../web/static/service_history.js | 20 +++++++++++++++++- src/ctf_gameserver/web/static/style.css | 8 +++++-- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/ctf_gameserver/web/static/progress_spinner.gif diff --git a/setup.py b/setup.py index 73d32ba..3a47c07 100755 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ 'templates/*.txt', 'static/robots.txt', 'static/*.css', + 'static/*.gif', 'static/*.js', 'static/ext/jquery.min.js', 'static/ext/bootstrap/css/*', diff --git a/src/ctf_gameserver/web/scoring/templates/service_history.html b/src/ctf_gameserver/web/scoring/templates/service_history.html index 40cffff..10d4089 100644 --- a/src/ctf_gameserver/web/scoring/templates/service_history.html +++ b/src/ctf_gameserver/web/scoring/templates/service_history.html @@ -16,7 +16,7 @@

{% block title %}{% trans 'Service History' %}{% endblock %}

- -
+
{% trans 'Min' %}
diff --git a/src/ctf_gameserver/web/static/progress_spinner.gif b/src/ctf_gameserver/web/static/progress_spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..c97ec6ea9739a68e25637c0aa4adaaea05e3e4ca GIT binary patch literal 4178 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 Date: Tue, 29 Dec 2020 16:45:53 +0100 Subject: [PATCH 06/34] Controller: Fix Prometheus import In more recent versions of Prometheus Python Client, the main module does not import `core` anymore. --- src/ctf_gameserver/controller/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctf_gameserver/controller/controller.py b/src/ctf_gameserver/controller/controller.py index 8408549..6e87afd 100644 --- a/src/ctf_gameserver/controller/controller.py +++ b/src/ctf_gameserver/controller/controller.py @@ -4,6 +4,7 @@ import os import prometheus_client +import prometheus_client.core import psycopg2 from psycopg2 import errorcodes as postgres_errors From 67d6b023d213f78190dab2782c992397a2cbceb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 18:30:32 +0100 Subject: [PATCH 07/34] Web: Add missing space to help text for registration confirm text --- src/ctf_gameserver/web/scoring/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctf_gameserver/web/scoring/forms.py b/src/ctf_gameserver/web/scoring/forms.py index ea1bcd5..5826c79 100644 --- a/src/ctf_gameserver/web/scoring/forms.py +++ b/src/ctf_gameserver/web/scoring/forms.py @@ -21,7 +21,7 @@ class Meta: 'services_public': _('Time at which information about the services is public, but the actual ' 'game has not started yet'), 'valid_ticks': _('Number of ticks a flag is valid for'), - 'registration_confirm_text': _('If set, teams will have to confirm to this text (e.g. a link to' + 'registration_confirm_text': _('If set, teams will have to confirm to this text (e.g. a link to ' 'T&C) when signing up. May contain HTML.'), 'min_net_number': _('If unset, team IDs will be used as net numbers'), 'max_net_number': _('(Inclusive) If unset, team IDs will be used as net numbers'), From f03ebcfb58421aeeec502722a5a47ff9443bf3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 18:50:12 +0100 Subject: [PATCH 08/34] Make flag prefix configurable Remove one of the last remaining hard-coded FAUST references. --- go/checkerlib/lib.go | 2 +- scripts/submission/ctf-submission | 6 +++--- src/ctf_gameserver/checker/database.py | 5 +++-- src/ctf_gameserver/checker/master.py | 5 +++-- src/ctf_gameserver/checkerlib/lib.py | 2 +- src/ctf_gameserver/lib/flag.py | 12 +++++------- src/ctf_gameserver/submission/flagserver.py | 5 +++-- src/ctf_gameserver/web/scoring/forms.py | 1 + src/ctf_gameserver/web/scoring/models.py | 1 + tests/checker/fixtures/integration.json | 1 + tests/checker/fixtures/master.json | 1 + tests/controller/fixtures/main_loop.json | 1 + tests/lib/test_flag.py | 8 ++++---- tests/test_submission.py | 2 +- 14 files changed, 29 insertions(+), 23 deletions(-) diff --git a/go/checkerlib/lib.go b/go/checkerlib/lib.go index f9bc18b..19a6a90 100644 --- a/go/checkerlib/lib.go +++ b/go/checkerlib/lib.go @@ -141,7 +141,7 @@ func genFlag(team, service, timestamp int, payload, secret []byte) string { mac := d.Sum(nil) b.Write(mac[:9]) - return "FAUST_" + base64.StdEncoding.EncodeToString(b.Bytes()) + return "FLAG_" + base64.StdEncoding.EncodeToString(b.Bytes()) } // StoreState allows a Checker Script to store data (serialized via diff --git a/scripts/submission/ctf-submission b/scripts/submission/ctf-submission index 6ac6cd7..614513f 100755 --- a/scripts/submission/ctf-submission +++ b/scripts/submission/ctf-submission @@ -42,9 +42,9 @@ def main(): with dbconnection: with dbconnection.cursor() as cursor: - cursor.execute('''SELECT start, "end", valid_ticks, tick_duration + cursor.execute('''SELECT start, "end", valid_ticks, tick_duration, flag_prefix FROM scoring_gamecontrol''') - conteststart, contestend, flagvalidity, tickduration = cursor.fetchone() + conteststart, contestend, flagvalidity, tickduration, flagprefix = cursor.fetchone() logging.debug("Starting asyncore") @@ -53,7 +53,7 @@ def main(): flagserver.FlagServer(family, args.listen, args.port, dbconnection, args.secret, conteststart, contestend, flagvalidity, tickduration, - team_regex) + flagprefix, team_regex) break except socket.gaierror as e: if e.errno != socket.EAI_ADDRFAMILY: diff --git a/src/ctf_gameserver/checker/database.py b/src/ctf_gameserver/checker/database.py index 252e1b7..b879b73 100644 --- a/src/ctf_gameserver/checker/database.py +++ b/src/ctf_gameserver/checker/database.py @@ -10,7 +10,7 @@ def get_control_info(db_conn, prohibit_changes=False): """ with transaction_cursor(db_conn, prohibit_changes) as cursor: - cursor.execute('SELECT start, valid_ticks, tick_duration FROM scoring_gamecontrol') + cursor.execute('SELECT start, valid_ticks, tick_duration, flag_prefix FROM scoring_gamecontrol') result = cursor.fetchone() if result is None: @@ -19,7 +19,8 @@ def get_control_info(db_conn, prohibit_changes=False): return { 'contest_start': result[0], 'valid_ticks': result[1], - 'tick_duration': result[2] + 'tick_duration': result[2], + 'flag_prefix': result[3] } diff --git a/src/ctf_gameserver/checker/master.py b/src/ctf_gameserver/checker/master.py index fa0ad5c..c3c5e19 100644 --- a/src/ctf_gameserver/checker/master.py +++ b/src/ctf_gameserver/checker/master.py @@ -246,6 +246,7 @@ def refresh_control_info(self): self.contest_start = control_info['contest_start'] self.tick_duration = datetime.timedelta(seconds=control_info['tick_duration']) self.flag_valid_ticks = control_info['valid_ticks'] + self.flag_prefix = control_info['flag_prefix'] def step(self): """ @@ -316,8 +317,8 @@ def handle_flag_request(self, task_info, params): self.refresh_control_info() expiration = self.contest_start + (self.flag_valid_ticks + tick) * self.tick_duration - return flag_lib.generate(task_info['team'], self.service['id'], self.flag_secret, payload, - expiration.timestamp()) + return flag_lib.generate(task_info['team'], self.service['id'], self.flag_secret, self.flag_prefix, + payload, expiration.timestamp()) def handle_load_request(self, task_info, param): return database.load_state(self.state_db_conn, self.service['id'], task_info['team'], param) diff --git a/src/ctf_gameserver/checkerlib/lib.py b/src/ctf_gameserver/checkerlib/lib.py index 67d4811..a22c01c 100644 --- a/src/ctf_gameserver/checkerlib/lib.py +++ b/src/ctf_gameserver/checkerlib/lib.py @@ -127,7 +127,7 @@ def get_flag(tick: int, payload: bytes = b'') -> str: # Return dummy flag when launched locally if payload == b'': payload = None - return ctf_gameserver.lib.flag.generate(team, 42, b'TOPSECRET', payload, tick) + return ctf_gameserver.lib.flag.generate(team, 42, b'TOPSECRET', payload=payload, timestamp=tick) payload_b64 = base64.b64encode(payload).decode('ascii') _send_ctrl_message({'action': 'FLAG', 'param': {'tick': tick, 'payload': payload_b64}}) diff --git a/src/ctf_gameserver/lib/flag.py b/src/ctf_gameserver/lib/flag.py index 359f564..8bdfcfe 100644 --- a/src/ctf_gameserver/lib/flag.py +++ b/src/ctf_gameserver/lib/flag.py @@ -12,13 +12,11 @@ PAYLOAD_LEN = 8 # timestamp + team + service + payload DATA_LEN = 4 + 2 + 1 + PAYLOAD_LEN -# Flag prefix -PREFIX = "FAUST" # Flag validity in seconds VALID = 900 -def generate(team_net_no, service_id, secret, payload=None, timestamp=None): +def generate(team_net_no, service_id, secret, prefix='FLAG_', payload=None, timestamp=None): """ Generates a flag for the given arguments. This is deterministic and should always return the same result for the same arguments (and the same time, if no timestamp is explicitly specified). @@ -46,10 +44,10 @@ def generate(team_net_no, service_id, secret, payload=None, timestamp=None): protected_data += payload mac = _gen_mac(secret, protected_data) - return PREFIX + '_' + base64.b64encode(protected_data + mac).decode('ascii') + return prefix + base64.b64encode(protected_data + mac).decode('ascii') -def verify(flag, secret): +def verify(flag, secret, prefix='FLAG_'): """ Verfies flag validity and returns data from the flag. Will raise an appropriate exception if verification fails. @@ -58,11 +56,11 @@ def verify(flag, secret): Data from the flag as a tuple of (team, service, payload, timestamp) """ - if not flag.startswith(PREFIX + '_'): + if not flag.startswith(prefix): raise InvalidFlagFormat() try: - raw_flag = base64.b64decode(flag.split('_')[1]) + raw_flag = base64.b64decode(flag[len(prefix):]) except binascii.Error: raise InvalidFlagFormat() diff --git a/src/ctf_gameserver/submission/flagserver.py b/src/ctf_gameserver/submission/flagserver.py index 073d624..50480e6 100644 --- a/src/ctf_gameserver/submission/flagserver.py +++ b/src/ctf_gameserver/submission/flagserver.py @@ -12,7 +12,7 @@ class FlagHandler(asynchat.async_chat): def __init__(self, sock, addr, dbconnection, secret, conteststart, contestend, flagvalidity, tickduration, - team_regex): + flagprefix, team_regex): asynchat.async_chat.__init__(self, sock=sock) ipaddr, port = addr[:2] # IPv4 returns two values, IPv6 four @@ -36,6 +36,7 @@ def __init__(self, sock, addr, dbconnection, secret, self._contestend = contestend self._flagvalidity = flagvalidity self._tickduration = tickduration + self._flagprefix = flagprefix def _reply(self, message): self._logger.debug("-> %s", message.decode('utf-8')) @@ -72,7 +73,7 @@ def _handle_flag(self): return try: - protecting_team, service, _, timestamp = flag.verify(curflag, self._secret) + protecting_team, service, _, timestamp = flag.verify(curflag, self._secret, self._flagprefix) except flag.InvalidFlagFormat: self._reply(b"Flag not recognized") return diff --git a/src/ctf_gameserver/web/scoring/forms.py b/src/ctf_gameserver/web/scoring/forms.py index 5826c79..361937e 100644 --- a/src/ctf_gameserver/web/scoring/forms.py +++ b/src/ctf_gameserver/web/scoring/forms.py @@ -21,6 +21,7 @@ class Meta: 'services_public': _('Time at which information about the services is public, but the actual ' 'game has not started yet'), 'valid_ticks': _('Number of ticks a flag is valid for'), + 'flag_prefix': _('Static text prepended to every flag'), 'registration_confirm_text': _('If set, teams will have to confirm to this text (e.g. a link to ' 'T&C) when signing up. May contain HTML.'), 'min_net_number': _('If unset, team IDs will be used as net numbers'), diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index a3251fb..aca7ac6 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -133,6 +133,7 @@ class GameControl(models.Model): # Number of ticks a flag is valid for including the one it was generated in valid_ticks = models.PositiveSmallIntegerField(default=5) current_tick = models.SmallIntegerField(default=-1) + flag_prefix = models.CharField(max_length=20, default='FLAG_') registration_open = models.BooleanField(default=True) registration_confirm_text = models.TextField(blank=True) min_net_number = models.PositiveIntegerField(null=True, blank=True) diff --git a/tests/checker/fixtures/integration.json b/tests/checker/fixtures/integration.json index af952dc..4211da8 100644 --- a/tests/checker/fixtures/integration.json +++ b/tests/checker/fixtures/integration.json @@ -84,6 +84,7 @@ "tick_duration": 180, "valid_ticks": 5, "current_tick": -1, + "flag_prefix": "FLAG_", "registration_open": false } } diff --git a/tests/checker/fixtures/master.json b/tests/checker/fixtures/master.json index 5f8a105..ebd89b5 100644 --- a/tests/checker/fixtures/master.json +++ b/tests/checker/fixtures/master.json @@ -82,6 +82,7 @@ "tick_duration": 180, "valid_ticks": 5, "current_tick": -1, + "flag_prefix": "FLAG_", "registration_open": false } } diff --git a/tests/controller/fixtures/main_loop.json b/tests/controller/fixtures/main_loop.json index 377041a..db1a715 100644 --- a/tests/controller/fixtures/main_loop.json +++ b/tests/controller/fixtures/main_loop.json @@ -111,6 +111,7 @@ "tick_duration": 180, "valid_ticks": 5, "current_tick": -1, + "flag_prefix": "FAUST_", "registration_open": false } } diff --git a/tests/lib/test_flag.py b/tests/lib/test_flag.py index 9908d14..9950f17 100644 --- a/tests/lib/test_flag.py +++ b/tests/lib/test_flag.py @@ -28,9 +28,9 @@ def test_valid_flag(self): def test_old_flag(self): timestamp = int(time.time() - 12) - test_flag = flag.generate(12, 13, b'secret', timestamp=timestamp) + test_flag = flag.generate(12, 13, b'secret', 'FLAGPREFIX-', timestamp=timestamp) with self.assertRaises(flag.FlagExpired): - flag.verify(test_flag, b'secret') + flag.verify(test_flag, b'secret', 'FLAGPREFIX-') def test_invalid_format(self): with self.assertRaises(flag.InvalidFlagFormat): @@ -93,12 +93,12 @@ def test_known_flags(self, time_mock): for secret in (b'secret1', b'secret2'): for payload in (None, b'payload1'): for timestamp in (1591000000, 1592000000): - actual_flag = flag.generate(team, service, secret, payload, timestamp) + actual_flag = flag.generate(team, service, secret, 'FAUST_', payload, timestamp) actual_flags.append(actual_flag) time_mock.return_value = timestamp - 5 actual_team, actual_service, actual_payload, actual_timestamp = \ - flag.verify(actual_flag, secret) + flag.verify(actual_flag, secret, 'FAUST_') self.assertEqual(actual_team, team) self.assertEqual(actual_service, service) if payload is not None: diff --git a/tests/test_submission.py b/tests/test_submission.py index 73e094d..dd2b25f 100644 --- a/tests/test_submission.py +++ b/tests/test_submission.py @@ -11,7 +11,7 @@ def setUp(self): self._handler = FlagHandler(None, ("203.0.113.42", 1337), None, 'c2VjcmV0', datetime.datetime.now(tz=datetime.timezone.utc), datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(minutes=10), - None, None, re.compile(r'^203\.0\.(\d+)\.\d+$')) + None, None, 'FLAG_', re.compile(r'^203\.0\.(\d+)\.\d+$')) def test_empty(self): From a121782c98effc4775615b92d705c073566750d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Drei=C3=9Fig?= Date: Tue, 29 Dec 2020 21:22:12 +0100 Subject: [PATCH 09/34] Web: Store competition name in the database --- conf/web/prod_settings.py | 3 --- src/ctf_gameserver/web/admin.py | 17 ++++++++++++---- src/ctf_gameserver/web/base_settings.py | 3 +-- src/ctf_gameserver/web/context_processors.py | 20 ++++++------------- src/ctf_gameserver/web/dev_settings.py | 2 -- .../web/flatpages/templates/flatpage.html | 2 +- src/ctf_gameserver/web/forms.py | 5 +++-- src/ctf_gameserver/web/registration/forms.py | 6 ++++-- .../web/registration/templates/register.html | 2 +- src/ctf_gameserver/web/scoring/forms.py | 1 + src/ctf_gameserver/web/scoring/models.py | 1 + .../web/templates/base-common.html | 6 +++--- tests/checker/fixtures/integration.json | 2 ++ tests/checker/fixtures/master.json | 2 ++ tests/controller/fixtures/main_loop.json | 2 ++ 15 files changed, 40 insertions(+), 34 deletions(-) diff --git a/conf/web/prod_settings.py b/conf/web/prod_settings.py index 2b6ef1b..04bccfe 100644 --- a/conf/web/prod_settings.py +++ b/conf/web/prod_settings.py @@ -8,9 +8,6 @@ from ctf_gameserver.web.base_settings import * -# The human-readable title of your CTF -COMPETITION_NAME = '' - # Content Security Policy header in the format `directive: [values]`, see e.g # http://www.html5rocks.com/en/tutorials/security/content-security-policy/ for an explanation # The initially selected directives should cover most sensitive cases, but still allow YouTube embeds, diff --git a/src/ctf_gameserver/web/admin.py b/src/ctf_gameserver/web/admin.py index fabce45..e8483ce 100644 --- a/src/ctf_gameserver/web/admin.py +++ b/src/ctf_gameserver/web/admin.py @@ -1,11 +1,12 @@ from django.contrib import admin -from django.conf import settings +from django.utils.decorators import classproperty from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin from .registration.models import Team from .registration.admin import InlineTeamAdmin +from .scoring.models import GameControl from .util import format_lazy @@ -14,11 +15,19 @@ class CTFAdminSite(admin.AdminSite): Custom variant of the AdminSite which replaces the default headers and titles. """ - site_header = format_lazy(_('{competition_name} administration'), - competition_name=settings.COMPETITION_NAME) - site_title = site_header index_title = _('Administration home') + # Declare this lazily through a classproperty in order to avoid a circular dependency when creating + # migrations + @classproperty + def site_header(cls): # pylint: disable=no-self-argument + return format_lazy(_('{competition_name} administration'), + competition_name=GameControl.get_instance().competition_name) + + @classproperty + def site_title(cls): # pylint: disable=no-self-argument + return cls.site_header + admin_site = CTFAdminSite() # pylint: disable=invalid-name diff --git a/src/ctf_gameserver/web/base_settings.py b/src/ctf_gameserver/web/base_settings.py index c2d539d..38607dd 100644 --- a/src/ctf_gameserver/web/base_settings.py +++ b/src/ctf_gameserver/web/base_settings.py @@ -54,8 +54,7 @@ 'django.template.context_processors.i18n', 'django.template.context_processors.static', 'django.template.context_processors.media', - 'ctf_gameserver.web.context_processors.competition_name', - 'ctf_gameserver.web.context_processors.competition_status', + 'ctf_gameserver.web.context_processors.game_control', 'ctf_gameserver.web.context_processors.flatpage_nav' ] } diff --git a/src/ctf_gameserver/web/context_processors.py b/src/ctf_gameserver/web/context_processors.py index 4643781..70e8bbf 100644 --- a/src/ctf_gameserver/web/context_processors.py +++ b/src/ctf_gameserver/web/context_processors.py @@ -4,25 +4,17 @@ from .flatpages import models as flatpages_models -def competition_name(_): +def game_control(_): """ - Context processor that adds the CTF's title to the context. + Context processor which adds information from the Game Control table to the context. """ - return {'COMPETITION_NAME': settings.COMPETITION_NAME} - - -def competition_status(_): - """ - Context processor which adds information about the competition's status (whether it is running or over - and whether registration is open) to the context. - """ - - game_control = scoring_models.GameControl.get_instance() + control_instance = scoring_models.GameControl.get_instance() return { - 'registration_open': game_control.registration_open, - 'services_public': game_control.are_services_public() + 'competition_name': control_instance.competition_name, + 'registration_open': control_instance.registration_open, + 'services_public': control_instance.are_services_public() } diff --git a/src/ctf_gameserver/web/dev_settings.py b/src/ctf_gameserver/web/dev_settings.py index c4ab780..f2b3d0d 100644 --- a/src/ctf_gameserver/web/dev_settings.py +++ b/src/ctf_gameserver/web/dev_settings.py @@ -8,8 +8,6 @@ from .base_settings import * -COMPETITION_NAME = 'Development CTF' - CSP_POLICIES = { # The debug error page uses inline JavaScript and CSS 'script-src': ["'self'", "'unsafe-inline'"], diff --git a/src/ctf_gameserver/web/flatpages/templates/flatpage.html b/src/ctf_gameserver/web/flatpages/templates/flatpage.html index c306d16..d414330 100644 --- a/src/ctf_gameserver/web/flatpages/templates/flatpage.html +++ b/src/ctf_gameserver/web/flatpages/templates/flatpage.html @@ -7,7 +7,7 @@

{% block title %} {% if page.is_home_page %} - {{ COMPETITION_NAME }} + {{ competition_name }} {% else %} {{ page.title }} {% endif %} diff --git a/src/ctf_gameserver/web/forms.py b/src/ctf_gameserver/web/forms.py index c9b15af..b32f2f4 100644 --- a/src/ctf_gameserver/web/forms.py +++ b/src/ctf_gameserver/web/forms.py @@ -1,8 +1,9 @@ from django import forms -from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm +from .scoring.models import GameControl + class TeamAuthenticationForm(AuthenticationForm): """ @@ -23,7 +24,7 @@ class FormalPasswordResetForm(PasswordResetForm): def send_mail(self, subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None): - context['competition_name'] = settings.COMPETITION_NAME + context['competition_name'] = GameControl.get_instance().competition_name return super().send_mail(subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name) diff --git a/src/ctf_gameserver/web/registration/forms.py b/src/ctf_gameserver/web/registration/forms.py index 2ec6dc0..bc6d693 100644 --- a/src/ctf_gameserver/web/registration/forms.py +++ b/src/ctf_gameserver/web/registration/forms.py @@ -93,8 +93,10 @@ def send_confirmation_mail(self, request): Args: request: The HttpRequest from which this function is being called """ + competition_name = scoring_models.GameControl.get_instance().competition_name + context = { - 'competition_name': settings.COMPETITION_NAME, + 'competition_name': competition_name, 'protocol': 'https' if request.is_secure() else 'http', 'domain': get_current_site(request), 'user': self.instance.pk, @@ -102,7 +104,7 @@ def send_confirmation_mail(self, request): } message = loader.render_to_string('confirmation_mail.txt', context) - send_mail(settings.COMPETITION_NAME+' email confirmation', message, settings.DEFAULT_FROM_EMAIL, + send_mail(competition_name+' email confirmation', message, settings.DEFAULT_FROM_EMAIL, [self.instance.email]) diff --git a/src/ctf_gameserver/web/registration/templates/register.html b/src/ctf_gameserver/web/registration/templates/register.html index faa3530..61035bd 100644 --- a/src/ctf_gameserver/web/registration/templates/register.html +++ b/src/ctf_gameserver/web/registration/templates/register.html @@ -9,7 +9,7 @@

{% block title %}{% trans 'Registration' %}{% endblock %}

{% blocktrans %} - Want to register a team for {{ COMPETITION_NAME }}? There you go: + Want to register a team for {{ competition_name }}? There you go: {% endblocktrans %}

diff --git a/src/ctf_gameserver/web/scoring/forms.py b/src/ctf_gameserver/web/scoring/forms.py index 361937e..6e2850e 100644 --- a/src/ctf_gameserver/web/scoring/forms.py +++ b/src/ctf_gameserver/web/scoring/forms.py @@ -18,6 +18,7 @@ class Meta: model = models.GameControl exclude = ('current_tick',) help_texts = { + 'competition_name': _('Human-readable title of the CTF'), 'services_public': _('Time at which information about the services is public, but the actual ' 'game has not started yet'), 'valid_ticks': _('Number of ticks a flag is valid for'), diff --git a/src/ctf_gameserver/web/scoring/models.py b/src/ctf_gameserver/web/scoring/models.py index aca7ac6..3eb1877 100644 --- a/src/ctf_gameserver/web/scoring/models.py +++ b/src/ctf_gameserver/web/scoring/models.py @@ -122,6 +122,7 @@ class GameControl(models.Model): Single-row database table to store control information for the competition. """ + competition_name = models.CharField(max_length=100, default='My A/D CTF') # Start and end times for the whole competition: Make them NULL-able (for the initial state), but not # blank-able (have to be set upon editing); "services_public" is the point at which information about the # services is public, but the actual game has not started yet diff --git a/src/ctf_gameserver/web/templates/base-common.html b/src/ctf_gameserver/web/templates/base-common.html index 793c785..6af2db9 100644 --- a/src/ctf_gameserver/web/templates/base-common.html +++ b/src/ctf_gameserver/web/templates/base-common.html @@ -19,10 +19,10 @@ - {% block title %}{% endblock %} | {{ COMPETITION_NAME }} + {% block title %}{% endblock %} | {{ competition_name }} - + @@ -37,7 +37,7 @@ - {{ COMPETITION_NAME }} + {{ competition_name }}