Skip to content

Commit 32c3c69

Browse files
Add some CTFd plugin features
* Better dynamic scoring formula (copied from watevrctf) * Add a GCP file uploader option which uploads files to GCP
1 parent db1d80a commit 32c3c69

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__
2+
*.sw?

__init__.py

+19-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import glob
2+
import importlib
3+
import os.path
14
from flask import send_file, jsonify
25
from flask.helpers import safe_join
36
from CTFd.models import Challenges, Solves, Pages, db
@@ -10,15 +13,27 @@
1013
from sqlalchemy.sql import and_
1114

1215
def load(app):
16+
# Load all the subcomponents (which resides in this directory)
17+
this_dir = os.path.abspath(os.path.dirname(__file__))
18+
modules = sorted(glob.glob(this_dir + '/*.py'))
19+
blacklist = {'__pycache__', '__init__.py'}
20+
for mod in modules:
21+
mod = os.path.basename(mod)
22+
if mod in blacklist: continue
23+
mod = '.' + mod[:-3]
24+
mod = importlib.import_module(mod, package=__package__)
25+
mod.load(app)
26+
print(' * pbctf: Loaded subcomponent, %s' % mod)
27+
1328
def nonce():
1429
from flask import session
1530
return session.get('nonce')
1631
app.jinja_env.globals.update(nonce=nonce)
1732

18-
@app.route("/OneSignalSDKWorker.js", methods=["GET"])
19-
def worker():
20-
filename = safe_join(app.root_path, 'themes', 'tsgctf', 'static', 'OneSignalSDKWorker.js')
21-
return send_file(filename)
33+
#@app.route("/OneSignalSDKWorker.js", methods=["GET"])
34+
#def worker():
35+
# filename = safe_join(app.root_path, 'themes', 'tsgctf', 'static', 'OneSignalSDKWorker.js')
36+
# return send_file(filename)
2237

2338
@app.route("/api/v1/dates", methods=["GET"])
2439
def dates():

dynamic.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import math
2+
3+
from CTFd.utils.modes import get_model
4+
from CTFd.models import Solves, db
5+
from CTFd.plugins.dynamic_challenges import DynamicValueChallenge
6+
7+
@classmethod
8+
def calculate_value(cls, challenge):
9+
Model = get_model()
10+
11+
solve_count = (
12+
Solves.query.join(Model, Solves.account_id == Model.id)
13+
.filter(
14+
Solves.challenge_id == challenge.id,
15+
Model.hidden == False,
16+
Model.banned == False,
17+
)
18+
.count()
19+
)
20+
21+
# If the solve count is 0 we shouldn't manipulate the solve count to
22+
# let the math update back to normal
23+
if solve_count != 0:
24+
# We subtract -1 to allow the first solver to get max point value
25+
solve_count -= 1
26+
27+
# It is important that this calculation takes into account floats.
28+
# Hence this file uses from __future__ import division
29+
# Changed in favor of a better decay formula
30+
value = ((challenge.initial - challenge.minimum) /
31+
(1 + solve_count / challenge.decay)) + challenge.minimum
32+
33+
value = math.ceil(value)
34+
35+
if value < challenge.minimum:
36+
value = challenge.minimum
37+
38+
challenge.value = value
39+
db.session.commit()
40+
return challenge
41+
42+
def load(app):
43+
DynamicValueChallenge.calculate_value = calculate_value

gcpuploader.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import os.path
3+
import string
4+
import sys
5+
6+
from flask import current_app, redirect
7+
from google.oauth2 import service_account
8+
from google.cloud import storage
9+
from werkzeug.utils import secure_filename
10+
11+
from CTFd.utils import get_app_config
12+
from CTFd.utils.uploads import UPLOADERS
13+
from CTFd.utils.uploads.uploaders import BaseUploader
14+
from CTFd.utils.encoding import hexencode
15+
16+
# Requires setting GCP_STORE_CRED and GCP_STORE_BUCKET vars
17+
# and GCP_STORE_CRED should be a json file for our private key creds
18+
#
19+
# Should also modify the requirements.in and requiremnts.txt file to have
20+
# google-cloud-storage==1.33.0 dependency
21+
22+
class GCPUploader(BaseUploader):
23+
def __init__(self):
24+
super(BaseUploader, self).__init__()
25+
self.gcp = storage.Client.from_service_account_json(
26+
get_app_config('GCP_STORE_CRED'))
27+
self.bucket = self.gcp.get_bucket(get_app_config("GCP_STORE_BUCKET"))
28+
29+
30+
def _clean_filename(self, c):
31+
if c in string.ascii_letters + string.digits + "-" + "_" + ".":
32+
return True
33+
34+
def store(self, fileobj, filename):
35+
blob = self.bucket.blob(filename)
36+
blob.upload_from_file(fileobj)
37+
blob.make_public()
38+
return filename
39+
40+
def upload(self, file_obj, filename):
41+
filename = filter(
42+
self._clean_filename, secure_filename(filename).replace(" ", "_")
43+
)
44+
filename = "".join(filename)
45+
if len(filename) <= 0:
46+
return False
47+
48+
md5hash = hexencode(os.urandom(16))
49+
50+
dst = md5hash + "/" + filename
51+
return self.store(file_obj, dst)
52+
53+
def download(self, filename):
54+
blob = self.bucket.blob(filename)
55+
return redirect(blob.public_url)
56+
57+
def delete(self, filename):
58+
try:
59+
# Might have an error if it's not there
60+
blob = self.bucket.blob(filename)
61+
blob.delete()
62+
except:
63+
import traceback
64+
traceback.print_exc(file=sys.stdout)
65+
66+
def sync(self):
67+
local_folder = current_app.config.get("UPLOAD_FOLDER")
68+
for blob in self.bucket.list_blobs():
69+
if blob.name.endswith("/") is False:
70+
local_path = os.path.join(local_folder, blob.name)
71+
directory = os.path.dirname(local_path)
72+
if not os.path.exists(directory):
73+
os.makedirs(directory)
74+
75+
blob.download_to_file(local_path)
76+
77+
def load(app):
78+
UPLOADERS['gcp'] = GCPUploader

0 commit comments

Comments
 (0)