Skip to content

Commit d78be6b

Browse files
Merge pull request #80 from libkush/master
feat: added staticfs integration
2 parents eafe48f + 820dcec commit d78be6b

File tree

6 files changed

+108
-22
lines changed

6 files changed

+108
-22
lines changed

src/pwncore/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from contextlib import asynccontextmanager
22

3+
import shutil
34
import aiodocker
5+
from logging import getLogger
46
from fastapi import FastAPI
57
from fastapi.middleware.cors import CORSMiddleware
68
from tortoise import Tortoise
@@ -11,6 +13,7 @@
1113
from pwncore.config import config
1214
from pwncore.models import Container
1315

16+
logger = getLogger(__name__)
1417

1518
@asynccontextmanager
1619
async def app_lifespan(app: FastAPI):
@@ -36,6 +39,10 @@ async def app_lifespan(app: FastAPI):
3639
Exception
3740
): # Raises DockerError if container does not exist, just pass for now.
3841
pass
42+
try:
43+
shutil.rmtree(config.staticfs_data_dir)
44+
except Exception as err:
45+
logger.exception("Failed to delete static files", exc_info=err)
3946

4047
# close_connections is deprecated, not sure how to use connections.close_all()
4148
await Tortoise.close_connections()

src/pwncore/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ class Config:
6060
jwt_valid_duration: int
6161
hint_penalty: int
6262
max_members_per_team: int
63+
staticfs_url: str
64+
staticfs_data_dir: str
65+
staticfs_jwt_secret: str
6366
admin_hash: str
6467

6568
config = Config(
@@ -76,6 +79,9 @@ class Config:
7679
msg_codes=msg_codes,
7780
hint_penalty=50,
7881
max_members_per_team=3,
82+
staticfs_url="http://localhost:8080",
83+
staticfs_data_dir=os.environ.get("STATIC_DATA_DIR", "/data"),
84+
staticfs_jwt_secret="PyMioVKFXHymQd+n7q5geOsT6fSYh3gDVw3GqilW+5U="
7985
admin_hash=admin_hash_value,
8086
)
8187

src/pwncore/models/container.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class Container(Model):
2020
)
2121
team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team")
2222
flag = fields.TextField()
23-
23+
24+
token = fields.TextField()
2425
ports: fields.ReverseRelation[Ports]
2526

2627

src/pwncore/models/ctf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Problem(BaseProblem):
3535
# image_config: fields.Field[dict[str, Any]] = fields.JSONField(
3636
# null=True
3737
# ) # type: ignore[assignment]
38+
static = fields.BooleanField(default=False)
3839

3940
mi = fields.IntField(default=50)
4041
ma = fields.IntField(default=500)
@@ -44,7 +45,7 @@ class Problem(BaseProblem):
4445
hints: fields.ReverseRelation[Hint]
4546

4647
class PydanticMeta:
47-
exclude = ["image_name", "mi", "ma", "visible"]
48+
exclude = ["image_name", "static", "mi", "ma", "visible"]
4849

4950
async def _solves(self) -> int:
5051
return await SolvedProblem.filter(problem=self).count()

src/pwncore/routes/ctf/__init__.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from asyncio import create_task
44
from collections import defaultdict
55
from logging import getLogger
6+
import shutil
67

78
from fastapi import APIRouter, Request, Response
89
from pydantic import BaseModel
@@ -124,16 +125,21 @@ async def flag_post(
124125
response.status_code = 500
125126
return {"msg_code": config.msg_codes["db_error"]}
126127

127-
container = await containerASD.docker_client.containers.get(
128-
team_container.docker_id
129-
)
130-
await container.kill()
131-
await container.delete()
132-
#
128+
if problem.static:
129+
shutil.rmtree(
130+
f"{config.staticfs_data_dir}/{team_id}/{team_container.docker_id}"
131+
)
132+
else:
133+
container = await containerASD.docker_client.containers.get(
134+
team_container.docker_id
135+
)
136+
await container.kill()
137+
await container.delete()
133138

134139
await SolvedProblem.create(team_id=team_id, problem_id=ctf_id, penalty=pnlt)
135140
create_task(update_points(req, ctf_id))
136141
return {"status": True}
142+
137143
return {"status": False}
138144

139145

src/pwncore/routes/ctf/start.py

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

3+
import os
34
import uuid
5+
import shutil
46
from logging import getLogger
5-
7+
import jwt as jwtlib
68
from fastapi import APIRouter, Response
79
from tortoise.transactions import in_transaction
810

@@ -43,24 +45,29 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw
4345
if team_container:
4446
a, b = team_container[0], team_container[1:]
4547
db_ports = await a.ports.all().values("port") # Get ports from DB
46-
ports = [db_port["port"]
47-
for db_port in db_ports] # Create a list out of it
48-
48+
ports = [db_port["port"] for db_port in db_ports] # Create a list out of it
49+
static_url = f"{config.staticfs_url}/{a.token}" if ctf.static else None
4950
for db_container in b:
5051
try:
5152
await db_container.delete()
5253
except Exception:
5354
pass
54-
55-
container = await containerASD.docker_client.containers.get(
56-
db_container.docker_id
57-
)
58-
await container.kill()
59-
await container.delete()
55+
# containers won't exist for static ctfs
56+
if ctf.static:
57+
staticLocation = f"{config.staticfs_data_dir}/{team_id}/{db_container.docker_id}"
58+
if os.path.exists(staticLocation):
59+
shutil.rmtree(staticLocation)
60+
else:
61+
container = await containerASD.docker_client.containers.get(
62+
db_container.docker_id
63+
)
64+
await container.kill()
65+
await container.delete()
6066

6167
return {
6268
"msg_code": config.msg_codes["container_already_running"],
6369
"ports": ports,
70+
"static_url": static_url,
6471
"ctf_id": ctf_id,
6572
}
6673

@@ -75,6 +82,53 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw
7582
container_name = f"{team_id}_{ctf_id}_{uuid.uuid4().hex}"
7683
container_flag = f"{config.flag}{{{uuid.uuid4().hex}}}"
7784

85+
if ctf.static:
86+
container_id = uuid.uuid4().hex
87+
payload = {
88+
"id": str(team_id),
89+
"containerId": container_id,
90+
}
91+
token = jwtlib.encode(payload, config.staticfs_jwt_secret, algorithm="HS256")
92+
container = await containerASD.docker_client.containers.run(
93+
name=container_name,
94+
config={
95+
"Image": ctf.image_name,
96+
"AttachStdin": False,
97+
"AttachStdout": False,
98+
"AttachStderr": False,
99+
"Tty": False,
100+
"OpenStdin": False,
101+
"HostConfig": {
102+
"AutoRemove": True,
103+
"Binds": [f"{config.staticfs_data_dir}/{team_id}/{container_id}:/dist"],
104+
},
105+
"Cmd": ["/root/gen_flag", container_flag],
106+
},
107+
)
108+
try:
109+
async with in_transaction():
110+
db_container = await Container.create(
111+
docker_id=container_id,
112+
team_id=team_id,
113+
problem_id=ctf_id,
114+
flag=container_flag,
115+
token=token,
116+
)
117+
except Exception as err:
118+
# Stop the container if failed to make a DB record
119+
await container.kill()
120+
await container.delete()
121+
logger.exception("Error while starting", exc_info=err)
122+
123+
response.status_code = 500
124+
return {"msg_code": config.msg_codes["db_error"]}
125+
return {
126+
"msg_code": config.msg_codes["container_start"],
127+
"ports": [],
128+
"static_url": f"{config.staticfs_url}/{token}",
129+
"ctf_id": ctf_id,
130+
}
131+
78132
# Run
79133
container = await containerASD.docker_client.containers.run(
80134
name=container_name,
@@ -91,9 +145,9 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw
91145
},
92146
)
93147

94-
await (
95-
await container.exec(["/root/gen_flag", container_flag])
96-
).start(detach=True)
148+
await (await container.exec(["/root/gen_flag", container_flag])).start(
149+
detach=True
150+
)
97151

98152
try:
99153
async with in_transaction():
@@ -126,6 +180,7 @@ async def start_docker_container(ctf_id: int, response: Response, jwt: RequireJw
126180
return {
127181
"msg_code": config.msg_codes["container_start"],
128182
"ports": ports,
183+
"static_url": None,
129184
"ctf_id": ctf_id,
130185
}
131186

@@ -144,7 +199,11 @@ async def stopall_docker_container(response: Response, jwt: RequireJwt):
144199
except Exception:
145200
response.status_code = 500
146201
return {"msg_code": config.msg_codes["db_error"]}
147-
202+
203+
team_path = f"{config.staticfs_data_dir}/{team_id}"
204+
if os.path.exists(team_path):
205+
shutil.rmtree(team_path)
206+
148207
for db_container in containers:
149208
container = await containerASD.docker_client.containers.get(
150209
db_container["docker_id"]
@@ -178,6 +237,12 @@ async def stop_docker_container(ctf_id: int, response: Response, jwt: RequireJwt
178237
response.status_code = 500
179238
return {"msg_code": config.msg_codes["db_error"]}
180239

240+
if ctf.static:
241+
shutil.rmtree(
242+
f"{config.staticfs_data_dir}/{team_id}/{team_container.docker_id}"
243+
)
244+
return {"msg_code": config.msg_codes["container_stop"]}
245+
181246
container = await containerASD.docker_client.containers.get(
182247
team_container.docker_id
183248
)

0 commit comments

Comments
 (0)