Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
strategy:
matrix:
python-version: [
"3.11",
"3.12"
]

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ __pycache__/

# C extensions
*.so

data/
# Distribution / packaging
.Python
build/
Expand Down
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends build-essential gcc python3-dev
WORKDIR /app

RUN pip install poetry
Expand All @@ -15,7 +16,9 @@ RUN poetry install

WORKDIR /app/src

EXPOSE 8000
WORKDIR /app/src

# Run FastAPI with Gunicorn
CMD ["gunicorn", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "pwncore:app", "--bind", "0.0.0.0:8000", "--log-level", "debug"]
EXPOSE 8000
EXPOSE 8081
# Run both main app and admin app
CMD ["sh", "-c", "gunicorn -w 4 -k uvicorn.workers.UvicornWorker pwncore:app --bind 0.0.0.0:8000 --log-level debug & uvicorn pwncore.admin_app:admin_app --host 0.0.0.0 --port 8081 & wait"]
20 changes: 8 additions & 12 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ services:
dockerfile: Dockerfile
ports:
- ${PORT}:8000
- ${PORT_ADMIN}:8081
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- WORKERS=${WORKERS}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${CONFIG_FILE}:/app/src/pwncore/config.py
# - ./src:/app/src
depends_on:
- db

Expand All @@ -32,16 +34,10 @@ services:
ports:
- 5432:5432

# admin_db:
# image: nocodb/nocodb:latest
# environment:
# NC_DB: pg://db:5432?u=${POSTGRES_USER}&p=${POSTGRES_PASSWORD}&d=${POSTGRES_DB}
# volumes:
# - nc_data:/usr/app/data
# ports:
# - ${PORT_ADMIN}:8080
# depends_on:
# - db

# volumes:
# nc_data: {}
# admin_db:
# image: nocodb/nocodb:latest
# volumes:
# - ./data:/usr/app/data
# ports:
# - 8001:8080
43 changes: 42 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ multidict = "^6.0.5"
gunicorn = "^23.0.0"
bcrypt = ">3.1.0,<4.0"
pydantic-core = "2.20.1"
psutil = "^5.9.0"
types-psutil = "^7.0.0.20251001"

[tool.poetry.group.dev.dependencies]
mypy = "^1.6.1"
Expand Down
31 changes: 28 additions & 3 deletions src/pwncore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import shutil
from contextlib import asynccontextmanager
from logging import getLogger

import shutil
import aiodocker
from logging import getLogger
from fastapi import FastAPI
import jwt
from fastapi import FastAPI, Request, Response, status
from fastapi.middleware.cors import CORSMiddleware
from tortoise import Tortoise

Expand All @@ -12,9 +13,11 @@
import pwncore.routes as routes
from pwncore.config import config
from pwncore.models import Container
from pwncore.routes.auth import JwtInfo

logger = getLogger(__name__)


@asynccontextmanager
async def app_lifespan(app: FastAPI):
# Startup
Expand Down Expand Up @@ -72,3 +75,25 @@ async def app_lifespan(app: FastAPI):
allow_methods=["*"],
allow_headers=["*"],
)


@app.middleware("http")
async def check_blacklist(
request: Request,
call_next, # noqa: ANN001
):
"""Middleware to handle bans."""
try:
token = request.headers["authorization"].split(" ")[1] # Remove Bearer

decoded_token: JwtInfo = jwt.decode(
token,
config.jwt_secret,
algorithms=["HS256"],
)
team_id = decoded_token["team_id"]
if team_id in config.blacklist:
return Response(status_code=status.HTTP_403_FORBIDDEN)
except KeyError:
pass
return await call_next(request)
1 change: 1 addition & 0 deletions src/pwncore/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


def run_dev():

uvicorn.run("pwncore:app", host="127.0.0.1", port=8080, reload=True)


Expand Down
15 changes: 10 additions & 5 deletions src/pwncore/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import bcrypt
from dataclasses import dataclass
import warnings
from dataclasses import dataclass

from passlib.hash import bcrypt_sha256

"""
Expand Down Expand Up @@ -65,6 +65,7 @@ class Config:
staticfs_data_dir: str
staticfs_jwt_secret: str
admin_hash: str
blacklist: list[int]


config = Config(
Expand All @@ -73,7 +74,7 @@ class Config:
db_url=os.environ.get("DATABASE_URL", "sqlite://:memory:"),
# docker_url=None, # None for default system docker
# Or set it to an arbitrary URL for testing without Docker
docker_url="http://google.com",
docker_url=None,
flag="C0D",
max_containers_per_team=3,
jwt_secret="mysecret",
Expand All @@ -85,9 +86,13 @@ class Config:
staticfs_data_dir=os.environ.get("STATIC_DATA_DIR", "/data"),
staticfs_jwt_secret="PyMioVKFXHymQd+n7q5geOsT6fSYh3gDVw3GqilW+5U=",
admin_hash=admin_hash_value,
blacklist=[],
)

# Warn in production if env not loaded
if not config.development and using_default_admin:
warnings.warn("Default admin hash being used in production!", RuntimeWarning)

warnings.warn(
"Default admin hash being used in production!",
RuntimeWarning,
stacklevel=2,
)
6 changes: 4 additions & 2 deletions src/pwncore/models/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ class Container(Model):
problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField(
"models.Problem", on_delete=fields.OnDelete.NO_ACTION
)
problem_id = fields.IntField()
team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team")
team_id = fields.IntField()
flag = fields.TextField()
token = fields.TextField()

token = fields.TextField(null=True)
ports: fields.ReverseRelation[Ports]


Expand Down
6 changes: 4 additions & 2 deletions src/pwncore/models/ctf.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ class BaseProblem(Model):


class Problem(BaseProblem):
id = fields.IntField(pk=True)
image_name = fields.TextField()

# commenting it for now, may be used later
# image_config: fields.Field[dict[str, Any]] = fields.JSONField(
# null=True
# ) # type: ignore[assignment]
static = fields.BooleanField(default=False)
static_files = fields.BooleanField(default=False)
static_flag = fields.TextField(null=True)

mi = fields.IntField(default=50)
ma = fields.IntField(default=500)
Expand All @@ -45,7 +47,7 @@ class Problem(BaseProblem):
hints: fields.ReverseRelation[Hint]

class PydanticMeta:
exclude = ["image_name", "static", "mi", "ma", "visible"]
exclude = ["image_name", "static_files", "mi", "ma", "visible"]

async def _solves(self) -> int:
return await SolvedProblem.filter(problem=self).count()
Expand Down
6 changes: 2 additions & 4 deletions src/pwncore/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter

from pwncore.routes import ctf, team, auth, admin, leaderboard
from pwncore.routes import ctf, team, auth, leaderboard, admin

# from pwncore.config import config

Expand All @@ -12,6 +12,4 @@
router.include_router(ctf.router)
router.include_router(team.router)
router.include_router(leaderboard.router)
router.include_router(admin.router)
# if config.development:
# router.include_router(admin.router)
router.include_router(admin.router) # Admin routes moved to separate app on port 8081
Loading