Revamped to work under docker.
This commit is contained in:
parent
4225cc4dde
commit
b0f0576fd0
20 changed files with 1283 additions and 108 deletions
45
docker-compose.yml
Normal file
45
docker-compose.yml
Normal file
|
@ -0,0 +1,45 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
build: nginx
|
||||
ports:
|
||||
- "8000:80"
|
||||
links:
|
||||
- app
|
||||
- filestore
|
||||
volumes:
|
||||
- filestore:/filestore-data
|
||||
filestore:
|
||||
build: filestore
|
||||
volumes:
|
||||
- filestore:/filestore-data
|
||||
environment:
|
||||
- "UPLOAD_FOLDER=/filestore-data"
|
||||
- "FILESTORE_PORT=80"
|
||||
app:
|
||||
build: server
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
environment:
|
||||
- "SECRET_KEY=${SECRET_KEY}"
|
||||
- "ADMIN_EMAIL=${ADMIN_EMAIL}"
|
||||
- "ENVIRONMENT=${ENVIRONMENT}"
|
||||
- "FLASK_APP=app"
|
||||
- "CACHE_REDIS_HOST=redis"
|
||||
- "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
|
||||
- "MYSQL_HOST=db"
|
||||
- "MYSQL_DATABASE=easyctf"
|
||||
- "FILESTORE_SAVE_ENDPOINT=http://filestore/save"
|
||||
db:
|
||||
image: mariadb:10
|
||||
expose:
|
||||
- 3306
|
||||
environment:
|
||||
- "MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}"
|
||||
- "MYSQL_DATABASE=easyctf"
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
volumes:
|
||||
filestore:
|
9
filestore/Dockerfile
Normal file
9
filestore/Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
FROM python:3.8-alpine3.11
|
||||
|
||||
COPY requirements.txt /
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . /filestore
|
||||
WORKDIR /filestore
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["python", "-m", "flask", "run", "--host", "0.0.0.0", "--port", "80"]
|
4
nginx/Dockerfile
Normal file
4
nginx/Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY easyctf.conf /etc/nginx/easyctf.conf
|
|
@ -1,18 +1,13 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost localhost.easyctf.com;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log error;
|
||||
listen 80 default_server;
|
||||
|
||||
underscores_in_headers on;
|
||||
root /filestore-data;
|
||||
|
||||
location /static {
|
||||
proxy_set_header HOST $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://filestore/;
|
||||
location /static/ {
|
||||
rewrite ^/static/(.*) /$1 break;
|
||||
autoindex off;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
@ -20,6 +15,6 @@ server {
|
|||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://easyctf:5000/;
|
||||
proxy_pass http://app:8000/;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,6 @@ http {
|
|||
gzip_proxied any;
|
||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/easyctf.conf;
|
||||
# include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/easyctf.conf;
|
||||
}
|
||||
|
|
13
server/Dockerfile
Normal file
13
server/Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM python:3.8-alpine3.11
|
||||
|
||||
RUN apk add build-base musl-dev libffi-dev mariadb-dev jpeg-dev
|
||||
|
||||
COPY Pipfile /
|
||||
COPY Pipfile.lock /
|
||||
RUN pip install pipenv
|
||||
RUN pipenv install --deploy --system
|
||||
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
EXPOSE 8000
|
||||
ENTRYPOINT ["pipenv", "run", "/app/entrypoint.sh", "runserver"]
|
49
server/Pipfile
Normal file
49
server/Pipfile
Normal file
|
@ -0,0 +1,49 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
Flask = "*"
|
||||
Flask-Breadcrumbs = "*"
|
||||
Flask-Caching = "*"
|
||||
Flask-Celery-Helper = "*"
|
||||
Flask-Login = "*"
|
||||
Flask-Migrate = "*"
|
||||
Flask-SQLAlchemy = "*"
|
||||
Flask-Script = "*"
|
||||
Flask-WTF = "*"
|
||||
GitPython = "*"
|
||||
Pillow = "*"
|
||||
PyQRCode = "*"
|
||||
PyYAML = "*"
|
||||
SQLAlchemy = "*"
|
||||
WTForms-Components = "*"
|
||||
bcrypt = "*"
|
||||
celery = "*"
|
||||
coverage = "*"
|
||||
cryptography = "*"
|
||||
email-validator = "*"
|
||||
flask-csp = "*"
|
||||
gitdb = "*"
|
||||
markdown2 = "*"
|
||||
mysqlclient = "*"
|
||||
onetimepass = "*"
|
||||
paramiko = "*"
|
||||
passlib = "*"
|
||||
pathlib = "*"
|
||||
pycryptodome = "*"
|
||||
pytest = "*"
|
||||
rauth = "*"
|
||||
raven = {extras = ["flask"], version = "*"}
|
||||
redis = "*"
|
||||
requests = "*"
|
||||
|
||||
[dev-packages]
|
||||
pyls = "*"
|
||||
pyls-mypy = "*"
|
||||
mypy = "*"
|
||||
neovim = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
1063
server/Pipfile.lock
generated
Normal file
1063
server/Pipfile.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,10 +5,11 @@ import socket
|
|||
|
||||
from flask import Flask, request
|
||||
from flask_login import current_user
|
||||
from flask_migrate import Migrate
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__, static_folder="assets", static_path="/assets")
|
||||
app = Flask(__name__, static_folder="assets", static_url_path="/assets")
|
||||
hostname = socket.gethostname()
|
||||
|
||||
if not config:
|
||||
|
@ -30,6 +31,7 @@ def create_app(config=None):
|
|||
app.jinja_env.filters["to_place_str"] = to_place_str
|
||||
|
||||
from easyctf.models import Config
|
||||
Migrate(app, db)
|
||||
|
||||
def get_competition_running():
|
||||
configs = Config.get_many(["start_time", "end_time"])
|
||||
|
|
|
@ -4,20 +4,21 @@ import sys
|
|||
import logging
|
||||
|
||||
import pathlib
|
||||
from werkzeug.contrib.cache import RedisCache
|
||||
|
||||
|
||||
class CTFCache(RedisCache):
|
||||
def dump_object(self, value):
|
||||
value_type = type(value)
|
||||
if value_type in (int, int):
|
||||
return str(value).encode('ascii')
|
||||
return b'!' + pickle.dumps(value, -1)
|
||||
|
||||
|
||||
def cache(app, config, args, kwargs):
|
||||
kwargs["host"] = app.config.get("CACHE_REDIS_HOST", "localhost")
|
||||
return CTFCache(*args, **kwargs)
|
||||
# from werkzeug.contrib.cache import RedisCache
|
||||
#
|
||||
#
|
||||
# class CTFCache(RedisCache):
|
||||
# def dump_object(self, value):
|
||||
# value_type = type(value)
|
||||
# if value_type in (int, int):
|
||||
# return str(value).encode('ascii')
|
||||
# return b'!' + pickle.dumps(value, -1)
|
||||
#
|
||||
#
|
||||
# def cache(app, config, args, kwargs):
|
||||
# kwargs["host"] = app.config.get("CACHE_REDIS_HOST", "localhost")
|
||||
# return CTFCache(*args, **kwargs)
|
||||
|
||||
|
||||
class Config(object):
|
||||
|
@ -34,7 +35,7 @@ class Config(object):
|
|||
self.SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
self.PREFERRED_URL_SCHEME = "https"
|
||||
|
||||
self.CACHE_TYPE = "easyctf.config.cache"
|
||||
self.CACHE_TYPE = "redis" # "easyctf.config.cache"
|
||||
self.CACHE_REDIS_HOST = os.getenv("CACHE_REDIS_HOST", "redis")
|
||||
|
||||
self.ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
||||
|
@ -45,6 +46,7 @@ class Config(object):
|
|||
"FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save")
|
||||
self.FILESTORE_STATIC = os.getenv("FILESTORE_STATIC", "/static")
|
||||
|
||||
self.DISABLE_EMAILS = os.getenv("DISABLE_EMAILS", "" if self.ENVIRONMENT == "production" else "1") != ""
|
||||
self.JUDGE_URL = os.getenv("JUDGE_URL", "http://127.0.0.1/")
|
||||
self.JUDGE_API_KEY = os.getenv("JUDGE_API_KEY", "")
|
||||
self.SHELL_HOST = os.getenv("SHELL_HOST", "")
|
||||
|
@ -78,4 +80,4 @@ class Config(object):
|
|||
url = os.getenv("DATABASE_URL")
|
||||
if url:
|
||||
return url
|
||||
return "mysql://root:%s@db/%s" % (os.getenv("MYSQL_ROOT_PASSWORD"), os.getenv("MYSQL_DATABASE"))
|
||||
return "mysql://root:%s@%s:3306/%s" % (os.getenv("MYSQL_ROOT_PASSWORD"), os.getenv("MYSQL_HOST"), os.getenv("MYSQL_DATABASE"))
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
import base64
|
||||
import imp
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import traceback
|
||||
from io import BytesIO, StringIO
|
||||
from string import Template
|
||||
|
||||
import onetimepass
|
||||
import paramiko
|
||||
import requests
|
||||
import yaml
|
||||
from Crypto.PublicKey import RSA
|
||||
from flask import current_app as app
|
||||
from flask import flash, url_for
|
||||
from flask import url_for
|
||||
from flask_login import current_user
|
||||
from markdown2 import markdown
|
||||
from passlib.hash import bcrypt
|
||||
|
@ -621,12 +618,10 @@ class Team(db.Model):
|
|||
return len(self.members)
|
||||
|
||||
@hybrid_property
|
||||
@cache.memoize(timeout=120)
|
||||
def observer(self):
|
||||
return User.query.filter(and_(User.tid == self.tid, User.level != USER_REGULAR)).count()
|
||||
|
||||
@observer.expression
|
||||
@cache.memoize(timeout=120)
|
||||
def observer(self):
|
||||
return db.session.query(User).filter(User.tid == self.tid and User.level != USER_REGULAR).count()
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% from "templates.html" import render_field, %}
|
||||
{% from "templates.html" import render_field %}
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}New Classroom{% endblock %}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ def to_timestamp(date):
|
|||
|
||||
|
||||
def to_place_str(n):
|
||||
# https://codegolf.stackexchange.com/a/4712
|
||||
k = n % 10
|
||||
return "%d%s" % (n, "tsnrhtdd"[(n / 10 % 10 != 1) * (k < 4) * k::4])
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from string import Template
|
|||
|
||||
import pyqrcode
|
||||
from flask import (Blueprint, abort, flash, redirect, render_template, request,
|
||||
url_for)
|
||||
url_for, current_app)
|
||||
from flask_login import current_user, login_required, login_user, logout_user
|
||||
from sqlalchemy import func
|
||||
|
||||
|
@ -280,8 +280,9 @@ def register_user(name, email, username, password, level, admin=False, **kwargs)
|
|||
for key, value in list(kwargs.items()):
|
||||
setattr(new_user, key, value)
|
||||
code = generate_string()
|
||||
new_user.email_token = code
|
||||
send_verification_email(username, email, url_for("users.verify", code=code, _external=True))
|
||||
if not current_app.config["DISABLE_EMAILS"]:
|
||||
new_user.email_token = code
|
||||
send_verification_email(username, email, url_for("users.verify", code=code, _external=True))
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
return new_user
|
||||
|
|
|
@ -1,22 +1,42 @@
|
|||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
set -e
|
||||
cd /var/easyctf/src
|
||||
PYTHON=python3
|
||||
|
||||
echo "determining bind location..."
|
||||
BIND_PORT=8000
|
||||
BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/private/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0")
|
||||
BIND_ADDR=$(echo $BIND_ADDR_ | xargs)
|
||||
# wait for mysql to be ready
|
||||
LIMIT=30
|
||||
i=0
|
||||
until [ $i -ge $LIMIT ]
|
||||
do
|
||||
nc -z db 3306 && break
|
||||
|
||||
i=$(( i + 1 ))
|
||||
|
||||
echo "$i: Waiting for DB 1 second ..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ $i -eq $LIMIT ]
|
||||
then
|
||||
echo "DB connection refused, terminating ..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# echo "determining bind location..."
|
||||
# BIND_PORT=8000
|
||||
# BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/private/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0")
|
||||
# BIND_ADDR=$(echo $BIND_ADDR_ | xargs)
|
||||
|
||||
echo "starting EasyCTF..."
|
||||
COMMAND=${1:-runserver}
|
||||
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||
WORKERS=${WORKERS:-4}
|
||||
$PYTHON manage.py db upgrade
|
||||
flask db upgrade
|
||||
|
||||
if [ "$COMMAND" == "runserver" ]; then
|
||||
if [ "$ENVIRONMENT" == "development" ]; then
|
||||
$PYTHON manage.py runserver
|
||||
else
|
||||
exec gunicorn --bind="$BIND_ADDR:$BIND_PORT" -w $WORKERS 'easyctf:create_app()'
|
||||
fi
|
||||
flask run --host 0.0.0.0 --port 8000
|
||||
# if [ "$ENVIRONMENT" == "development" ]; then
|
||||
# $PYTHON manage.py runserver
|
||||
# else
|
||||
# exec gunicorn --bind="$BIND_ADDR:$BIND_PORT" -w $WORKERS 'easyctf:create_app()'
|
||||
# fi
|
||||
fi
|
||||
|
|
0
server/migrations/README
Executable file → Normal file
0
server/migrations/README
Executable file → Normal file
43
server/migrations/env.py
Executable file → Normal file
43
server/migrations/env.py
Executable file → Normal file
|
@ -1,8 +1,12 @@
|
|||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
|
@ -18,8 +22,9 @@ logger = logging.getLogger('alembic.env')
|
|||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
|
@ -41,7 +46,9 @@ def run_migrations_offline():
|
|||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
@ -65,21 +72,23 @@ def run_migrations_online():
|
|||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
|
|
0
server/migrations/script.py.mako
Executable file → Normal file
0
server/migrations/script.py.mako
Executable file → Normal file
|
@ -1,8 +1,8 @@
|
|||
"""Initial.
|
||||
"""empty message
|
||||
|
||||
Revision ID: 59f8fa2f0c98
|
||||
Revision ID: c226d7f7ad5a
|
||||
Revises:
|
||||
Create Date: 2018-02-21 04:34:27.788175
|
||||
Create Date: 2020-11-25 21:23:24.378872
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '59f8fa2f0c98'
|
||||
revision = 'c226d7f7ad5a'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
@ -161,7 +161,7 @@ def upgrade():
|
|||
op.create_table('game_states',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('uid', sa.Integer(), nullable=True),
|
||||
sa.Column('last_updated', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('last_updated', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||
sa.Column('state', sa.UnicodeText(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['uid'], ['users.uid'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
|
@ -1,33 +0,0 @@
|
|||
bcrypt
|
||||
celery
|
||||
coverage
|
||||
cryptography==2.1.4
|
||||
flask
|
||||
flask-breadcrumbs
|
||||
flask-caching
|
||||
flask-celery-helper
|
||||
flask-csp
|
||||
flask-login
|
||||
flask-migrate
|
||||
flask-script
|
||||
flask-sqlalchemy
|
||||
flask-wtf
|
||||
gitdb
|
||||
gitpython
|
||||
markdown2
|
||||
mysqlclient
|
||||
onetimepass
|
||||
paramiko
|
||||
passlib
|
||||
pathlib
|
||||
pillow
|
||||
pycryptodome
|
||||
pyqrcode
|
||||
pytest
|
||||
pyyaml
|
||||
rauth
|
||||
raven[flask]
|
||||
redis
|
||||
requests
|
||||
sqlalchemy==1.1.0b3
|
||||
wtforms_components
|
Loading…
Reference in a new issue