Skip to content

Commit

Permalink
Merge pull request #3 from trickeydan/better-runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
trickeydan committed Mar 1, 2024
2 parents 912f922 + df43802 commit 4cd92a6
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 120 deletions.
7 changes: 3 additions & 4 deletions ctff/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"""
CTFFramework.
"""CTF Framework."""

Autogenerated by cookiecutter-awesome-poetry.
"""
from flask import request

from .challenge import Challenge
from .challenge_group import ChallengeGroup
Expand All @@ -13,6 +11,7 @@
"Challenge",
"ChallengeGroup",
"CTFF",
"request",
]

__version__ = "0.3.3"
53 changes: 37 additions & 16 deletions ctff/challenge.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,57 @@
"""The base challenge class."""
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, cast
from abc import ABCMeta
from typing import TYPE_CHECKING, TypeVar

from slugify import slugify

from ctff.challenge_view import ChallengeView
from ctff.part import Part

if TYPE_CHECKING:
from ctff.challenge_group import ChallengeGroup # noqa: F401

ChallengeViewT = TypeVar("ChallengeViewT", bound=ChallengeView)


class Challenge(metaclass=ABCMeta):
"""A challenge presents a problem to the competitor."""

group: ChallengeGroup | None = None
title = "Challenge"
flag = "DEFAULT_FLAG"
parts: list[Part] = []
success_message: str = "You completed the challenge."
failure_message: str = "Incorrect."
flag: str = "DEFAULT_FLAG"

@property
@abstractmethod
def title(self) -> str:
"""The title of the challenge."""
raise NotImplementedError

@classmethod
def get_url_slug(cls) -> str:

success_message = "You completed the challenge."
failure_message = "Incorrect."

def __init__(self, *, group: ChallengeGroup) -> None:
self.group = group

def get_failure_message(self) -> str:
return self.failure_message

def get_flag(self) -> str:
return self.flag

def get_success_message(self) -> str:
return self.success_message

def get_parts(self) -> list[Part]:
return self.parts

def get_title(self) -> str:
return self.title

def get_url_slug(self) -> str:
"""The URL slug."""
return slugify(cast(str, cls.title))
return slugify(self.get_title())

def get_view(self) -> type[ChallengeViewT]:
class SpecificChallengeView(ChallengeView):
challenge = self

return SpecificChallengeView # type: ignore[return-value]

def verify_submission(self) -> bool:
"""Verify a submission."""
Expand Down
62 changes: 35 additions & 27 deletions ctff/challenge_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

from collections.abc import Collection
from typing import Iterator, TypeVar
from warnings import warn

import mistune
from flask import current_app, render_template
from slugify import slugify

from .challenge import Challenge
from .challenge_view import ChallengeView
from .challenge import Challenge, ChallengeView
from .part import HTMLPart, MarkdownPart, Part

ChallengeT = TypeVar("ChallengeT", bound=Challenge)
ChallengeViewT = TypeVar("ChallengeViewT", bound=ChallengeView)


Expand All @@ -22,36 +21,41 @@ def __init__(
self,
name: str,
*,
parts: list[Part] | None = None,
introduction_md: str | None = None,
introduction_html: str = "",
introduction_html: str | None = None,
) -> None:
self.name = name
self.parts = parts or []
self._challenges: list[Challenge] = []

self._introduction_md = introduction_md
self._introduction_html = introduction_html

self._challenges: list[type[ChallengeT]] = [] # type: ignore
# Warn about deprecated args
if introduction_md is not None:
warn(
"introduction_md is deprecated and will be removed in ctff v0.5.0",
DeprecationWarning,
stacklevel=2,
)

if introduction_html is not None:
warn(
"introduction_html is deprecated and will be removed in ctff v0.5.0",
DeprecationWarning,
stacklevel=2,
)

@property
def url_slug(self) -> str:
"""Get a url slug."""
return slugify(self.name)

@property
def introduction_html(self) -> str:
"""
The HTML for the Challenge Group introduction.
If introduction_md is set, this will be rendered from it.
"""
if self._introduction_md is None:
return self._introduction_html
else:
return mistune.markdown(self._introduction_md)

def __len__(self) -> int:
return len(self._challenges)

def __iter__(self) -> Iterator[type[ChallengeT]]:
def __iter__(self) -> Iterator[Challenge]:
return iter(self._challenges)

def __contains__(self, __x: object) -> bool:
Expand All @@ -65,9 +69,9 @@ def index_view(self) -> str:
ctff=current_app,
)

def add_challenge(self, challenge: type[Challenge]) -> None:
def add_challenge(self, challenge_type: type[Challenge]) -> None:
"""Add a challenge."""
challenge.group = self
challenge = challenge_type(group=self)
self._challenges.append(challenge)

def challenge(self, cls: type[Challenge]) -> type[Challenge]:
Expand All @@ -77,12 +81,16 @@ def challenge(self, cls: type[Challenge]) -> type[Challenge]:

def get_challenge_views(self) -> list[type[ChallengeViewT]]:
"""Get the challenge views that we need to add."""
views: list[type[ChallengeViewT]] = []
return [challenge.get_view() for challenge in self._challenges]

def get_parts(self) -> list[Part]:
parts = self.parts

for chal in self._challenges:
# Add deprecated parts
if self._introduction_md is not None:
parts.append(MarkdownPart(self._introduction_md))

class SpecificChallengeView(ChallengeView):
challenge = chal
if self._introduction_html is not None:
parts.append(HTMLPart(self._introduction_html))

views.append(SpecificChallengeView) # type: ignore
return views
return parts
32 changes: 13 additions & 19 deletions ctff/challenge_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,42 @@
from __future__ import annotations

from logging import Logger
from typing import TypeVar
from typing import TYPE_CHECKING

from flask import current_app, flash
from flask.templating import render_template
from flask.views import MethodView

from ctff.challenge import Challenge
if TYPE_CHECKING:
from ctff.challenge import Challenge

LOGGER = Logger(__name__)

ChallengeT = TypeVar("ChallengeT", bound=Challenge)


class ChallengeView(MethodView):
"""Renders and processes a challenge."""

challenge: type[Challenge]
challenge: Challenge

@classmethod
def get_template_name(cls) -> str:
def get_template_name(self) -> str:
"""Get the name of the Jinja template."""
return "challenge.html"

def get(self) -> str:
"""Render and return a request."""
return render_template(
self.get_template_name(),
challenge=self.challenge(),
challenge_group=self.challenge.group,
challenge=self.challenge,
ctff=current_app,
)

def post(self) -> str:
"""Verify a submission."""
challenge = self.challenge()
if challenge.verify_submission():
flash(challenge.success_message, "success")
flash(challenge.flag, "flag")
LOGGER.error(f"{challenge.title} has been solved.")
else:
flash(challenge.failure_message, "danger")
if self.challenge.group is None:
raise RuntimeError
if self.challenge.verify_submission():
flash(self.challenge.get_success_message(), "success")
flash(self.challenge.get_flag(), "flag")
LOGGER.error(f"{self.challenge.get_title()} has been solved.")
else:
return self.get()
flash(self.challenge.get_failure_message(), "danger")

return self.get()
65 changes: 38 additions & 27 deletions ctff/ctff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from __future__ import annotations

from pathlib import Path
from warnings import warn

import mistune
from flask import Flask, current_app, render_template

from ctff.part import HTMLPart, MarkdownPart, Part

from .challenge_group import ChallengeGroup


Expand All @@ -18,8 +20,9 @@ def __init__(
*,
title: str = "CTF",
template_folder: str | Path | None = None,
parts: list[Part] | None = None,
introduction_md: str | None = None,
introduction_html: str = "",
introduction_html: str | None = None,
) -> None:
if template_folder is None:
template_folder = Path(__file__).parent.resolve() / "templates"
Expand All @@ -30,13 +33,31 @@ def __init__(
)

self.secret_key = secret_key
self._title = title
self._introduction_md = introduction_md
self._introduction_html = introduction_html
self.title = title
self.parts = parts or []

self._challenge_groups: list[ChallengeGroup] = []

self.add_url_rule("/", view_func=self.index_view)

self._introduction_md = introduction_md
self._introduction_html = introduction_html

# Warn about deprecated args
if introduction_md is not None:
warn(
"introduction_md is deprecated and will be removed in ctff v0.5.0",
DeprecationWarning,
stacklevel=2,
)

if introduction_html is not None:
warn(
"introduction_html is deprecated and will be removed in ctff v0.5.0",
DeprecationWarning,
stacklevel=2,
)

def index_view(self) -> str:
"""Render the index view for the CTF."""
return render_template(
Expand All @@ -45,28 +66,6 @@ def index_view(self) -> str:
ctff=current_app,
)

@property
def challenge_groups(self) -> list[ChallengeGroup]:
"""The challenge groups."""
return self._challenge_groups

@property
def introduction_html(self) -> str:
"""
The HTML for the CTF introduction.
If introduction_md is set, this will be rendered from it.
"""
if self._introduction_md is None:
return self._introduction_html
else:
return mistune.markdown(self._introduction_md)

@property
def title(self) -> str:
"""The title of the CTF."""
return self._title

def register_challenge_group(self, challenge_group: ChallengeGroup) -> None:
"""Register a challenge group."""
self._challenge_groups.append(challenge_group)
Expand All @@ -85,3 +84,15 @@ def register_challenge_group(self, challenge_group: ChallengeGroup) -> None:
f"/{group_slug}/{challenge_slug}",
view_func=view.as_view(f"{group_slug}_{challenge_slug}"),
)

def get_parts(self) -> list[Part]:
parts = self.parts

# Add deprecated parts
if self._introduction_md is not None:
parts.append(MarkdownPart(self._introduction_md))

if self._introduction_html is not None:
parts.append(HTMLPart(self._introduction_html))

return parts
11 changes: 11 additions & 0 deletions ctff/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ def __init__(self, name: str) -> None:
self.name = name


class HTMLPart(Part):
"""A part that renders some HTML."""

def __init__(self, content: str) -> None:
self.content = content

def render(self) -> str:
"""Render as html."""
return self.content


class MarkdownPart(Part):
"""A part that renders some markdown."""

Expand Down
Loading

0 comments on commit 4cd92a6

Please sign in to comment.