From 008b4c29c548a0c42207bf550004510ef2ae72c2 Mon Sep 17 00:00:00 2001 From: hugsy Date: Thu, 29 Jun 2023 18:17:47 -0700 Subject: [PATCH 01/15] refreshed dashboard to display member work status --- ctfhub/decorators/user.py | 55 ------------------- ctfhub/helpers.py | 5 +- ctfhub/mixins.py | 4 ++ ctfhub/templates/ctfhub/ctfs/create.html | 25 ++++----- .../templates/ctfhub/dashboard/dashboard.html | 22 ++------ ctfhub/templatetags/ctfhub_filters.py | 4 +- ctfhub/views/challenges.py | 6 ++ ctfhub/views/ctfs.py | 11 ++-- 8 files changed, 39 insertions(+), 93 deletions(-) delete mode 100644 ctfhub/decorators/user.py diff --git a/ctfhub/decorators/user.py b/ctfhub/decorators/user.py deleted file mode 100644 index 7dc9e7a..0000000 --- a/ctfhub/decorators/user.py +++ /dev/null @@ -1,55 +0,0 @@ -from django.contrib import messages -from django.http.request import HttpRequest -from django.shortcuts import redirect -from django.urls import reverse -from django.utils.http import urlencode - - -def is_not_authenticated(view_func): - """ - Decorator to redirect back logged-in users - """ - - def wrapper_func(request: HttpRequest, *args, **kwargs): - if request.user.is_authenticated: - messages.warning(request, "You're already authenticated!") - return redirect(request.META.get("HTTP_REFERER")) - else: - return view_func(request, *args, **kwargs) - - return wrapper_func - - -def is_authenticated(view_func): - """ - Decorator to redirect back unauthenticated users - """ - - def wrapper_func(request: HttpRequest, *args, **kwargs): - if not request.user.is_authenticated: - messages.warning(request, "You must be authenticated!") - return redirect( - reverse("ctfhub:user-login") - + "?" - + urlencode({"redirect_to": request.path}) - ) - else: - return view_func(request, *args, **kwargs) - - return wrapper_func - - -def is_admin(view_func): - """ - View decorator only for admin - """ - - def wrapper_func(request: HttpRequest, *args, **kwargs): - group = None - if request.user.groups.exists(): - group = request.user.groups.all()[0].name - - if group == "admin": - return view_func(request, *args, **kwargs) - - return wrapper_func diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index 283656d..23b3779 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -38,7 +38,7 @@ ) if TYPE_CHECKING: - from ctfhub.models import ChallengeFile + from ctfhub.models import ChallengeFile, Member @lru_cache(maxsize=1) @@ -378,7 +378,7 @@ def generate_github_page_header(**kwargs) -> str: return content -def export_challenge_note(member, note_id: uuid.UUID) -> str: +def export_challenge_note(member: "Member", note_id: uuid.UUID) -> str: """Export a challenge note. `member` is required for privilege requirements Args: @@ -390,7 +390,6 @@ def export_challenge_note(member, note_id: uuid.UUID) -> str: """ result = "" url = which_hedgedoc() - print(url) with requests.Session() as session: h = session.post( f"{url}/login", diff --git a/ctfhub/mixins.py b/ctfhub/mixins.py index 1d9d9e9..a1e815d 100644 --- a/ctfhub/mixins.py +++ b/ctfhub/mixins.py @@ -1,5 +1,7 @@ from django.contrib.auth.mixins import AccessMixin +from ctfhub.models import Member + class RequireSuperPowersMixin(AccessMixin): """Verify that the current user has super powers.""" @@ -7,6 +9,7 @@ class RequireSuperPowersMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): if not request.user.member.has_superpowers: return self.handle_no_permission() + self.member: Member = request.user.member return super().dispatch(request, *args, **kwargs) @@ -16,4 +19,5 @@ class MembersOnlyMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): if request.user.member.is_guest: return self.handle_no_permission() + self.member: Member = request.user.member return super().dispatch(request, *args, **kwargs) diff --git a/ctfhub/templates/ctfhub/ctfs/create.html b/ctfhub/templates/ctfhub/ctfs/create.html index 870af32..249f07f 100644 --- a/ctfhub/templates/ctfhub/ctfs/create.html +++ b/ctfhub/templates/ctfhub/ctfs/create.html @@ -158,19 +158,18 @@
min="0" value="{{ form.weight.value }}"> - {% if form.instance.is_finished %} - -
- - - - -
- {% endif %} + + +
+ + + + +
- - - - - {% for member in members %} - {% if member.selected_ctf %} - - - + {% for challenge in member.assigned_challenges.all %} + + + + - {% endif %} + {% endfor %} {% endfor %}
NicknameWorking On
{{ member.username }} - {% if member.selected_ctf.is_public %} - {{ member.selected_ctf }} - {% else %} - (private) - {% endif %} -
{% if challenge.ctf.is_finished %}worked on{% elif challenge.ctf.is_running %}works on{% else %}will work on{% endif %}{{ challenge.ctf.name }} ⟫ {{ challenge.name }} ({{ challenge.points }} pts)
diff --git a/ctfhub/templatetags/ctfhub_filters.py b/ctfhub/templatetags/ctfhub_filters.py index 985f8f8..8da6957 100644 --- a/ctfhub/templatetags/ctfhub_filters.py +++ b/ctfhub/templatetags/ctfhub_filters.py @@ -32,7 +32,7 @@ def theme_cookie(context): @register.filter -def html_sanitize(html): +def html_sanitize(html: str) -> str: """Only authorize links (a tags) html. Escape the rest Args: @@ -52,7 +52,7 @@ def html_sanitize(html): @register.filter(is_safe=True, needs_autoescape=False) -def as_tick_or_cross(b): +def as_tick_or_cross(b: bool): if b: return mark_safe( """""" diff --git a/ctfhub/views/challenges.py b/ctfhub/views/challenges.py index 7070cad..a2cc503 100644 --- a/ctfhub/views/challenges.py +++ b/ctfhub/views/challenges.py @@ -254,6 +254,7 @@ def assign_to_current_member(request: HttpRequest, pk: str) -> HttpResponse: request, f"{member.username} removed from assigned players of {challenge.ctf.name}/{challenge.name}", ) + else: challenge.assigned_members.add(member) messages.info( @@ -261,4 +262,9 @@ def assign_to_current_member(request: HttpRequest, pk: str) -> HttpResponse: f"{member.username} added to assigned players of {challenge.ctf.name}/{challenge.name}", ) + # + # Update last modification date + # + member.save() + return redirect(reverse("ctfhub:ctfs-detail", kwargs={"pk": challenge.ctf.id})) diff --git a/ctfhub/views/ctfs.py b/ctfhub/views/ctfs.py index 962cb1b..8f4b8f3 100644 --- a/ctfhub/views/ctfs.py +++ b/ctfhub/views/ctfs.py @@ -1,3 +1,4 @@ +import requests from ctfhub.forms import CategoryCreateForm, CtfCreateUpdateForm, TagCreateForm from ctfhub.helpers import ctftime_ctfs, ctftime_get_ctf_info, ctftime_parse_date from ctfhub.mixins import MembersOnlyMixin @@ -41,7 +42,7 @@ def get_context_data(self, **kwargs): def get_queryset(self): qs = super(CtfListView, self).get_queryset() return qs.filter( - Q(visibility="public") | Q(created_by=self.request.user.member) + Q(visibility=Ctf.VisibilityType.PUBLIC) | Q(created_by=self.member) ).order_by("-start_date") @@ -68,6 +69,7 @@ class CtfCreateView( success_message = "CTF '%(name)s' created" def get(self, request, *args, **kwargs): + assert self.form_class form = self.form_class(initial=self.initial) return render(request, self.template_name, {"form": form}) @@ -117,6 +119,7 @@ def get(self, request, *args, **kwargs): except (RuntimeError, requests.exceptions.ReadTimeout) as e: messages.warning(self.request, f"CTFTime GET request failed: {str(e)}") + assert self.form_class form = self.form_class(initial=initial) return render(request, self.template_name, {"form": form}) @@ -161,13 +164,13 @@ def get_success_url(self): def form_valid(self, form): if ( "visibility" in form.changed_data - and self.request.user.member != form.instance.created_by + and self.member != form.instance.created_by ): messages.error( self.request, f"Visibility can only by updated by {form.instance.created_by}", ) - return render(self.request, self.template_name, {"form": form}) + return super().form_invalid(form) return super().form_valid(form) @@ -191,6 +194,6 @@ class CtfExportNotesView(LoginRequiredMixin, DetailView): def get(self, request, *args, **kwargs): self.ctf = self.get_object() response = HttpResponse(content_type="application/zip") - zip_filename = self.ctf.export_notes_as_zipstream(response, request.user.member) + zip_filename = self.ctf.export_notes_as_zipstream(response, self.member) response["Content-Disposition"] = f"attachment; filename={zip_filename}" return response From af7d9f3b5669353a9871f292bd24b379ce917bec Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 08:36:10 -0700 Subject: [PATCH 02/15] minor typos --- ctfhub/helpers.py | 5 +---- ctfhub/models.py | 22 ++++++++++------------ ctfhub/views/users.py | 14 +++++++------- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index 23b3779..0f8ce2c 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -81,10 +81,7 @@ def register_new_hedgedoc_user(username: str, password: str) -> bool: allow_redirects=False, ) - if res.status_code != requests.codes.found: - return False - - return True + return res.status_code == requests.codes.found def create_new_note() -> str: diff --git a/ctfhub/models.py b/ctfhub/models.py index 97e4f10..0b33003 100644 --- a/ctfhub/models.py +++ b/ctfhub/models.py @@ -20,7 +20,6 @@ from django.db.models import Count, Q, Sum from django.db.models.functions import TruncMonth from django.urls.base import reverse -from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -39,6 +38,7 @@ get_file_mime, get_named_storage, get_random_string_128, + get_random_string_64, register_new_hedgedoc_user, which_hedgedoc, ) @@ -393,6 +393,7 @@ class Member(TimeStampedModel): class StatusType(models.IntegerChoices): MEMBER = 0, _("Member") GUEST = 1, _("Guest") + INACTIVE = 2, _("Inactive") class Country(models.TextChoices): ANDORRA = "AD", _("Andorra") @@ -1379,32 +1380,29 @@ def save(self, **kwargs): # # First create/save the user, otherwise `hedgedoc_username` may not exist # + is_create = self.hedgedoc_password is None + super(Member, self).save(**kwargs) # # If this is an insert, also register the same username in hedgedoc # - is_create = bool(kwargs.get("force_insert", False)) + print(f"{is_create=}") if is_create: - hedgedoc_password = get_random_string(64) + print(self.hedgedoc_username) + hedgedoc_password = get_random_string_64() if not register_new_hedgedoc_user( self.hedgedoc_username, hedgedoc_password ): # # Register the user in hedgedoc failed, delete the user, and raise # - username = self.username - self.delete() raise ExternalError( - f"Registration of user {username} on hedgedoc failed" + f"Registration of user {self.username} on hedgedoc failed" ) - else: - # - # Save the password - # - self.hedgedoc_password = hedgedoc_password - self.save() + self.hedgedoc_password = hedgedoc_password + self.save() return @property diff --git a/ctfhub/views/users.py b/ctfhub/views/users.py index 5b7510c..0b3b179 100644 --- a/ctfhub/views/users.py +++ b/ctfhub/views/users.py @@ -12,7 +12,7 @@ from django.forms.models import BaseModelForm from django.http.request import HttpRequest from django.http.response import HttpResponseForbidden, HttpResponse -from django.shortcuts import redirect +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.views.generic import ( CreateView, @@ -68,7 +68,7 @@ def get(self, request, *args, **kwargs): not request.user.member.has_superpowers and self.object.pk != request.user.id ): - raise Http403() + return HttpResponseForbidden() return super().get(request, *args, **kwargs) def form_valid(self, form: BaseModelForm) -> HttpResponse: @@ -231,17 +231,17 @@ class MemberDeleteView( success_message = "Member successfully deleted" def post(self, request, *args, **kwargs): - member = self.get_object() + member = get_object_or_404(Member, pk=kwargs.get("pk")) if member.has_superpowers: messages.error(request, "Refusing to delete super-user") return redirect("ctfhub:home") # rotate the team api key - t = Team.objects.first() - t.api_key = get_random_string_128() - t.save() + team = Team.objects.first() + assert team + team.api_key = get_random_string_128() + team.save() - # delete the associated django user member.user.delete() # delete the member entry From 03e0da741db74a33e9ecd76abb38313ab1276829 Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 12:11:32 -0700 Subject: [PATCH 03/15] moved the hedgedoc stuff into a proper class --- ctfhub/helpers.py | 436 ++++++++++++++++-- ctfhub/migrations/0018_alter_member_status.py | 19 + ctfhub/models.py | 2 - ctfhub/tests/test_helpers.py | 77 ++++ ctfhub/tests/test_urls.py | 88 +++- ctfhub/tests/utils.py | 19 + 6 files changed, 590 insertions(+), 51 deletions(-) create mode 100644 ctfhub/migrations/0018_alter_member_status.py diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index 0f8ce2c..bc8fbd5 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -6,7 +6,7 @@ import uuid from datetime import datetime from functools import lru_cache -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, Optional, Union import django.core.mail import django.utils.crypto @@ -14,37 +14,373 @@ import requests from django.conf import settings from django.core.files.storage import get_storage_class +from django.core.validators import URLValidator +from django.core.exceptions import ValidationError + +import ctfhub.models -from ctfhub_project.settings import ( - CTFHUB_ACCEPTED_IMAGE_EXTENSIONS, - CTFHUB_DEFAULT_CTF_LOGO, - CTFHUB_DOMAIN, - CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, - CTFHUB_PORT, - CTFHUB_USE_SSL, - CTFTIME_API_EVENTS_URL, - CTFTIME_USER_AGENT, - DISCORD_WEBHOOK_URL, - EMAIL_HOST, - EMAIL_HOST_PASSWORD, - EMAIL_HOST_USER, - EXCALIDRAW_ROOM_ID_CHARSET, - EXCALIDRAW_ROOM_ID_LENGTH, - EXCALIDRAW_ROOM_KEY_CHARSET, - EXCALIDRAW_ROOM_KEY_LENGTH, - HEDGEDOC_URL, - IMAGE_URL, - USE_INTERNAL_HEDGEDOC, -) if TYPE_CHECKING: - from ctfhub.models import ChallengeFile, Member + from ctfhub.models import ChallengeFile, Ctf + + +class HedgeDoc: + __username: str + __password: str + __session: Optional[requests.Session] + __url: Optional[str] + + def __init__( + self, credentials: Union["ctfhub.models.Member", tuple[str, str]] + ) -> None: + if isinstance(credentials, ctfhub.models.Member): + self.__username = credentials.hedgedoc_username + if not credentials.hedgedoc_password: + raise AttributeError( + "Member is not registered on the hedgedoc instance" + ) + self.__password = credentials.hedgedoc_password + elif isinstance(credentials, tuple): + self.__username, self.__password = credentials + else: + raise TypeError("Invalid type for creentials") + + self.__session = None + self.__url = None + return + + def __del__(self) -> None: + if self.logged_in: + self.logout() + return + + @property + def logged_in(self) -> bool: + if not self.__session: + return False + + response = self.__session.get( + f"{self.url}/me", + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + if response.status_code != requests.codes.ok: + return False + + data = response.json() + print(data) + return data["status"] == "ok" + + @property + def url(self) -> str: + """Get the URL base to the hedgedoc. + + Raises: + ValidationError: if the url is invalid (bad pattern OR unreachable) + + Returns: + str: _description_ + """ + if not self.__url: + # + # lazy fetching, cache it, raises ValidationError on failure + # + if settings.USE_INTERNAL_HEDGEDOC: + self.__url = "http://hedgedoc:3000" + else: + # + # If specified an HedgeDoc URL outside of the docker-compose env, also ping it + # + url = settings.HEDGEDOC_URL.rstrip("/") + is_valid = URLValidator(schemes=["http", "https"]) + is_valid(url) + if not self.ping(url): + raise ValidationError(f"Failed to reach {url}") + self.__url = url + + return self.__url + + def ping(self, url: Optional[str] = None) -> bool: + """Sends a simple ping to the server + + Args: + url (Optional[str], optional): the url to ping, if not given default to the instance `__url` attribute + + Returns: + bool: true if the server responded correctly, false on timeout + """ + if not url: + url = self.__url + assert url + try: + requests.head( + url, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + except requests.exceptions.Timeout: + return False + except requests.exceptions.ConnectionError: + return False + return True + + def register(self) -> bool: + """Register the member in hedgedoc. If fail, the member will be seen as anonymous. Anonymous user can read but + not write to notes. + + Returns: + bool: if the register action succeeded, returns True; False in any other cases + """ + + sess = requests.Session() + + res = sess.post( + f"{self.url}/register", + data={"email": self.__username, "password": self.__password}, + allow_redirects=False, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + # + # HedgeDoc successful user registration can be fingerprint by an HTTP/302 to the root + # and a Cookie `connect.sid` + # + if res.status_code != requests.codes.found: + return False + + if "Set-Cookie" not in res.headers: + return False + + cookie = res.headers["Set-Cookie"].lower() + if not cookie.startswith("connect.sid"): + return False + + # + # The registration is ok, the session is valid + # Affect it to the instance + # + return self.login() + + def delete(self) -> bool: + """Delete the current user, invalidate the session + + Returns: + bool: true on success, false otherwise + """ + if not self.logged_in: + return False + + assert self.__session + + # + # Retrieve the delete `nonce` hidden in tag + # + response = self.__session.get( + f"{self.url}/", + allow_redirects=False, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + if response.status_code != requests.codes.ok: + return False + + text = response.text + text = text[text.find("/me/delete/") + 11 :] + text = text[: text.find('"')] + + nonce = text + + self.__session.cookies["connect.sid"] + response = self.__session.get( + f"{self.url}/me/delete/{nonce}", + allow_redirects=False, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + # + # On successful user delete, expect HTTP/302 + a new `connect.sid` cookie + # + if response.status_code != requests.codes.found: + return False + + if "set-cookie" not in response.headers: + return False + + if not response.headers["set-cookie"].lower().startswith("connect.sid"): + return False + + self.__session.close() + self.__session = None + return True + + def login(self) -> bool: + """Logs the current user in using the credentials of the instance. If the users is already logged in, just + return successfully immediately. + + Returns: + bool: true if the operation succeeded, false otherwise + """ + if self.logged_in: + return True + + assert not self.__session + sess = requests.Session() + response = sess.post( + f"{self.url}/login", + data={ + "email": self.__username, + "password": self.__password, + }, + allow_redirects=False, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + # + # On success, expect HTTP/302 + new auth cookie + # + if response.status_code != requests.codes.found: + sess.close() + return False + + if "connect.sid" not in sess.cookies: + return False + + self.__session = sess + return True + + def logout(self) -> bool: + """Logout the current user, invalidate the session + + Returns: + bool: true on success, false otherwise + """ + if not self.logged_in: + return False + + assert self.__session + + old_auth_cookie = self.__session.cookies["connect.sid"] + + response = self.__session.get( + f"{self.url}/logout", + allow_redirects=False, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + # + # Successful logout means 302 redirect + Cookie invalidation + # + if response.status_code != requests.codes.found: + return False + + new_auth_cookie = self.__session.cookies["connect.sid"] + + if old_auth_cookie == new_auth_cookie: + return False + + self.__session.close() + self.__session = None + return True + + def info(self) -> dict: + """Returns the HedgeDoc info of the current user + + Returns: + dict: _description_ + """ + if not self.logged_in: + print("logging in") + assert self.login() + assert self.logged_in + + assert self.__session + response = self.__session.get( + f"{self.url}/me", + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + + if response.status_code != requests.codes.ok: + return {} + + data = response.json() + print(data) + if data["status"] == "forbidden": + return {} + + return data + + def create_note(self) -> str: + """ "Returns a unique note ID so that the note will be automatically created when accessed for the first time + + Returns: + str: a string of the GUID for the new note + """ + return f"/{uuid.uuid4()}" + + def note_exists(self, note_id) -> str: + """ "Checks if a specific note exists from its ID. + + Args: + id (str): the identifier to check + + Returns: + bool: returns True if it exists + """ + if not self.logged_in: + self.login() + assert self.logged_in + + assert self.__session + res = self.__session.head( + f"{self.url}/{note_id}", + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + return res.status_code == requests.codes.found + + def export_note(self, note_id: str) -> str: + """Export a challenge note as string + + Args: + note_id (str): the note id to export, usually the string of a GUID + + Raises: + AttributeError: if not authenticated + KeyError: if the note_id doesn't exist + + Returns: + str: The body of the note if successful; an empty string otherwise + """ + if not self.logged_in: + if not self.login(): + raise AttributeError + + assert self.__session + response = self.__session.get( + f"{self.url}/{note_id}/download", + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + ) + if response.status_code != requests.codes.ok: + raise KeyError(f"Note {note_id} doesn't exist") + + return response.text + + def download_archive(self, ctf: "Ctf") -> bool: + """Export all notes of a given CTF as a ZIP stream + + Args: + ctf (Ctf): _description_ + + Raises: + NotImplementedError: _description_ + + Returns: + bool: _description_ + """ + raise NotImplementedError @lru_cache(maxsize=1) def get_current_site() -> str: - r = "https://" if CTFHUB_USE_SSL else "http://" - r += f"{CTFHUB_DOMAIN}:{CTFHUB_PORT}" + r = "https://" if settings.CTFHUB_USE_SSL else "http://" + r += f"{settings.CTFHUB_DOMAIN}:{settings.CTFHUB_PORT}" return r @@ -57,11 +393,13 @@ def which_hedgedoc() -> str: Returns: str: the base HedgeDoc URL """ - if USE_INTERNAL_HEDGEDOC: + if settings.USE_INTERNAL_HEDGEDOC: return "http://hedgedoc:3000" - requests.get(HEDGEDOC_URL, timeout=CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT) - return HEDGEDOC_URL + requests.get( + settings.HEDGEDOC_URL, timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT + ) + return settings.HEDGEDOC_URL def register_new_hedgedoc_user(username: str, password: str) -> bool: @@ -102,7 +440,7 @@ def check_note_id(id: str) -> bool: Returns: bool: returns True if it exists """ - res = requests.head(f"{HEDGEDOC_URL}/{id}") + res = requests.head(f"{settings.HEDGEDOC_URL}/{id}") return res.status_code == requests.codes.found @@ -168,7 +506,7 @@ def ctftime_ctfs(running=True, future=True) -> list: Returns: list: current and future CTFs """ - ctfs = ctftime_fetch_ctfs() + ctfs = settings.CTFTIME_fetch_ctfs() now = datetime.now() result = [] @@ -194,9 +532,9 @@ def ctftime_fetch_ctfs(limit=100) -> list: start = time.time() - (3600 * 24 * 60) end = time.time() + (3600 * 24 * 7 * 26) res = requests.get( - f"{CTFTIME_API_EVENTS_URL}?limit={limit}&start={start:.0f}&finish={end:.0f}", - headers={"user-agent": CTFTIME_USER_AGENT}, - timeout=CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + f"{settings.CTFTIME_API_EVENTS_URL}?limit={limit}&start={start:.0f}&finish={end:.0f}", + headers={"user-agent": settings.CTFTIME_USER_AGENT}, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, ) if res.status_code != requests.codes.ok: raise RuntimeError( @@ -223,11 +561,11 @@ def ctftime_get_ctf_info(ctftime_id: int) -> dict: Returns: dict: JSON output from CTFTime """ - url = f"{CTFTIME_API_EVENTS_URL}{ctftime_id}/" + url = f"{settings.CTFTIME_API_EVENTS_URL}{ctftime_id}/" res = requests.get( url, - headers={"user-agent": CTFTIME_USER_AGENT}, - timeout=CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, + headers={"user-agent": settings.CTFTIME_USER_AGENT}, + timeout=settings.CTFHUB_HTTP_REQUEST_DEFAULT_TIMEOUT, ) if res.status_code != requests.codes.ok: raise RuntimeError( @@ -247,13 +585,13 @@ def ctftime_get_ctf_logo_url(ctftime_id: int) -> str: Returns: str: [description] """ - default_logo = f"{IMAGE_URL}/{CTFHUB_DEFAULT_CTF_LOGO}" + default_logo = f"{settings.IMAGE_URL}/{settings.CTFHUB_DEFAULT_CTF_LOGO}" if ctftime_id != 0: try: ctf_info = ctftime_get_ctf_info(ctftime_id) logo = ctf_info.setdefault("logo", default_logo) _, ext = os.path.splitext(logo) - if ext.lower() not in CTFHUB_ACCEPTED_IMAGE_EXTENSIONS: + if ext.lower() not in settings.CTFHUB_ACCEPTED_IMAGE_EXTENSIONS: return default_logo except ValueError: logo = default_logo @@ -272,10 +610,14 @@ def send_mail(recipients: list[str], subject: str, body: str) -> bool: Returns: bool: [description] """ - if EMAIL_HOST and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD: + if ( + settings.EMAIL_HOST + and settings.EMAIL_HOST_USER + and settings.EMAIL_HOST_PASSWORD + ): try: django.core.mail.send_mail( - subject, body, EMAIL_HOST_USER, recipients, fail_silently=False + subject, body, settings.EMAIL_HOST_USER, recipients, fail_silently=False ) return True except smtplib.SMTPException: @@ -308,7 +650,8 @@ def generate_excalidraw_room_id() -> str: str: [description] """ return django.utils.crypto.get_random_string( - EXCALIDRAW_ROOM_ID_LENGTH, allowed_chars=EXCALIDRAW_ROOM_ID_CHARSET + settings.EXCALIDRAW_ROOM_ID_LENGTH, + allowed_chars=settings.EXCALIDRAW_ROOM_ID_CHARSET, ) @@ -319,7 +662,8 @@ def generate_excalidraw_room_key() -> str: str: [description] """ return django.utils.crypto.get_random_string( - EXCALIDRAW_ROOM_KEY_LENGTH, allowed_chars=EXCALIDRAW_ROOM_KEY_CHARSET + settings.EXCALIDRAW_ROOM_KEY_LENGTH, + allowed_chars=settings.EXCALIDRAW_ROOM_KEY_CHARSET, ) @@ -336,11 +680,11 @@ def discord_send_message(js: dict) -> bool: Returns: bool: True if a message was successfully sent, False in any other cases """ - if not DISCORD_WEBHOOK_URL: + if not settings.DISCORD_WEBHOOK_URL: return False try: - h = requests.post(DISCORD_WEBHOOK_URL, json=js) + h = requests.post(settings.DISCORD_WEBHOOK_URL, json=js) if h.status_code not in (200, 204): raise Exception(f"Incorrect response, got {h.status_code}") @@ -375,11 +719,11 @@ def generate_github_page_header(**kwargs) -> str: return content -def export_challenge_note(member: "Member", note_id: uuid.UUID) -> str: +def export_challenge_note(member: "ctfhub.models.Member", note_id: uuid.UUID) -> str: """Export a challenge note. `member` is required for privilege requirements Args: - member (Member): [description] + member (ctfhub.models.Member): [description] note_id (uuid.UUID): [description] Returns: diff --git a/ctfhub/migrations/0018_alter_member_status.py b/ctfhub/migrations/0018_alter_member_status.py new file mode 100644 index 0000000..85b2931 --- /dev/null +++ b/ctfhub/migrations/0018_alter_member_status.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-06-30 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ctfhub", "0017_merge_20230629_1617"), + ] + + operations = [ + migrations.AlterField( + model_name="member", + name="status", + field=models.IntegerField( + choices=[(0, "Member"), (1, "Guest"), (2, "Inactive")], default=0 + ), + ), + ] diff --git a/ctfhub/models.py b/ctfhub/models.py index 0b33003..4d76131 100644 --- a/ctfhub/models.py +++ b/ctfhub/models.py @@ -1387,9 +1387,7 @@ def save(self, **kwargs): # # If this is an insert, also register the same username in hedgedoc # - print(f"{is_create=}") if is_create: - print(self.hedgedoc_username) hedgedoc_password = get_random_string_64() if not register_new_hedgedoc_user( self.hedgedoc_username, hedgedoc_password diff --git a/ctfhub/tests/test_helpers.py b/ctfhub/tests/test_helpers.py index 89112b1..49da2a8 100644 --- a/ctfhub/tests/test_helpers.py +++ b/ctfhub/tests/test_helpers.py @@ -1,9 +1,11 @@ import datetime +from django.forms import ValidationError import pytest from django.test import TestCase import requests from ctfhub import helpers +from ctfhub.tests.utils import django_set_temporary_setting from ctfhub_project import settings @@ -58,3 +60,78 @@ def test_helpers_ctftime(self): except (RuntimeError, requests.exceptions.ReadTimeout): # CTFTime is probably down, discard test pytest.skip("CTFTime.org is not responding") + + +class TestUnauthHedgedocHelper(TestCase): + def setUp(self) -> None: + self.email: str = "testuser01@ctfhub.localdomain" + self.password: str = "testuser01" + return super().setUp() + + def tearDown(self) -> None: + return super().tearDown() + + @django_set_temporary_setting("USE_INTERNAL_HEDGEDOC", False) + @django_set_temporary_setting("HEDGEDOC_URL", "http://localhost:8000") + def test_hedgedoc_url_valid(self): + cli = helpers.HedgeDoc((self.email, self.password)) + assert cli.url + + @django_set_temporary_setting("USE_INTERNAL_HEDGEDOC", False) + @django_set_temporary_setting("HEDGEDOC_URL", "http://not_valid:1337") + def test_hedgedoc_url_invalid(self): + cli = helpers.HedgeDoc((self.email, self.password)) + with pytest.raises(ValidationError): + assert cli.url + + def test_hedgedoc_ping(self): + assert helpers.HedgeDoc((self.email, self.password)).ping( + url="https://google.com" + ) + + cli = helpers.HedgeDoc((self.email, self.password)) + assert not cli.ping(url="http://meh:1337") + + def test_hedgedoc_register(self): + valid_client = helpers.HedgeDoc((self.email, self.password)) + assert valid_client.register() + assert valid_client.logged_in + assert valid_client.delete() + + invalid_client = helpers.HedgeDoc(("bad_user_name!!", "1234")) + assert not invalid_client.register() + assert not invalid_client.logged_in + + +class TestAuthHedgedocHelper(TestCase): + def setUp(self) -> None: + self.email: str = "testuser02@ctfhub.localdomain" + self.password: str = "testuser02" + self.cli = helpers.HedgeDoc((self.email, self.password)) + assert self.cli.register() + assert self.cli.logged_in + return super().setUp() + + def tearDown(self) -> None: + assert self.cli.delete() + return super().tearDown() + + def test_hedgedoc_login_logout(self): + assert self.cli.logout() + assert not self.cli.logged_in + + for _ in range(10): + # do a bunch of login/logout to make sure the session cookies are properly rotated + assert self.cli.login() + assert self.cli.logged_in + assert self.cli.logout() + assert not self.cli.logged_in + + assert self.cli.login() # must stay because of the destructor + + def test_hedgedoc_info(self): + data = self.cli.info() + username = self.email[: self.email.find("@")] + assert data + assert data["status"] == "ok" + assert data["name"] == username diff --git a/ctfhub/tests/test_urls.py b/ctfhub/tests/test_urls.py index 39c88e2..6cb026e 100644 --- a/ctfhub/tests/test_urls.py +++ b/ctfhub/tests/test_urls.py @@ -1,3 +1,85 @@ -class TestUrlBasic: - def test_basic_url(self): - pass +from django.test import SimpleTestCase +from django.urls import resolve +from ctfhub.views import ( + index, + teams, + users, + dashboard, + search, + generate_stats, + ctfs, + challenges, + files, + categories, + tags, +) + + +class UrlsTest(SimpleTestCase): + def test_home_url_resolves(self): + url = "/" + self.assertEqual(resolve(url).func, index) + + def test_teams_register_url_resolves(self): + url = "/teams/register/" + self.assertEqual(resolve(url).func.view_class, teams.TeamCreateView) + + def test_teams_edit_url_resolves(self): + url = "/teams/edit/1" + self.assertEqual(resolve(url).func.view_class, teams.TeamUpdateView) + + # Add tests for other team URLs + + def test_users_list_url_resolves(self): + url = "/users/" + self.assertEqual(resolve(url).func.view_class, users.MemberListView) + + def test_users_add_url_resolves(self): + url = "/users/add/" + self.assertEqual(resolve(url).func.view_class, users.MemberCreateView) + + # Add tests for other user URLs + + def test_dashboard_url_resolves(self): + url = "/dashboard/" + self.assertEqual(resolve(url).func, dashboard) + + def test_search_url_resolves(self): + url = "/search/" + self.assertEqual(resolve(url).func, search) + + def test_stats_url_resolves(self): + url = "/stats/" + self.assertEqual(resolve(url).func, generate_stats) + + # Add tests for other stats URLs + + def test_ctfs_list_url_resolves(self): + url = "/ctfs/" + self.assertEqual(resolve(url).func.view_class, ctfs.CtfListView) + + # Add tests for other CTF URLs + + def test_challenges_list_url_resolves(self): + url = "/challenges/" + self.assertEqual(resolve(url).func.view_class, challenges.ChallengeListView) + + # Add tests for other challenge URLs + + def test_files_add_url_resolves(self): + url = "/challenges//files/add/" + self.assertEqual(resolve(url).func.view_class, files.ChallengeFileCreateView) + + # Add tests for other file URLs + + def test_categories_create_url_resolves(self): + url = "/categories/create/" + self.assertEqual(resolve(url).func.view_class, categories.CategoryCreateView) + + def test_tags_list_url_resolves(self): + url = "/tags/" + self.assertEqual(resolve(url).func.view_class, tags.TagListView) + + # Add tests for other tag URLs + + # Add more tests for other URLs in your `urls.py` file diff --git a/ctfhub/tests/utils.py b/ctfhub/tests/utils.py index e4a1631..39a635a 100644 --- a/ctfhub/tests/utils.py +++ b/ctfhub/tests/utils.py @@ -3,6 +3,25 @@ from django.contrib.auth.models import User from ctfhub.models import Member, Team, Ctf +from django.conf import settings +from functools import wraps + + +def django_set_temporary_setting(setting_name, temporary_value): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + original_value = getattr(settings, setting_name) + setattr(settings, setting_name, temporary_value) + try: + return func(*args, **kwargs) + finally: + setattr(settings, setting_name, original_value) + + return wrapper + + return decorator + def get_messages(response) -> list[str]: request = response.context["request"] From ab0150c7f51817555d336da3e53c03bc09a11dbb Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 12:14:24 -0700 Subject: [PATCH 04/15] `register_new_hedgedoc_user` is obsolete --- ctfhub/helpers.py | 20 -------------------- ctfhub/models.py | 8 ++++---- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index bc8fbd5..9b138e1 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -402,26 +402,6 @@ def which_hedgedoc() -> str: return settings.HEDGEDOC_URL -def register_new_hedgedoc_user(username: str, password: str) -> bool: - """Register the member in hedgedoc. If fail, the member will be - seen as anonymous. - - Args: - username (str): member HedgeDoc username - password (str): member HedgeDoc password - - Returns: - bool: if the register action succeeded, returns True; False in any other cases - """ - res = requests.post( - which_hedgedoc() + "/register", - data={"email": username, "password": password}, - allow_redirects=False, - ) - - return res.status_code == requests.codes.found - - def create_new_note() -> str: """ "Returns a unique note ID so that the note will be automatically created when accessed for the first time diff --git a/ctfhub/models.py b/ctfhub/models.py index 4d76131..e3ae400 100644 --- a/ctfhub/models.py +++ b/ctfhub/models.py @@ -25,6 +25,7 @@ from django.utils.translation import gettext_lazy as _ from model_utils import Choices, FieldTracker from model_utils.fields import MonitorField, StatusField +from ctfhub import helpers from ctfhub.exceptions import ExternalError from ctfhub.helpers import ( @@ -39,7 +40,6 @@ get_named_storage, get_random_string_128, get_random_string_64, - register_new_hedgedoc_user, which_hedgedoc, ) from ctfhub.validators import challenge_file_max_size_validator @@ -1389,9 +1389,9 @@ def save(self, **kwargs): # if is_create: hedgedoc_password = get_random_string_64() - if not register_new_hedgedoc_user( - self.hedgedoc_username, hedgedoc_password - ): + hedgedoc_cli = helpers.HedgeDoc((self.hedgedoc_username, hedgedoc_password)) + + if not hedgedoc_cli.register(): # # Register the user in hedgedoc failed, delete the user, and raise # From 0a5344aa43bf78a4a21753d3906245237f512da0 Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 12:32:57 -0700 Subject: [PATCH 05/15] make `note_id` more consistent to its name --- ctfhub/helpers.py | 8 +--- ...ter_challenge_note_id_alter_ctf_note_id.py | 37 +++++++++++++++++++ ctfhub/models.py | 13 +++---- 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 ctfhub/migrations/0019_alter_challenge_note_id_alter_ctf_note_id.py diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index 9b138e1..5e36b68 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -43,7 +43,7 @@ def __init__( elif isinstance(credentials, tuple): self.__username, self.__password = credentials else: - raise TypeError("Invalid type for creentials") + raise TypeError("Invalid type for credentials") self.__session = None self.__url = None @@ -403,11 +403,7 @@ def which_hedgedoc() -> str: def create_new_note() -> str: - """ "Returns a unique note ID so that the note will be automatically created when accessed for the first time - - Returns: - str: the string ID of the new note - """ + """OBSOLETE FUNCTION: use the HedgeDoc() class""" return f"/{uuid.uuid4()}" diff --git a/ctfhub/migrations/0019_alter_challenge_note_id_alter_ctf_note_id.py b/ctfhub/migrations/0019_alter_challenge_note_id_alter_ctf_note_id.py new file mode 100644 index 0000000..ab31dac --- /dev/null +++ b/ctfhub/migrations/0019_alter_challenge_note_id_alter_ctf_note_id.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.2 on 2023-06-30 19:19 + +from django.db import migrations, models +import uuid + + +def forwards_func(apps, schema_editor): + for model_name in "Ctf", "Challenge": + MemberModel = apps.get_model("ctfhub", model_name) + for entry in MemberModel.objects.all(): + if entry.note_id.startswith("/"): + entry.note_id = entry.note_id[1:] + entry.save() + + +def reverse_func(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + dependencies = [ + ("ctfhub", "0018_alter_member_status"), + ] + + operations = [ + migrations.RunPython(forwards_func, reverse_func), + migrations.AlterField( + model_name="challenge", + name="note_id", + field=models.UUIDField(default=uuid.uuid4), + ), + migrations.AlterField( + model_name="ctf", + name="note_id", + field=models.UUIDField(default=uuid.uuid4), + ), + ] diff --git a/ctfhub/models.py b/ctfhub/models.py index e3ae400..1d50aec 100644 --- a/ctfhub/models.py +++ b/ctfhub/models.py @@ -3,6 +3,7 @@ import tempfile import uuid import zipfile +from django.conf import settings import requests from collections import Counter, namedtuple @@ -29,7 +30,6 @@ from ctfhub.exceptions import ExternalError from ctfhub.helpers import ( - create_new_note, ctftime_ctfs, ctftime_get_ctf_logo_url, generate_excalidraw_room_id, @@ -143,7 +143,7 @@ class VisibilityType(models.TextChoices): ) weight = models.FloatField(default=1.0, blank=False, null=False) rating = models.FloatField(default=0.0, blank=False, null=False) - note_id = models.CharField(default=create_new_note, max_length=38, blank=False) + note_id = models.UUIDField(default=uuid.uuid4, editable=True) # # Typing @@ -369,8 +369,7 @@ def export_notes_as_zipstream( @property def note_url(self) -> str: - note_id = self.note_id or "/" - return f"{which_hedgedoc()}{note_id}" + return f"{settings.HEDGEDOC_URL}/{self.note_id}" def get_absolute_url(self): return reverse( @@ -1521,7 +1520,7 @@ class Challenge(TimeStampedModel): category = models.ForeignKey( ChallengeCategory, on_delete=models.DO_NOTHING, null=True ) - note_id = models.CharField(default=create_new_note, max_length=38, blank=True) + note_id = models.UUIDField(default=uuid.uuid4, editable=True) excalidraw_room_id = models.CharField( default=generate_excalidraw_room_id, validators=[ @@ -1583,8 +1582,8 @@ def is_public(self) -> bool: @property def note_url(self) -> str: - note_id = self.note_id or "/" - return f"{HEDGEDOC_URL}{note_id}" + note_id = self.note_id or "" + return f"{settings.HEDGEDOC_URL}/{note_id}" def get_excalidraw_url(self, member=None) -> str: """ From cf3c05c41dbab5e55bc91cdf638d2d7ca1c51b70 Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 12:59:51 -0700 Subject: [PATCH 06/15] typo in test_urls --- ctfhub/tests/test_urls.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/ctfhub/tests/test_urls.py b/ctfhub/tests/test_urls.py index 6cb026e..36d505f 100644 --- a/ctfhub/tests/test_urls.py +++ b/ctfhub/tests/test_urls.py @@ -28,8 +28,6 @@ def test_teams_edit_url_resolves(self): url = "/teams/edit/1" self.assertEqual(resolve(url).func.view_class, teams.TeamUpdateView) - # Add tests for other team URLs - def test_users_list_url_resolves(self): url = "/users/" self.assertEqual(resolve(url).func.view_class, users.MemberListView) @@ -38,8 +36,6 @@ def test_users_add_url_resolves(self): url = "/users/add/" self.assertEqual(resolve(url).func.view_class, users.MemberCreateView) - # Add tests for other user URLs - def test_dashboard_url_resolves(self): url = "/dashboard/" self.assertEqual(resolve(url).func, dashboard) @@ -52,26 +48,14 @@ def test_stats_url_resolves(self): url = "/stats/" self.assertEqual(resolve(url).func, generate_stats) - # Add tests for other stats URLs - def test_ctfs_list_url_resolves(self): url = "/ctfs/" self.assertEqual(resolve(url).func.view_class, ctfs.CtfListView) - # Add tests for other CTF URLs - def test_challenges_list_url_resolves(self): url = "/challenges/" self.assertEqual(resolve(url).func.view_class, challenges.ChallengeListView) - # Add tests for other challenge URLs - - def test_files_add_url_resolves(self): - url = "/challenges//files/add/" - self.assertEqual(resolve(url).func.view_class, files.ChallengeFileCreateView) - - # Add tests for other file URLs - def test_categories_create_url_resolves(self): url = "/categories/create/" self.assertEqual(resolve(url).func.view_class, categories.CategoryCreateView) @@ -79,7 +63,3 @@ def test_categories_create_url_resolves(self): def test_tags_list_url_resolves(self): url = "/tags/" self.assertEqual(resolve(url).func.view_class, tags.TagListView) - - # Add tests for other tag URLs - - # Add more tests for other URLs in your `urls.py` file From fe7b2f5fbfb83c99724fc30dbda4d8ce8110461e Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 13:26:41 -0700 Subject: [PATCH 07/15] [models] CTF export can also include binary files --- ctfhub/helpers.py | 20 +++----------- ctfhub/models.py | 66 +++++++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/ctfhub/helpers.py b/ctfhub/helpers.py index 5e36b68..fbbc916 100644 --- a/ctfhub/helpers.py +++ b/ctfhub/helpers.py @@ -4,6 +4,8 @@ import smtplib import time import uuid +import zipfile + from datetime import datetime from functools import lru_cache from typing import TYPE_CHECKING, Any, Optional, Union @@ -335,11 +337,11 @@ def note_exists(self, note_id) -> str: ) return res.status_code == requests.codes.found - def export_note(self, note_id: str) -> str: + def export_note(self, note_id: uuid.UUID) -> str: """Export a challenge note as string Args: - note_id (str): the note id to export, usually the string of a GUID + note_id (uuid.UUID): the note id to export, usually the string of a GUID Raises: AttributeError: if not authenticated @@ -362,20 +364,6 @@ def export_note(self, note_id: str) -> str: return response.text - def download_archive(self, ctf: "Ctf") -> bool: - """Export all notes of a given CTF as a ZIP stream - - Args: - ctf (Ctf): _description_ - - Raises: - NotImplementedError: _description_ - - Returns: - bool: _description_ - """ - raise NotImplementedError - @lru_cache(maxsize=1) def get_current_site() -> str: diff --git a/ctfhub/models.py b/ctfhub/models.py index 1d50aec..d75c939 100644 --- a/ctfhub/models.py +++ b/ctfhub/models.py @@ -1,5 +1,6 @@ import hashlib import os +import pathlib import tempfile import uuid import zipfile @@ -322,50 +323,53 @@ def team_timeline(self): return members def export_notes_as_zipstream( - self, stream, member: Optional["Member"] = None + self, stream: pathlib.Path, member: "Member", include_files: bool = False ) -> str: - zip_file = zipfile.ZipFile(stream, "w") + """Export the CTF as a ZIP arhchive + + Returns: + str: the file name of the archive + """ + archive = zipfile.ZipFile(stream, "w") now = datetime.now() ts = (now.year, now.month, now.day, 0, 0, 0) - session = requests.Session() + cli = helpers.HedgeDoc(member) + if not cli.login(): + raise RuntimeError(f"Failed to authenticate {member}") # - # try impersonating requesting user on HedgeDoc, this way we're sure anonymous & unauthorized users - # can't dump data + # Add the CTF notes # - if member: - session.post( - f"{which_hedgedoc()}/login", - data={ - "email": member.hedgedoc_username, - "password": member.hedgedoc_password, - }, - allow_redirects=False, - ) - - # add ctf notes - fname = slugify(f"{self.name}.md") + fname = f"{slugify(self.name)}.md" with tempfile.TemporaryFile(): - result = session.get(f"{which_hedgedoc()}{self.note_id}/download") - zip_file.writestr( - zipfile.ZipInfo(filename=fname, date_time=ts), result.text - ) + text = cli.export_note(self.note_id) + archive.writestr(zipfile.ZipInfo(filename=fname, date_time=ts), text) - # add challenge notes + # + # Add the notes of every challenge + # for challenge in self.challenges: fname = f"{slugify(self.name)}-{slugify(challenge.name)}.md" with tempfile.TemporaryFile(): - result = session.get(f"{which_hedgedoc()}{challenge.note_id}/download") - if result.status_code != requests.codes.ok: - continue - zinfo = zipfile.ZipInfo(filename=fname, date_time=ts) - zip_file.writestr(zinfo, result.text) + data = cli.export_note(challenge.note_id) + sub_stream = zipfile.ZipInfo(filename=fname, date_time=ts) + archive.writestr(sub_stream, data) - if member: - session.post(f"{which_hedgedoc()}/logout", allow_redirects=False) - - return f"{slugify(self.name)}-notes.zip" + if include_files: + # + # Add all the challenge files + # + fname = f"{slugify(self.name)}-{slugify(challenge.name)}" + for challenge_file in challenge.challengefile_set: + fname += f"-{challenge_file.name}.bin" + with tempfile.TemporaryFile(): + data = challenge_file.file.open("rb").read() + sub_stream = zipfile.ZipInfo(filename=fname, date_time=ts) + archive.writestr(sub_stream, data) + + suffix = "notes" if not include_files else "full" + return f"{slugify(self.name)}-{suffix}.zip" @property def note_url(self) -> str: From 32f57af6e4eac49ffcd0d9bda2e972c85c89acdd Mon Sep 17 00:00:00 2001 From: hugsy Date: Fri, 30 Jun 2023 14:03:44 -0700 Subject: [PATCH 08/15] [models] fixed the member deletion view to also delete on hedgedoc --- ctfhub/templates/ctfhub/stats/team.html | 12 ++++++------ ctfhub/templates/users/confirm_delete.html | 2 +- ctfhub/templates/users/detail.html | 6 +++--- ctfhub/templates/users/edit.html | 2 +- ctfhub/templates/users/list.html | 4 ++-- ctfhub/views/users.py | 17 +++++++++++------ 6 files changed, 24 insertions(+), 19 deletions(-) diff --git a/ctfhub/templates/ctfhub/stats/team.html b/ctfhub/templates/ctfhub/stats/team.html index 532e438..013e17f 100644 --- a/ctfhub/templates/ctfhub/stats/team.html +++ b/ctfhub/templates/ctfhub/stats/team.html @@ -53,7 +53,7 @@
{{team.name}}
{% if team.ctftime_id > 0 %}
  • View team on - CTFTime + CTFTime
  • {% endif %} @@ -81,12 +81,12 @@
    Members
    {% for member in members %} - + avatar - {{member.username}} + {{member.username}} {{member.country}} @@ -111,11 +111,11 @@
    Members
    {% endif %}  ● - {% if request.user.member.has_superpowers or request.user.id == member.user.id %} - + {% if request.user.member.has_superpowers or request.user.pk == member.user.pk %} + {% endif %} {% if request.user.member.has_superpowers %} - + {% endif %} {% endfor %} diff --git a/ctfhub/templates/users/confirm_delete.html b/ctfhub/templates/users/confirm_delete.html index 9b3c9d7..62d7bbd 100644 --- a/ctfhub/templates/users/confirm_delete.html +++ b/ctfhub/templates/users/confirm_delete.html @@ -16,7 +16,7 @@

    Delete team member '{{member.username}}' ?

    Confirm deletion?

    -
    + {% csrf_token %} @@ -51,18 +51,18 @@
    Latest CTFs
    diff --git a/ctfhub/templates/ctfhub/dashboard/status.html b/ctfhub/templates/ctfhub/dashboard/status.html index d8a83b7..53b3333 100644 --- a/ctfhub/templates/ctfhub/dashboard/status.html +++ b/ctfhub/templates/ctfhub/dashboard/status.html @@ -5,7 +5,7 @@
    -
    Members
    +
      Members

    {{members | length}}

    @@ -18,7 +18,7 @@

    {{members | length}}

    -
    Played CTFs
    +
      Played CTFs

    {{nb_ctf_played}}

    @@ -31,7 +31,7 @@

    {{nb_ctf_played}}

    -
    Currently running CTF(s): {{current_ctfs | length}} {% if current_ctfs|length == 0 %}😴{% endif %}
    +
      Currently running CTF(s): {{current_ctfs | length}} {% if current_ctfs|length == 0 %}😴{% endif %}
      diff --git a/ctfhub/views/ctfs.py b/ctfhub/views/ctfs.py index 8f4b8f3..13906d2 100644 --- a/ctfhub/views/ctfs.py +++ b/ctfhub/views/ctfs.py @@ -136,9 +136,11 @@ class CtfDetailView(LoginRequiredMixin, DetailView): } def get_context_data(self, **kwargs): + obj = self.get_object() + assert isinstance(obj, Ctf) ctx = super().get_context_data(**kwargs) ctx |= { - "team_timeline": self.object.team_timeline(), + "team_timeline": obj.team_timeline(), } return ctx @@ -159,7 +161,9 @@ def get_context_data(self, **kwargs): return ctx def get_success_url(self): - return reverse("ctfhub:ctfs-detail", kwargs={"pk": self.object.pk}) + obj = self.get_object() + assert isinstance(obj, Ctf) + return reverse("ctfhub:ctfs-detail", kwargs={"pk": obj.pk}) def form_valid(self, form): if ( From 077594bb8577e50454a7a216853766d931d75cc5 Mon Sep 17 00:00:00 2001 From: hugsy Date: Sun, 2 Jul 2023 16:27:13 -0700 Subject: [PATCH 15/15] added member local time info to template --- ctfhub/context_processors.py | 8 ++++++-- ctfhub/templates/ctfhub/stats/team.html | 25 +++++++++++-------------- ctfhub_project/settings.py | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ctfhub/context_processors.py b/ctfhub/context_processors.py index 54a9530..2029ebc 100644 --- a/ctfhub/context_processors.py +++ b/ctfhub/context_processors.py @@ -1,3 +1,5 @@ +import datetime +from typing import Union import django.http from django.conf import settings @@ -22,7 +24,9 @@ def add_debug_context(request: django.http.HttpRequest) -> dict[str, dict[str, s } -def add_timezone_context(request: django.http.HttpRequest) -> dict[str, str]: +def add_timezone_context( + request: django.http.HttpRequest, +) -> dict[str, Union[str, datetime.datetime]]: """Add the client timezone information to the HTTP context Args: @@ -33,6 +37,6 @@ def add_timezone_context(request: django.http.HttpRequest) -> dict[str, str]: """ try: member = Member.objects.get(user=request.user) - return {"TZ": member.timezone} + return {"TZ": member.timezone, "NOW": datetime.datetime.now()} except Exception: return {"TZ": "UTC"} diff --git a/ctfhub/templates/ctfhub/stats/team.html b/ctfhub/templates/ctfhub/stats/team.html index 013e17f..e3c4332 100644 --- a/ctfhub/templates/ctfhub/stats/team.html +++ b/ctfhub/templates/ctfhub/stats/team.html @@ -1,4 +1,5 @@ {% load static %} +{% load tz %} {% load ctfhub_filters %}
      @@ -68,11 +69,12 @@
      Members
      Name - Country Status + Country + Local Time Master Skill - Member Since - Social Media + Joined In + Links {% if request.user.member.has_superpowers %} @@ -81,23 +83,18 @@
      Members
      {% for member in members %} - + avatar - {{member.username}} - - {{member.country}} - - {% if not member.is_active %} - Inactive - {% else %} - {{member.status | title}} - {% endif %} + {{member.username}} + {{member.get_status_display }} + + {{ NOW | timezone:member.timezone | date:'Y/m/d H:i' }} {% best_category member year_pick %} - {{member.joined_time | date:'Y'}} + {{member.joined_time | date:'Y' }}  ●  {% if member.blog_url%} diff --git a/ctfhub_project/settings.py b/ctfhub_project/settings.py index bf1d11f..6bd3036 100644 --- a/ctfhub_project/settings.py +++ b/ctfhub_project/settings.py @@ -138,7 +138,7 @@ def get_boolean(key: str) -> bool: LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images)