Bump Dependencies (#2332)

* Bump dependencies
* Closes #2300 
* Closes #2331
This commit is contained in:
Kevin Chung
2023-07-02 17:33:58 -04:00
committed by GitHub
parent 7b68babee6
commit deae9e1941
43 changed files with 205 additions and 166 deletions

View File

@@ -23,7 +23,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install -r development.txt python -m pip install -r linting.txt
sudo yarn install --non-interactive sudo yarn install --non-interactive
sudo yarn global add prettier@1.17.0 sudo yarn global add prettier@1.17.0

View File

@@ -12,7 +12,6 @@ from flask_migrate import upgrade
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment from jinja2.sandbox import SandboxedEnvironment
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.utils import cached_property
import CTFd.utils.config import CTFd.utils.config
from CTFd import utils from CTFd import utils
@@ -36,16 +35,14 @@ __channel__ = "oss"
class CTFdRequest(Request): class CTFdRequest(Request):
@cached_property def __init__(self, *args, **kwargs):
def path(self): super().__init__(*args, **kwargs)
""" """
Hijack the original Flask request path because it does not account for subdirectory deployments in an intuitive Hijack the original Flask request path because it does not account for subdirectory deployments in an intuitive
manner. We append script_root so that the path always points to the full path as seen in the browser. manner. We append script_root so that the path always points to the full path as seen in the browser.
e.g. /subdirectory/path/route vs /path/route e.g. /subdirectory/path/route vs /path/route
:return: string
""" """
return self.script_root + super(CTFdRequest, self).path self.path = self.script_root + self.path
class CTFdFlask(Flask): class CTFdFlask(Flask):
@@ -86,24 +83,30 @@ class SandboxedBaseEnvironment(SandboxedEnvironment):
theme = str(utils.get_config("ctf_theme")) theme = str(utils.get_config("ctf_theme"))
cache_name = theme + "/" + name cache_name = theme + "/" + name
# Rest of this code is copied from Jinja # Rest of this code roughly copied from Jinja
# https://github.com/pallets/jinja/blob/master/src/jinja2/environment.py#L802-L815 # https://github.com/pallets/jinja/blob/b08cd4bc64bb980df86ed2876978ae5735572280/src/jinja2/environment.py#L956-L973
cache_key = (weakref.ref(self.loader), cache_name) cache_key = (weakref.ref(self.loader), cache_name)
if self.cache is not None: if self.cache is not None:
template = self.cache.get(cache_key) template = self.cache.get(cache_key)
if template is not None and ( if template is not None and (
not self.auto_reload or template.is_up_to_date not self.auto_reload or template.is_up_to_date
): ):
# template.globals is a ChainMap, modifying it will only
# affect the template, not the environment globals.
if globals:
template.globals.update(globals)
return template return template
template = self.loader.load(self, name, globals)
template = self.loader.load(self, name, self.make_globals(globals))
if self.cache is not None: if self.cache is not None:
self.cache[cache_key] = template self.cache[cache_key] = template
return template return template
class ThemeLoader(FileSystemLoader): class ThemeLoader(FileSystemLoader):
"""Custom FileSystemLoader that is aware of theme structure and config. """Custom FileSystemLoader that is aware of theme structure and config."""
"""
DEFAULT_THEMES_PATH = os.path.join(os.path.dirname(__file__), "themes") DEFAULT_THEMES_PATH = os.path.join(os.path.dirname(__file__), "themes")
_ADMIN_THEME_PREFIX = ADMIN_THEME + "/" _ADMIN_THEME_PREFIX = ADMIN_THEME + "/"

View File

@@ -120,7 +120,7 @@ def export_ctf():
backup = export_ctf_util() backup = export_ctf_util()
ctf_name = ctf_config.ctf_name() ctf_name = ctf_config.ctf_name()
day = datetime.datetime.now().strftime("%Y-%m-%d_%T") day = datetime.datetime.now().strftime("%Y-%m-%d_%T")
full_name = u"{}.{}.zip".format(ctf_name, day) full_name = "{}.{}.zip".format(ctf_name, day)
return send_file( return send_file(
backup, cache_timeout=-1, as_attachment=True, attachment_filename=full_name backup, cache_timeout=-1, as_attachment=True, attachment_filename=full_name
) )

View File

@@ -31,7 +31,5 @@ def sqlalchemy_to_pydantic(
for name, python_type in include.items(): for name, python_type in include.items():
default = None default = None
fields[name] = (python_type, default) fields[name] = (python_type, default)
pydantic_model = create_model( pydantic_model = create_model(db_model.__name__, **fields) # type: ignore
db_model.__name__, **fields # type: ignore
)
return pydantic_model return pydantic_model

View File

@@ -51,7 +51,10 @@ class TopicList(Resource):
{ {
"value": (str, None), "value": (str, None),
"q": (str, None), "q": (str, None),
"field": (RawEnum("TopicFields", {"value": "value"}), None,), "field": (
RawEnum("TopicFields", {"value": "value"}),
None,
),
}, },
location="query", location="query",
) )
@@ -122,7 +125,8 @@ class TopicList(Resource):
responses={200: ("Success", "APISimpleSuccessResponse")}, responses={200: ("Success", "APISimpleSuccessResponse")},
) )
@validate_args( @validate_args(
{"type": (str, None), "target_id": (int, 0)}, location="query", {"type": (str, None), "target_id": (int, 0)},
location="query",
) )
def delete(self, query_args): def delete(self, query_args):
topic_type = query_args.get("type") topic_type = query_args.get("type")

View File

@@ -208,9 +208,11 @@ def register():
registration_code = str(request.form.get("registration_code", "")) registration_code = str(request.form.get("registration_code", ""))
name_len = len(name) == 0 name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first() names = (
Users.query.add_columns(Users.name, Users.id).filter_by(name=name).first()
)
emails = ( emails = (
Users.query.add_columns("email", "id") Users.query.add_columns(Users.email, Users.id)
.filter_by(email=email_address) .filter_by(email=email_address)
.first() .first()
) )

View File

@@ -21,7 +21,7 @@ def timed_lru_cache(timeout: int = 300, maxsize: int = 64, typed: bool = False):
def wrapper_cache(func): def wrapper_cache(func):
func = lru_cache(maxsize=maxsize, typed=typed)(func) func = lru_cache(maxsize=maxsize, typed=typed)(func)
func.delta = timeout * 10 ** 9 func.delta = timeout * 10**9
func.expiration = monotonic_ns() + func.delta func.expiration = monotonic_ns() + func.delta
@wraps(func) @wraps(func)

View File

@@ -12,7 +12,8 @@ def render_error(error):
try: try:
return ( return (
render_template( render_template(
"errors/{}.html".format(error.code), error=error.description, "errors/{}.html".format(error.code),
error=error.description,
), ),
error.code, error.code,
) )

View File

@@ -49,7 +49,8 @@ class AccountSettingsForm(BaseForm):
description="Max number of teams (Teams mode only)", description="Max number of teams (Teams mode only)",
) )
num_users = IntegerField( num_users = IntegerField(
widget=NumberInput(min=0), description="Max number of users", widget=NumberInput(min=0),
description="Max number of users",
) )
verify_emails = SelectField( verify_emails = SelectField(
"Verify Emails", "Verify Emails",
@@ -101,13 +102,15 @@ class LegalSettingsForm(BaseForm):
description="External URL to a Terms of Service document hosted elsewhere", description="External URL to a Terms of Service document hosted elsewhere",
) )
tos_text = TextAreaField( tos_text = TextAreaField(
"Terms of Service", description="Text shown on the Terms of Service page", "Terms of Service",
description="Text shown on the Terms of Service page",
) )
privacy_url = URLField( privacy_url = URLField(
"Privacy Policy URL", "Privacy Policy URL",
description="External URL to a Privacy Policy document hosted elsewhere", description="External URL to a Privacy Policy document hosted elsewhere",
) )
privacy_text = TextAreaField( privacy_text = TextAreaField(
"Privacy Policy", description="Text shown on the Privacy Policy page", "Privacy Policy",
description="Text shown on the Privacy Policy page",
) )
submit = SubmitField("Update") submit = SubmitField("Update")

View File

@@ -15,12 +15,13 @@ ma = Marshmallow()
def get_class_by_tablename(tablename): def get_class_by_tablename(tablename):
"""Return class reference mapped to table. """Return class reference mapped to table.
https://stackoverflow.com/a/23754464 https://stackoverflow.com/a/66666783
:param tablename: String with name of table. :param tablename: String with name of table.
:return: Class reference or None. :return: Class reference or None.
""" """
for c in db.Model._decl_class_registry.values(): for m in db.Model.registry.mappers:
c = m.class_
if hasattr(c, "__tablename__") and c.__tablename__ == tablename: if hasattr(c, "__tablename__") and c.__tablename__ == tablename:
return c return c
return None return None

View File

@@ -25,11 +25,13 @@ class DynamicChallenge(Challenges):
class DynamicValueChallenge(BaseChallenge): class DynamicValueChallenge(BaseChallenge):
id = "dynamic" # Unique identifier used to register challenges id = "dynamic" # Unique identifier used to register challenges
name = "dynamic" # Name of a challenge type name = "dynamic" # Name of a challenge type
templates = { # Handlebars templates used for each aspect of challenge editing & viewing templates = (
{ # Handlebars templates used for each aspect of challenge editing & viewing
"create": "/plugins/dynamic_challenges/assets/create.html", "create": "/plugins/dynamic_challenges/assets/create.html",
"update": "/plugins/dynamic_challenges/assets/update.html", "update": "/plugins/dynamic_challenges/assets/update.html",
"view": "/plugins/dynamic_challenges/assets/view.html", "view": "/plugins/dynamic_challenges/assets/view.html",
} }
)
scripts = { # Scripts that are loaded when a template is loaded scripts = { # Scripts that are loaded when a template is loaded
"create": "/plugins/dynamic_challenges/assets/create.js", "create": "/plugins/dynamic_challenges/assets/create.js",
"update": "/plugins/dynamic_challenges/assets/update.js", "update": "/plugins/dynamic_challenges/assets/update.js",

View File

@@ -57,8 +57,8 @@ def logarithmic(challenge):
# It is important that this calculation takes into account floats. # It is important that this calculation takes into account floats.
# Hence this file uses from __future__ import division # Hence this file uses from __future__ import division
value = ( value = (
((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) ((challenge.minimum - challenge.initial) / (challenge.decay**2))
* (solve_count ** 2) * (solve_count**2)
) + challenge.initial ) + challenge.initial
value = math.ceil(value) value = math.ceil(value)

View File

@@ -68,5 +68,7 @@ class ChallengeSchema(ma.ModelSchema):
) )
requirements = field_for( requirements = field_for(
Challenges, "requirements", validate=[ChallengeRequirementsValidator()], Challenges,
"requirements",
validate=[ChallengeRequirementsValidator()],
) )

View File

@@ -16,7 +16,9 @@ class PageSchema(ma.ModelSchema):
"title", "title",
validate=[ validate=[
validate.Length( validate.Length(
min=0, max=80, error="Page could not be saved. Your title is too long.", min=0,
max=80,
error="Page could not be saved. Your title is too long.",
) )
], ],
) )

View File

@@ -111,10 +111,14 @@ def get_solve_counts_for_challenges(challenge_id=None, admin=False):
else: else:
freeze_cond = true() freeze_cond = true()
exclude_solves_cond = and_( exclude_solves_cond = and_(
AccountModel.banned == false(), AccountModel.hidden == false(), AccountModel.banned == false(),
AccountModel.hidden == false(),
) )
solves_q = ( solves_q = (
db.session.query(Solves.challenge_id, sa_func.count(Solves.challenge_id),) db.session.query(
Solves.challenge_id,
sa_func.count(Solves.challenge_id),
)
.join(AccountModel) .join(AccountModel)
.filter(*challenge_id_filter, freeze_cond, exclude_solves_cond) .filter(*challenge_id_filter, freeze_cond, exclude_solves_cond)
.group_by(Solves.challenge_id) .group_by(Solves.challenge_id)

View File

@@ -368,21 +368,31 @@ def load_challenges_csv(dict_reader):
if flags: if flags:
flags = [flag.strip() for flag in flags.split(",")] flags = [flag.strip() for flag in flags.split(",")]
for flag in flags: for flag in flags:
f = Flags(type="static", challenge_id=challenge.id, content=flag,) f = Flags(
type="static",
challenge_id=challenge.id,
content=flag,
)
db.session.add(f) db.session.add(f)
db.session.commit() db.session.commit()
if tags: if tags:
tags = [tag.strip() for tag in tags.split(",")] tags = [tag.strip() for tag in tags.split(",")]
for tag in tags: for tag in tags:
t = Tags(challenge_id=challenge.id, value=tag,) t = Tags(
challenge_id=challenge.id,
value=tag,
)
db.session.add(t) db.session.add(t)
db.session.commit() db.session.commit()
if hints: if hints:
hints = [hint.strip() for hint in hints.split(",")] hints = [hint.strip() for hint in hints.split(",")]
for hint in hints: for hint in hints:
h = Hints(challenge_id=challenge.id, content=hint,) h = Hints(
challenge_id=challenge.id,
content=hint,
)
db.session.add(h) db.session.add(h)
db.session.commit() db.session.commit()
if errors: if errors:

View File

@@ -5,7 +5,7 @@ from CTFd.utils import get_config
def ctftime(): def ctftime():
""" Checks whether it's CTF time or not. """ """Checks whether it's CTF time or not."""
start = get_config("start") start = get_config("start")
end = get_config("end") end = get_config("end")

View File

@@ -84,6 +84,7 @@ def export_ctf():
backup_zip.close() backup_zip.close()
backup.seek(0) backup.seek(0)
db.close()
return backup return backup

View File

@@ -17,10 +17,10 @@ migrations = Migrate()
def create_database(): def create_database():
url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) url = make_url(app.config["SQLALCHEMY_DATABASE_URI"])
if url.drivername == "postgres": if url.drivername == "postgres":
url.drivername = "postgresql" url = url.set(drivername="postgresql")
if url.drivername.startswith("mysql"): if url.drivername.startswith("mysql"):
url.query["charset"] = "utf8mb4" url = url.update_query_dict({"charset": "utf8mb4"})
# Creates database if the database database does not exist # Creates database if the database database does not exist
if not database_exists_util(url): if not database_exists_util(url):
@@ -34,7 +34,7 @@ def create_database():
def drop_database(): def drop_database():
url = make_url(app.config["SQLALCHEMY_DATABASE_URI"]) url = make_url(app.config["SQLALCHEMY_DATABASE_URI"])
if url.drivername == "postgres": if url.drivername == "postgres":
url.drivername = "postgresql" url = url.set(drivername="postgresql")
drop_database_util(url) drop_database_util(url)

View File

@@ -152,7 +152,7 @@ def is_verified():
def get_ip(req=None): def get_ip(req=None):
""" Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies """Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies
(in this case the local network), and only trust the most recently defined untrusted IP address. (in this case the local network), and only trust the most recently defined untrusted IP address.
Taken from http://stackoverflow.com/a/22936947/4285524 but the generator there makes no sense. Taken from http://stackoverflow.com/a/22936947/4285524 but the generator there makes no sense.
The trusted_proxies regexes is taken from Ruby on Rails. The trusted_proxies regexes is taken from Ruby on Rails.

View File

@@ -124,9 +124,15 @@ def setup():
password = request.form["password"] password = request.form["password"]
name_len = len(name) == 0 name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first() names = (
Users.query.add_columns(Users.name, Users.id)
.filter_by(name=name)
.first()
)
emails = ( emails = (
Users.query.add_columns("email", "id").filter_by(email=email).first() Users.query.add_columns(Users.email, Users.id)
.filter_by(email=email)
.first()
) )
pass_short = len(password) == 0 pass_short = len(password) == 0
pass_long = len(password) > 128 pass_long = len(password) > 128

View File

@@ -3,17 +3,14 @@ pip-tools==5.4.0
pytest==7.3.1 pytest==7.3.1
pytest-randomly==3.12.0 pytest-randomly==3.12.0
coverage==7.2.3 coverage==7.2.3
ruff==0.0.260
psycopg2-binary==2.9.6 psycopg2-binary==2.9.6
moto==1.3.16 moto==4.1.11
bandit==1.6.2 bandit==1.6.2
flask_profiler==1.8.1 flask_profiler==1.8.1
pytest-xdist==3.2.1 pytest-xdist==3.2.1
pytest-cov==4.0.0 pytest-cov==4.0.0
sphinx_rtd_theme==0.4.3 sphinx_rtd_theme==0.4.3
flask-debugtoolbar==0.11.0 flask-debugtoolbar==0.11.0
isort==4.3.21
Faker==4.1.0 Faker==4.1.0
pipdeptree==2.2.0 pipdeptree==2.2.0
black==19.10b0
pytest-sugar==0.9.7 pytest-sugar==0.9.7

3
linting.txt Normal file
View File

@@ -0,0 +1,3 @@
black==22.3.0
isort==4.3.21
ruff==0.0.260

View File

@@ -17,7 +17,7 @@ if url.drivername.startswith("sqlite"):
# Null out the database so raw_connection doesnt error if it doesnt exist # Null out the database so raw_connection doesnt error if it doesnt exist
# CTFd will create the database if it doesnt exist # CTFd will create the database if it doesnt exist
url.database = None url = url.set(database=None)
# Wait for the database server to be available # Wait for the database server to be available
engine = create_engine(url) engine = create_engine(url)

View File

@@ -1,24 +1,22 @@
Flask==1.1.2 Flask==2.0.3
Werkzeug==1.0.1 Werkzeug==2.1.2
Jinja2==2.11.3 Flask-SQLAlchemy==2.5.1
Flask-SQLAlchemy==2.4.3 Flask-Caching==2.0.2
Flask-Caching==1.8.0
Flask-Migrate==2.5.3 Flask-Migrate==2.5.3
Flask-Script==2.0.6 Flask-Script==2.0.6
SQLAlchemy==1.3.17 SQLAlchemy==1.4.48
SQLAlchemy-Utils==0.41.0 SQLAlchemy-Utils==0.41.1
passlib==1.7.4 passlib==1.7.4
bcrypt==4.0.1 bcrypt==4.0.1
itsdangerous==1.1.0
requests==2.28.1 requests==2.28.1
PyMySQL[rsa]==0.9.3 PyMySQL[rsa]==0.9.3
gunicorn==20.1.0 gunicorn==20.1.0
dataset==1.3.1 dataset==1.5.2
cmarkgfm==2022.10.27 cmarkgfm==2022.10.27
redis==4.4.4 redis==4.5.5
gevent==22.10.2 gevent==22.10.2
python-dotenv==0.13.0 python-dotenv==0.13.0
flask-restx==0.5.1 flask-restx==1.1.0
flask-marshmallow==0.10.1 flask-marshmallow==0.10.1
marshmallow-sqlalchemy==0.17.0 marshmallow-sqlalchemy==0.17.0
boto3==1.13.9 boto3==1.13.9

View File

@@ -1,6 +1,6 @@
# #
# This file is autogenerated by pip-compile with python 3.9 # This file is autogenerated by pip-compile with Python 3.9
# To update, run: # by the following command:
# #
# ./scripts/pip-compile.sh # ./scripts/pip-compile.sh
# #
@@ -16,6 +16,8 @@ attrs==20.3.0
# via jsonschema # via jsonschema
babel==2.12.1 babel==2.12.1
# via flask-babel # via flask-babel
banal==1.0.6
# via dataset
bcrypt==4.0.1 bcrypt==4.0.1
# via -r requirements.in # via -r requirements.in
boto3==1.13.9 boto3==1.13.9
@@ -24,6 +26,8 @@ botocore==1.16.26
# via # via
# boto3 # boto3
# s3transfer # s3transfer
cachelib==0.9.0
# via flask-caching
certifi==2022.12.7 certifi==2022.12.7
# via requests # via requests
cffi==1.15.0 cffi==1.15.0
@@ -39,11 +43,11 @@ cmarkgfm==2022.10.27
# via -r requirements.in # via -r requirements.in
cryptography==40.0.2 cryptography==40.0.2
# via pymysql # via pymysql
dataset==1.3.1 dataset==1.5.2
# via -r requirements.in # via -r requirements.in
docutils==0.15.2 docutils==0.15.2
# via botocore # via botocore
flask==1.1.2 flask==2.0.3
# via # via
# -r requirements.in # -r requirements.in
# flask-babel # flask-babel
@@ -55,17 +59,17 @@ flask==1.1.2
# flask-sqlalchemy # flask-sqlalchemy
flask-babel==2.0.0 flask-babel==2.0.0
# via -r requirements.in # via -r requirements.in
flask-caching==1.8.0 flask-caching==2.0.2
# via -r requirements.in # via -r requirements.in
flask-marshmallow==0.10.1 flask-marshmallow==0.10.1
# via -r requirements.in # via -r requirements.in
flask-migrate==2.5.3 flask-migrate==2.5.3
# via -r requirements.in # via -r requirements.in
flask-restx==0.5.1 flask-restx==1.1.0
# via -r requirements.in # via -r requirements.in
flask-script==2.0.6 flask-script==2.0.6
# via -r requirements.in # via -r requirements.in
flask-sqlalchemy==2.4.3 flask-sqlalchemy==2.5.1
# via # via
# -r requirements.in # -r requirements.in
# flask-migrate # flask-migrate
@@ -74,18 +78,17 @@ freezegun==1.2.2
gevent==22.10.2 gevent==22.10.2
# via -r requirements.in # via -r requirements.in
greenlet==2.0.1 greenlet==2.0.1
# via gevent # via
# gevent
# sqlalchemy
gunicorn==20.1.0 gunicorn==20.1.0
# via -r requirements.in # via -r requirements.in
idna==2.10 idna==2.10
# via requests # via requests
itsdangerous==1.1.0 itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via # via
# -r requirements.in
# flask
jinja2==2.11.3
# via
# -r requirements.in
# flask # flask
# flask-babel # flask-babel
jmespath==0.10.0 jmespath==0.10.0
@@ -96,7 +99,7 @@ jsonschema==3.2.0
# via flask-restx # via flask-restx
mako==1.1.3 mako==1.1.3
# via alembic # via alembic
markupsafe==1.1.1 markupsafe==2.1.3
# via # via
# jinja2 # jinja2
# mako # mako
@@ -139,7 +142,7 @@ pytz==2020.4
# via # via
# flask-babel # flask-babel
# flask-restx # flask-restx
redis==3.5.2 redis==4.5.5
# via -r requirements.in # via -r requirements.in
requests==2.28.1 requests==2.28.1
# via -r requirements.in # via -r requirements.in
@@ -148,11 +151,10 @@ s3transfer==0.3.3
six==1.15.0 six==1.15.0
# via # via
# flask-marshmallow # flask-marshmallow
# flask-restx
# jsonschema # jsonschema
# python-dateutil # python-dateutil
# tenacity # tenacity
sqlalchemy==1.3.17 sqlalchemy==1.4.48
# via # via
# -r requirements.in # -r requirements.in
# alembic # alembic
@@ -160,7 +162,7 @@ sqlalchemy==1.3.17
# flask-sqlalchemy # flask-sqlalchemy
# marshmallow-sqlalchemy # marshmallow-sqlalchemy
# sqlalchemy-utils # sqlalchemy-utils
sqlalchemy-utils==0.41.0 sqlalchemy-utils==0.41.1
# via -r requirements.in # via -r requirements.in
tenacity==6.2.0 tenacity==6.2.0
# via -r requirements.in # via -r requirements.in
@@ -168,7 +170,7 @@ urllib3==1.25.11
# via # via
# botocore # botocore
# requests # requests
werkzeug==1.0.1 werkzeug==2.1.2
# via # via
# -r requirements.in # -r requirements.in
# flask # flask

View File

@@ -7,4 +7,4 @@ docker run \
-v $ROOTDIR:/mnt/CTFd \ -v $ROOTDIR:/mnt/CTFd \
-e CUSTOM_COMPILE_COMMAND='./scripts/pip-compile.sh' \ -e CUSTOM_COMPILE_COMMAND='./scripts/pip-compile.sh' \
-it python:3.9-slim-buster \ -it python:3.9-slim-buster \
-c 'cd /mnt/CTFd && pip install pip-tools==6.6.0 && pip-compile' -c 'cd /mnt/CTFd && pip install pip-tools==6.13.0 && pip-compile'

View File

@@ -59,7 +59,7 @@ def test_admin_access():
for route in routes: for route in routes:
r = client.get(route) r = client.get(route)
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/login") assert r.location.startswith("/login")
admin = login_as_user(app, name="admin") admin = login_as_user(app, name="admin")
routes.remove("/admin") routes.remove("/admin")
@@ -78,5 +78,5 @@ def test_get_admin_as_user():
client = login_as_user(app) client = login_as_user(app)
r = client.get("/admin") r = client.get("/admin")
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/login") assert r.location.startswith("/login")
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -132,7 +132,8 @@ def test_config_value_types():
# Test regular length strings # Test regular length strings
r = admin.patch( r = admin.patch(
"/api/v1/configs", json={"ctf_footer": "// regular length string"}, "/api/v1/configs",
json={"ctf_footer": "// regular length string"},
) )
assert r.status_code == 200 assert r.status_code == 200
assert get_config("ctf_footer") == "// regular length string" assert get_config("ctf_footer") == "// regular length string"

View File

@@ -44,5 +44,5 @@ def test_api_hint_404():
for endpoint in endpoints: for endpoint in endpoints:
r = client.get(endpoint.format(1)) r = client.get(endpoint.format(1))
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/login") assert r.location.startswith("/login")
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -138,7 +138,7 @@ def create_ctfd(
config.APPLICATION_ROOT = application_root config.APPLICATION_ROOT = application_root
url = make_url(config.SQLALCHEMY_DATABASE_URI) url = make_url(config.SQLALCHEMY_DATABASE_URI)
if url.database: if url.database:
url.database = str(uuid.uuid4()) url = url.set(database=str(uuid.uuid4()))
config.SQLALCHEMY_DATABASE_URI = str(url) config.SQLALCHEMY_DATABASE_URI = str(url)
app = create_app(config) app = create_app(config)

View File

@@ -18,7 +18,7 @@ def test_oauth_not_configured():
with app.app_context(): with app.app_context():
with app.test_client() as client: with app.test_client() as client:
r = client.get("/oauth", follow_redirects=False) r = client.get("/oauth", follow_redirects=False)
assert r.location == "http://localhost/login" assert r.location == "/login"
r = client.get(r.location) r = client.get(r.location)
resp = r.get_data(as_text=True) resp = r.get_data(as_text=True)
assert "OAuth Settings not configured" in resp assert "OAuth Settings not configured" in resp

View File

@@ -51,7 +51,7 @@ def test_anonymous_users_view_public_challenges_without_team():
with app.test_client() as client: with app.test_client() as client:
r = client.get("/challenges") r = client.get("/challenges")
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/login") assert r.location.startswith("/login")
set_config("challenge_visibility", "public") set_config("challenge_visibility", "public")
with app.test_client() as client: with app.test_client() as client:
@@ -61,5 +61,5 @@ def test_anonymous_users_view_public_challenges_without_team():
with login_as_user(app) as client: with login_as_user(app) as client:
r = client.get("/challenges") r = client.get("/challenges")
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/team") assert r.location.startswith("/team")
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -90,7 +90,7 @@ def test_that_ctfd_can_be_deployed_in_subdir():
with app.test_client() as client: with app.test_client() as client:
r = client.get("/") r = client.get("/")
assert r.status_code == 302 assert r.status_code == 302
assert r.location == "http://localhost/ctf/setup" assert r.location == "/ctf/setup"
r = client.get("/setup") r = client.get("/setup")
with client.session_transaction() as sess: with client.session_transaction() as sess:
@@ -105,13 +105,13 @@ def test_that_ctfd_can_be_deployed_in_subdir():
} }
r = client.post("/setup", data=data) r = client.post("/setup", data=data)
assert r.status_code == 302 assert r.status_code == 302
assert r.location == "http://localhost/ctf/" assert r.location == "/ctf/"
c = Client(app) c = Client(app)
app_iter, status, headers = c.get("/") response = c.get("/")
headers = dict(headers) headers = dict(response.headers)
assert status == "302 FOUND" assert response.status == "302 FOUND"
assert headers["Location"] == "http://localhost/ctf/" assert headers["Location"] == "/ctf/?"
r = client.get("/challenges") r = client.get("/challenges")
assert r.status_code == 200 assert r.status_code == 200

View File

@@ -83,7 +83,7 @@ def test_page_requiring_auth():
with app.test_client() as client: with app.test_client() as client:
r = client.get("/this-is-a-route") r = client.get("/this-is-a-route")
assert r.status_code == 302 assert r.status_code == 302
assert r.location == "http://localhost/login?next=%2Fthis-is-a-route%3F" assert r.location == "/login?next=%2Fthis-is-a-route%3F"
register_user(app) register_user(app)
client = login_as_user(app) client = login_as_user(app)
@@ -388,6 +388,13 @@ def test_user_can_access_files_with_auth_token():
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
set_config("end", "1507262400") set_config("end", "1507262400")
set_config("view_after_ctf", True) set_config("view_after_ctf", True)
# Get file_url under current time
with login_as_user(app) as user:
req = user.get("/api/v1/challenges/1")
data = req.get_json()
file_url = data["data"]["files"][0]
for v in ("public", "private"): for v in ("public", "private"):
set_config("challenge_visibility", v) set_config("challenge_visibility", v)
@@ -424,12 +431,13 @@ def test_user_can_access_files_if_view_after_ctf():
register_user(app) register_user(app)
with login_as_user(app) as client: with login_as_user(app) as client:
# After ctf end
# Get file_url during freeze time
with freeze_time("2017-10-7"):
req = client.get("/api/v1/challenges/1") req = client.get("/api/v1/challenges/1")
data = req.get_json() data = req.get_json()
file_url = data["data"]["files"][0] file_url = data["data"]["files"][0]
# After ctf end
with freeze_time("2017-10-7"):
# Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
set_config("end", "1507262400") set_config("end", "1507262400")
@@ -443,8 +451,8 @@ def test_user_can_access_files_if_view_after_ctf():
assert r.get_data(as_text=True) == "testing file load" assert r.get_data(as_text=True) == "testing file load"
# Unauthed users should be able to download if view_after_ctf # Unauthed users should be able to download if view_after_ctf
client = app.test_client() unauth_client = app.test_client()
r = client.get(file_url) r = unauth_client.get(file_url)
assert r.status_code == 200 assert r.status_code == 200
assert r.get_data(as_text=True) == "testing file load" assert r.get_data(as_text=True) == "testing file load"
finally: finally:

View File

@@ -119,9 +119,7 @@ def test_user_bad_login():
with client.session_transaction() as sess: with client.session_transaction() as sess:
assert sess.get("id") is None assert sess.get("id") is None
r = client.get("/profile") r = client.get("/profile")
assert r.location.startswith( assert r.location.startswith("/login") # We got redirected to login
"http://localhost/login"
) # We got redirected to login
destroy_ctfd(app) destroy_ctfd(app)
@@ -132,9 +130,7 @@ def test_user_login():
register_user(app) register_user(app)
client = login_as_user(app) client = login_as_user(app)
r = client.get("/profile") r = client.get("/profile")
assert ( assert r.location is None # We didn't get redirected to login
r.location != "http://localhost/login"
) # We didn't get redirected to login
assert r.status_code == 200 assert r.status_code == 200
destroy_ctfd(app) destroy_ctfd(app)
@@ -146,9 +142,7 @@ def test_user_login_with_email():
register_user(app) register_user(app)
client = login_as_user(app, name="user@examplectf.com", password="password") client = login_as_user(app, name="user@examplectf.com", password="password")
r = client.get("/profile") r = client.get("/profile")
assert ( assert r.location is None # We didn't get redirected to login
r.location != "http://localhost/login"
) # We didn't get redirected to login
assert r.status_code == 200 assert r.status_code == 200
destroy_ctfd(app) destroy_ctfd(app)
@@ -161,7 +155,7 @@ def test_user_get_logout():
client = login_as_user(app) client = login_as_user(app)
client.get("/logout", follow_redirects=True) client.get("/logout", follow_redirects=True)
r = client.get("/challenges") r = client.get("/challenges")
assert r.location == "http://localhost/login?next=%2Fchallenges%3F" assert r.location == "/login?next=%2Fchallenges%3F"
assert r.status_code == 302 assert r.status_code == 302
destroy_ctfd(app) destroy_ctfd(app)
@@ -182,7 +176,7 @@ def test_user_isnt_admin():
"config", "config",
]: ]:
r = client.get("/admin/{}".format(page)) r = client.get("/admin/{}".format(page))
assert r.location.startswith("http://localhost/login?next=") assert r.location.startswith("/login?next=")
assert r.status_code == 302 assert r.status_code == 302
destroy_ctfd(app) destroy_ctfd(app)
@@ -302,7 +296,7 @@ def test_user_can_confirm_email(mock_smtp):
client = login_as_user(app, name="user1", password="password") client = login_as_user(app, name="user1", password="password")
r = client.get("http://localhost/confirm") r = client.get("/confirm")
assert "We've sent a confirmation email" in r.get_data(as_text=True) assert "We've sent a confirmation email" in r.get_data(as_text=True)
# smtp send message function was called # smtp send message function was called
@@ -310,23 +304,21 @@ def test_user_can_confirm_email(mock_smtp):
with client.session_transaction() as sess: with client.session_transaction() as sess:
data = {"nonce": sess.get("nonce")} data = {"nonce": sess.get("nonce")}
r = client.post("http://localhost/confirm", data=data) r = client.post("/confirm", data=data)
assert "Confirmation email sent to" in r.get_data(as_text=True) assert "Confirmation email sent to" in r.get_data(as_text=True)
r = client.get("/challenges") r = client.get("/challenges")
assert ( assert r.location == "/confirm" # We got redirected to /confirm
r.location == "http://localhost/confirm"
) # We got redirected to /confirm
r = client.get("http://localhost/confirm/" + serialize("user@user.com")) r = client.get("/confirm/" + serialize("user@user.com"))
assert r.location == "http://localhost/challenges" assert r.location == "/challenges"
# The team is now verified # The team is now verified
user = Users.query.filter_by(email="user@user.com").first() user = Users.query.filter_by(email="user@user.com").first()
assert user.verified is True assert user.verified is True
r = client.get("http://localhost/confirm") r = client.get("/confirm")
assert r.location == "http://localhost/settings" assert r.location == "/settings"
destroy_ctfd(app) destroy_ctfd(app)
@@ -462,7 +454,7 @@ def test_registration_code_required():
data["registration_code"] = "secret-sauce" data["registration_code"] = "secret-sauce"
r = client.post("/register", data=data) r = client.post("/register", data=data)
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/challenges") assert r.location.startswith("/challenges")
destroy_ctfd(app) destroy_ctfd(app)
@@ -492,5 +484,5 @@ def test_registration_code_allows_numeric():
data["registration_code"] = "1234567890" data["registration_code"] = "1234567890"
r = client.post("/register", data=data) r = client.post("/register", data=data)
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/challenges") assert r.location.startswith("/challenges")
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -228,7 +228,7 @@ def test_submitting_unicode_flag():
register_user(app) register_user(app)
client = login_as_user(app) client = login_as_user(app)
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
gen_flag(app.db, challenge_id=chal.id, content=u"你好") gen_flag(app.db, challenge_id=chal.id, content="你好")
with client.session_transaction(): with client.session_transaction():
data = {"submission": "你好", "challenge_id": chal.id} data = {"submission": "你好", "challenge_id": chal.id}
r = client.post("/api/v1/challenges/attempt", json=data) r = client.post("/api/v1/challenges/attempt", json=data)
@@ -251,7 +251,7 @@ def test_challenges_with_max_attempts():
chal.max_attempts = 3 chal.max_attempts = 3
app.db.session.commit() app.db.session.commit()
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
for _ in range(3): for _ in range(3):
data = {"submission": "notflag", "challenge_id": chal_id} data = {"submission": "notflag", "challenge_id": chal_id}
r = client.post("/api/v1/challenges/attempt", json=data) r = client.post("/api/v1/challenges/attempt", json=data)
@@ -281,7 +281,7 @@ def test_challenge_kpm_limit():
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
chal_id = chal.id chal_id = chal.id
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
for _ in range(11): for _ in range(11):
with client.session_transaction(): with client.session_transaction():
data = {"submission": "notflag", "challenge_id": chal_id} data = {"submission": "notflag", "challenge_id": chal_id}

View File

@@ -259,7 +259,7 @@ def test_user_needs_all_required_fields():
r = client.get("/challenges") r = client.get("/challenges")
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/settings") assert r.location.startswith("/settings")
# Populate the non-required fields # Populate the non-required fields
r = client.patch( r = client.patch(
@@ -277,7 +277,7 @@ def test_user_needs_all_required_fields():
# I should still be restricted from seeing challenges # I should still be restricted from seeing challenges
r = client.get("/challenges") r = client.get("/challenges")
assert r.status_code == 302 assert r.status_code == 302
assert r.location.startswith("http://localhost/settings") assert r.location.startswith("/settings")
# I should still see all fields b/c I don't have a complete profile # I should still see all fields b/c I don't have a complete profile
r = client.get("/settings") r = client.get("/settings")

View File

@@ -11,7 +11,7 @@ def test_ctfd_setup_redirect():
with app.test_client() as client: with app.test_client() as client:
r = client.get("/users") r = client.get("/users")
assert r.status_code == 302 assert r.status_code == 302
assert r.location == "http://localhost/setup" assert r.location == "/setup"
# Files in /themes load properly # Files in /themes load properly
r = client.get("/themes/core/static/css/main.dev.css") r = client.get("/themes/core/static/css/main.dev.css")
@@ -52,5 +52,5 @@ def test_ctfd_setup_verification():
data["email"] = "admin@examplectf.com" data["email"] = "admin@examplectf.com"
r = client.post("/setup", data=data) r = client.post("/setup", data=data)
assert r.status_code == 302 assert r.status_code == 302
assert r.location == "http://localhost/" assert r.location == "/"
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -21,7 +21,7 @@ def test_ctftime_prevents_accessing_challenges_before_ctf():
register_user(app) register_user(app)
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
chal_id = chal.id chal_id = chal.id
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
with ctftime.not_started(): with ctftime.not_started():
client = login_as_user(app) client = login_as_user(app)
@@ -48,7 +48,7 @@ def test_ctftime_redirects_to_teams_page_in_teams_mode_before_ctf():
with ctftime.init(): with ctftime.init():
register_user(app) register_user(app)
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
with ctftime.not_started(): with ctftime.not_started():
client = login_as_user(app) client = login_as_user(app)
@@ -83,7 +83,7 @@ def test_ctftime_allows_accessing_challenges_during_ctf():
register_user(app) register_user(app)
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
chal_id = chal.id chal_id = chal.id
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
with ctftime.started(): with ctftime.started():
client = login_as_user(app) client = login_as_user(app)
@@ -112,7 +112,7 @@ def test_ctftime_prevents_accessing_challenges_after_ctf():
register_user(app) register_user(app)
chal = gen_challenge(app.db) chal = gen_challenge(app.db)
chal_id = chal.id chal_id = chal.id
gen_flag(app.db, challenge_id=chal.id, content=u"flag") gen_flag(app.db, challenge_id=chal.id, content="flag")
with ctftime.ended(): with ctftime.ended():
client = login_as_user(app) client = login_as_user(app)

View File

@@ -104,7 +104,7 @@ def test_sendmail_with_mailgun_from_config_file(fake_post_request):
args, kwargs = fake_post_request.call_args args, kwargs = fake_post_request.call_args
assert args[0] == "https://api.mailgun.net/v3/file.faked.com/messages" assert args[0] == "https://api.mailgun.net/v3/file.faked.com/messages"
assert kwargs["auth"] == ("api", u"key-1234567890-file-config") assert kwargs["auth"] == ("api", "key-1234567890-file-config")
assert kwargs["timeout"] == 1.0 assert kwargs["timeout"] == 1.0
assert kwargs["data"] == { assert kwargs["data"] == {
"to": ["user@user.com"], "to": ["user@user.com"],
@@ -144,7 +144,7 @@ def test_sendmail_with_mailgun_from_db_config(fake_post_request):
args, kwargs = fake_post_request.call_args args, kwargs = fake_post_request.call_args
assert args[0] == "https://api.mailgun.net/v3/db.faked.com/messages" assert args[0] == "https://api.mailgun.net/v3/db.faked.com/messages"
assert kwargs["auth"] == ("api", u"key-1234567890-db-config") assert kwargs["auth"] == ("api", "key-1234567890-db-config")
assert kwargs["timeout"] == 1.0 assert kwargs["timeout"] == 1.0
assert kwargs["data"] == { assert kwargs["data"] == {
"to": ["user@user.com"], "to": ["user@user.com"],

View File

@@ -1,7 +1,6 @@
from unittest.mock import Mock, patch
from uuid import UUID from uuid import UUID
from mock import Mock, patch
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
@@ -40,7 +39,7 @@ def test_session_invalidation_on_admin_password_change():
r = user.get("/settings") r = user.get("/settings")
# User's password was changed # User's password was changed
# They should be logged out # They should be logged out
assert r.location.startswith("http://localhost/login") assert r.location.startswith("/login")
assert r.status_code == 302 assert r.status_code == 302
destroy_ctfd(app) destroy_ctfd(app)

View File

@@ -69,14 +69,14 @@ def test_update_check_ignores_downgrades(fake_post_request):
fake_response = Mock() fake_response = Mock()
fake_post_request.return_value = fake_response fake_post_request.return_value = fake_response
fake_response.json = lambda: { fake_response.json = lambda: {
u"resource": { "resource": {
u"html_url": u"https://github.com/CTFd/CTFd/releases/tag/0.0.1", "html_url": "https://github.com/CTFd/CTFd/releases/tag/0.0.1",
u"download_url": u"https://api.github.com/repos/CTFd/CTFd/zipball/0.0.1", "download_url": "https://api.github.com/repos/CTFd/CTFd/zipball/0.0.1",
u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", "published_at": "Wed, 25 Oct 2017 19:39:42 -0000",
u"tag": u"0.0.1", "tag": "0.0.1",
u"prerelease": False, "prerelease": False,
u"id": 6, "id": 6,
u"latest": True, "latest": True,
} }
} }
update_check() update_check()
@@ -85,18 +85,18 @@ def test_update_check_ignores_downgrades(fake_post_request):
fake_response = Mock() fake_response = Mock()
fake_post_request.return_value = fake_response fake_post_request.return_value = fake_response
fake_response.json = lambda: { fake_response.json = lambda: {
u"resource": { "resource": {
u"html_url": u"https://github.com/CTFd/CTFd/releases/tag/{}".format( "html_url": "https://github.com/CTFd/CTFd/releases/tag/{}".format(
app.VERSION app.VERSION
), ),
u"download_url": u"https://api.github.com/repos/CTFd/CTFd/zipball/{}".format( "download_url": "https://api.github.com/repos/CTFd/CTFd/zipball/{}".format(
app.VERSION app.VERSION
), ),
u"published_at": u"Wed, 25 Oct 2017 19:39:42 -0000", "published_at": "Wed, 25 Oct 2017 19:39:42 -0000",
u"tag": u"{}".format(app.VERSION), "tag": "{}".format(app.VERSION),
u"prerelease": False, "prerelease": False,
u"id": 6, "id": 6,
u"latest": True, "latest": True,
} }
} }
update_check() update_check()