Initial.
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
.data
|
||||
.env
|
||||
.vagrant
|
||||
.vscode
|
||||
.digitalocean-token
|
||||
.idea
|
||||
|
||||
__pycache__
|
||||
*.pyc
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
13
Vagrantfile
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/ruby
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = "ubuntu/xenial64"
|
||||
config.vm.network "forwarded_port", guest: 80, host: 7000
|
||||
config.vm.network "forwarded_port", guest: 8000, host: 8000
|
||||
|
||||
config.vm.synced_folder "../problems", "/problems"
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
end
|
||||
|
||||
# config.vm.provision "shell", path: "provision.sh"
|
||||
end
|
7
config.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"resource_prefix": "staging_",
|
||||
"region": "nyc3",
|
||||
"load_balancer": {
|
||||
"name": "load_balancer"
|
||||
}
|
||||
}
|
0
db/Vagrantfile
vendored
Normal file
15
db/conf.d/easyctf.cnf
Executable file
|
@ -0,0 +1,15 @@
|
|||
[client]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysql]
|
||||
default-character-set=utf8mb4
|
||||
|
||||
[mysqld]
|
||||
innodb_buffer_pool_size=20M
|
||||
init_connect='SET collation_connection = utf8_unicode_ci'
|
||||
init_connect='SET NAMES utf8mb4'
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8_unicode_ci
|
||||
skip-character-set-client-handshake
|
||||
max_allowed_packet=512M
|
||||
wait_timeout=31536000
|
1
deploy/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
config.yml
|
1
deploy/config.yml.exmaple
Normal file
|
@ -0,0 +1 @@
|
|||
private_key: "~/.ssh/id_rsa"
|
54
deploy/deploy.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
import paramiko
|
||||
import yaml
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def read_config():
|
||||
with open(os.path.join(os.path.dirname(__file__), "config.yml")) as f:
|
||||
data = yaml.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def get_client():
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
return client
|
||||
|
||||
|
||||
def update_web_server(client, host, pkey):
|
||||
client.connect(host, username="root", pkey=pkey)
|
||||
(sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/easyctf-platform && git reset --hard && git pull origin master && systemctl restart easyctf'")
|
||||
print(sout.read(), serr.read())
|
||||
client.close()
|
||||
|
||||
|
||||
def reimport_problems(client, host, pkey):
|
||||
client.connect(host, username="root", pkey=pkey)
|
||||
(sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/problems && git reset --hard && git pull origin master && cd /root/easyctf-platform/server && dotenv /var/easyctf/env python3 manage.py import /root/problems'")
|
||||
print(sout.read(), serr.read())
|
||||
client.close()
|
||||
|
||||
|
||||
def update_judge(client, host, pkey):
|
||||
client.connect(host, username="root", pkey=pkey)
|
||||
(sin, sout, serr) = client.exec_command("/bin/bash -c 'cd /root/easyctf-platform && git reset --hard && git pull origin master && systemctl restart judge'")
|
||||
print(sout.read(), serr.read())
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
service = None
|
||||
if len(sys.argv) > 1:
|
||||
service = sys.argv[1]
|
||||
config = read_config()
|
||||
key_path = os.path.expanduser(config.get("private_key"))
|
||||
pkey = paramiko.RSAKey.from_private_key_file(key_path)
|
||||
client = get_client()
|
||||
if not service or service == "web":
|
||||
for host in config.get("web"):
|
||||
update_web_server(client, host, pkey)
|
||||
reimport_problems(client, host, pkey)
|
||||
if not service or service == "judge":
|
||||
for host in config.get("judge"):
|
||||
update_judge(client, host, pkey)
|
1
filestore/.gitignore
vendored
Executable file
|
@ -0,0 +1 @@
|
|||
.env
|
53
filestore/app.py
Executable file
|
@ -0,0 +1,53 @@
|
|||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from flask import Flask, abort, request, send_file
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["UPLOAD_FOLDER"] = os.getenv("UPLOAD_FOLDER", "/usr/share/nginx/html")
|
||||
|
||||
if not os.path.exists(app.config["UPLOAD_FOLDER"]):
|
||||
os.makedirs(app.config["UPLOAD_FOLDER"])
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return "You shouldn't be here."
|
||||
|
||||
|
||||
@app.route("/save", methods=["POST"])
|
||||
def save():
|
||||
if "file" not in request.files:
|
||||
return "no file uploaded", 400
|
||||
file = request.files["file"]
|
||||
if file.filename == "":
|
||||
return "no filename found", 400
|
||||
filename = hashlib.sha256(file.read()).hexdigest()
|
||||
file.seek(0)
|
||||
if "filename" in request.form:
|
||||
name, ext = json.loads(request.form["filename"])
|
||||
filename = "%s.%s.%s" % (name, filename, ext)
|
||||
else:
|
||||
if "prefix" in request.form:
|
||||
filename = "%s%s" % (request.form["prefix"], filename)
|
||||
if "suffix" in request.form:
|
||||
filename = "%s%s" % (filename, request.form["suffix"])
|
||||
file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
|
||||
return filename
|
||||
|
||||
|
||||
# This route should be used for debugging filestore locally.
|
||||
@app.route("/static/<string:path>")
|
||||
def serve(path):
|
||||
path = os.path.join(app.config["UPLOAD_FOLDER"], path)
|
||||
if not os.path.exists(path):
|
||||
return abort(404)
|
||||
return send_file(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.warning("Uploading to {}".format(app.config["UPLOAD_FOLDER"]))
|
||||
port = int(os.getenv("FILESTORE_PORT", "8001"))
|
||||
app.run(use_debugger=True, use_reloader=True, port=port, host="0.0.0.0")
|
65
filestore/cloud-provision.sh
Executable file
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
# run this to set up the server
|
||||
# only do this the first time
|
||||
set -e
|
||||
set -o xtrace
|
||||
|
||||
PROJECT_DIRECTORY="/var/filestore/src"
|
||||
PYTHON=python3
|
||||
|
||||
echo "installing system dependencies..."
|
||||
if [ ! -f $HOME/.installdep.filestore.apt ]; then
|
||||
apt-get update && apt-get install -y \
|
||||
git \
|
||||
nginx \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-nose \
|
||||
python3-pip \
|
||||
realpath \
|
||||
systemd
|
||||
touch $HOME/.installdep.filestore.apt
|
||||
fi
|
||||
|
||||
mkdir -p /var/filestore
|
||||
mkdir -p /var/log/filestore
|
||||
|
||||
if [ ! -d $PROJECT_DIRECTORY ]; then
|
||||
b=`realpath $(basename $0)`
|
||||
c=`dirname $b`
|
||||
# cp -r $d $PROJECT_DIRECTORY
|
||||
ln -s $c $PROJECT_DIRECTORY
|
||||
else
|
||||
(cd $PROJECT_DIRECTORY; git pull origin master || true)
|
||||
fi
|
||||
|
||||
mkdir -p /usr/share/nginx/html/static
|
||||
touch /usr/share/nginx/html/static/index.html
|
||||
echo "<!-- silence is golden -->" > /usr/share/nginx/html/static/index.html
|
||||
rm -rf /etc/nginx/conf.d/* /etc/nginx/sites-enabled/*
|
||||
cp $PROJECT_DIRECTORY/default.conf /etc/nginx/sites-enabled/filestore
|
||||
|
||||
service nginx reload
|
||||
service nginx restart
|
||||
|
||||
echo "installing python dependencies..."
|
||||
if [ ! -f $HOME/.installdep.filestore.pip ]; then
|
||||
$PYTHON -m pip install -U pip
|
||||
$PYTHON -m pip install gunicorn
|
||||
$PYTHON -m pip install -r $PROJECT_DIRECTORY/requirements.txt
|
||||
touch $HOME/.installdep.filestore.pip
|
||||
fi
|
||||
|
||||
# dirty hack
|
||||
KILL=/bin/kill
|
||||
eval "echo \"$(< $PROJECT_DIRECTORY/systemd/filestore.service)\"" > /etc/systemd/system/filestore.service
|
||||
|
||||
echo "Filestore has been deployed!"
|
||||
echo "Modify the env file at /var/filestore/env."
|
||||
echo "Then run"
|
||||
echo
|
||||
echo "systemctl --system daemon-reload && systemctl restart filestore"
|
||||
echo "gucci gang"
|
||||
|
||||
cp env.example /var/filestore/env
|
||||
systemctl --system daemon-reload && systemctl restart filestore
|
9
filestore/default.conf
Executable file
|
@ -0,0 +1,9 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.html;
|
||||
}
|
||||
}
|
21
filestore/entrypoint.sh
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
cd /var/filestore/src
|
||||
|
||||
PYTHON=/usr/bin/python3
|
||||
|
||||
echo "determining bind location..."
|
||||
BIND_PORT=${FILESTORE_PORT:-8000}
|
||||
PRIVATE_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")
|
||||
PRIVATE_BIND_ADDR=$(echo $BIND_ADDR_ | xargs)
|
||||
PUBLIC_BIND_ADDR_=$(curl -w "\n" http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address --connect-timeout 2 || printf "0.0.0.0")
|
||||
PUBLIC_BIND_ADDR=$(echo $BIND_ADDR_ | xargs)
|
||||
|
||||
WORKERS=${WORKERS:-4}
|
||||
ENVIRONMENT=${ENVIRONMENT:-production}
|
||||
service nginx start
|
||||
if [ "$ENVIRONMENT" == "development" ]; then
|
||||
$PYTHON app.py
|
||||
else
|
||||
exec gunicorn --bind="$PRIVATE_BIND_ADDR:$BIND_PORT" --bind="$PUBLIC_BIND_ADDR:$BIND_PORT" -w $WORKERS app:app
|
||||
fi
|
2
filestore/env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
UPLOAD_FOLDER=/usr/share/nginx/html
|
||||
FILESTORE_PORT=8001
|
2
filestore/requirements.txt
Executable file
|
@ -0,0 +1,2 @@
|
|||
flask
|
||||
gunicorn
|
16
filestore/systemd/filestore.service
Normal file
|
@ -0,0 +1,16 @@
|
|||
[Unit]
|
||||
Description=easyctf static file server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/var/filestore/env
|
||||
PIDFile=/run/filestore/pid
|
||||
User=root
|
||||
WorkingDirectory=$PROJECT_DIRECTORY
|
||||
ExecStart=$PROJECT_DIRECTORY/entrypoint.sh
|
||||
ExecReload=$KILL -s HUP \$MAINPID
|
||||
ExecStop=$KILL -s TERM \$MAINPID
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
10
judge/.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
build
|
||||
# ideally these should all be in a subfolder
|
||||
generator.py
|
||||
grader.py
|
||||
program.py
|
||||
case_number
|
||||
input
|
||||
report
|
||||
error
|
||||
.idea
|
50
judge/api.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from languages import languages, Python3
|
||||
from models import Job, Problem
|
||||
|
||||
|
||||
class API(object):
|
||||
def __init__(self, key, base_url):
|
||||
self.key = key
|
||||
self.base_url = base_url
|
||||
|
||||
def api_call(self, url, method="GET", data=None, headers=None):
|
||||
if headers is None:
|
||||
headers = dict()
|
||||
headers.update({"API-Key": self.key})
|
||||
r = requests.request(method, url, data=data, headers=headers)
|
||||
return r
|
||||
|
||||
def claim(self):
|
||||
r = self.api_call(self.base_url + "/jobs")
|
||||
print("text:", repr(r.text))
|
||||
if not r.text:
|
||||
return None
|
||||
required_fields = ["id", "language", "source", "pid", "test_cases", "time_limit", "memory_limit", "generator_code", "grader_code", "source_verifier_code"]
|
||||
# create job object
|
||||
obj = r.json()
|
||||
if not all(field in obj for field in required_fields):
|
||||
return None
|
||||
problem = Problem(obj["pid"], obj["test_cases"], obj["time_limit"], obj["memory_limit"],
|
||||
obj["generator_code"], Python3,
|
||||
obj["grader_code"], Python3,
|
||||
obj["source_verifier_code"], Python3)
|
||||
language = languages.get(obj["language"])
|
||||
if not language:
|
||||
return None # TODO: should definitely not do this
|
||||
return Job(obj["id"], problem, obj["source"], language)
|
||||
|
||||
def submit(self, result):
|
||||
verdict = result.verdict
|
||||
data = dict(
|
||||
id=result.job.id,
|
||||
verdict=result.verdict.value if verdict else "JE",
|
||||
last_ran_case=result.last_ran_case,
|
||||
execution_time=result.execution_time,
|
||||
execution_memory=result.execution_memory
|
||||
)
|
||||
r = self.api_call(self.base_url + "/jobs", method="POST", data=data)
|
||||
return r.status_code // 100 == 2
|
55
judge/cloud-provision.sh
Executable file
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
declare API_KEY=$1
|
||||
declare JUDGE_URL=$2
|
||||
|
||||
if [ ! $API_KEY ]; then
|
||||
echo "please provide a key."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_DIRECTORY="/var/judge/src"
|
||||
PYTHON=$(which python3)
|
||||
mkdir -p /var/judge
|
||||
mkdir -p /var/log/judge
|
||||
|
||||
echo "installing system dependencies..."
|
||||
if [ ! -f $HOME/.installdep.judge.apt ]; then
|
||||
apt-get update && apt-get install -y software-properties-common && \
|
||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 5BB92C09DB82666C && \
|
||||
add-apt-repository -y ppa:fkrull/deadsnakes && \
|
||||
add-apt-repository -y ppa:openjdk-r/ppa && \
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
openjdk-7-jdk \
|
||||
pkg-config \
|
||||
python2.7 \
|
||||
python3.5 \
|
||||
python3 \
|
||||
python3-pip
|
||||
touch $HOME/.installdep.judge.apt
|
||||
fi
|
||||
|
||||
if [ ! -d $PROJECT_DIRECTORY ]; then
|
||||
b=`realpath $(basename $0)`
|
||||
c=`dirname $b`
|
||||
d=`dirname $c`
|
||||
ln -s $c $PROJECT_DIRECTORY
|
||||
else
|
||||
(cd $PROJECT_DIRECTORY; git pull origin master || true)
|
||||
fi
|
||||
|
||||
echo "installing python dependencies..."
|
||||
if [ ! -f $HOME/.installdep.judge.pip ]; then
|
||||
$PYTHON -m pip install -U pip
|
||||
$PYTHON -m pip install requests
|
||||
touch $HOME/.installdep.judge.pip
|
||||
fi
|
||||
|
||||
# dirty hack
|
||||
echo "writing systemd entry..."
|
||||
PYTHON=$(which python3)
|
||||
eval "echo \"$(< $PROJECT_DIRECTORY/systemd/judge.service)\"" > /etc/systemd/system/judge.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable judge
|
||||
systemctl start judge
|
13
judge/config.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import os
|
||||
import pathlib
|
||||
from typing import Dict
|
||||
|
||||
|
||||
APP_ROOT = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
CONFINE_PATH = APP_ROOT / 'confine'
|
||||
|
||||
COMPILATION_TIME_LIMIT = 10
|
||||
GRADER_TIME_LIMIT = 10
|
||||
|
||||
PARTIAL_JOB_SUBMIT_TIME_THRESHOLD = 2 # Seconds
|
||||
PARTIAL_JOB_SUBMIT_CASES_THRESHOLD = 10
|
BIN
judge/confine
Executable file
300
judge/executor.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import Iterator
|
||||
|
||||
import config
|
||||
from languages import Language
|
||||
from models import ExecutionResult, Job, JobVerdict, Problem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
logging.info('Starting up')
|
||||
|
||||
verdict_map = {
|
||||
'InternalError': JobVerdict.judge_error,
|
||||
'RuntimeError': JobVerdict.runtime_error,
|
||||
'TimeLimitExceeded': JobVerdict.time_limit_exceeded,
|
||||
'MemoryLimitExceeded': JobVerdict.memory_limit_exceeded,
|
||||
'IllegalSyscall': JobVerdict.illegal_syscall,
|
||||
'IllegalOpen': JobVerdict.illegal_syscall,
|
||||
'IllegalWrite': JobVerdict.illegal_syscall,
|
||||
}
|
||||
|
||||
|
||||
class ExecutionReport:
|
||||
def __init__(self, execution_ok: bool, execution_error_code: JobVerdict, exitcode: int, realtime: float,
|
||||
cputime: float,
|
||||
memory: int):
|
||||
self.execution_ok = execution_ok
|
||||
self.execution_error_code = execution_error_code
|
||||
self.exitcode = exitcode
|
||||
self.realtime = realtime
|
||||
self.cputime = cputime
|
||||
self.memory = memory
|
||||
|
||||
@classmethod
|
||||
def error_report(cls):
|
||||
return cls(
|
||||
execution_ok=False,
|
||||
execution_error_code=JobVerdict.judge_error,
|
||||
exitcode=-1,
|
||||
realtime=0,
|
||||
cputime=0,
|
||||
memory=0,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_string: str):
|
||||
try:
|
||||
obj = json.loads(json_string)
|
||||
return cls(
|
||||
execution_ok=obj['execution_ok'],
|
||||
execution_error_code=verdict_map[obj['execution_error_code']['code']] if obj['execution_error_code'] else None,
|
||||
exitcode=obj['exitcode'],
|
||||
realtime=obj['realtime'],
|
||||
cputime=obj['cputime'],
|
||||
memory=obj['memory'],
|
||||
)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
logger.error('Failed to load execution report from json!')
|
||||
return cls.error_report()
|
||||
|
||||
|
||||
class ExecutionProfile:
|
||||
def __init__(self, confine_path: str, problem: Problem, language: Language, workdir: str,
|
||||
input_file='input', output_file='output', error_file='error', report_file='report'):
|
||||
self.confine_path = confine_path
|
||||
self.language = language
|
||||
self.workdir = workdir
|
||||
self.time_limit = problem.time_limit
|
||||
self.memory_limit = problem.memory_limit
|
||||
self.input_file = os.path.join(workdir, input_file)
|
||||
self.output_file = os.path.join(workdir, output_file)
|
||||
self.error_file = os.path.join(workdir, error_file)
|
||||
self.report_file = os.path.join(workdir, report_file)
|
||||
|
||||
def as_json(self, executable_name: str):
|
||||
return json.dumps({
|
||||
'cputime_limit': self.time_limit,
|
||||
'realtime_limit': self.time_limit * 1000,
|
||||
'allowed_files': self.language.get_allowed_files(self.workdir, executable_name),
|
||||
'allowed_prefixes': self.language.get_allowed_file_prefixes(self.workdir, executable_name),
|
||||
'stdin_file': self.input_file,
|
||||
'stdout_file': self.output_file,
|
||||
'stderr_file': self.error_file,
|
||||
'json_report_file': self.report_file,
|
||||
})
|
||||
|
||||
def execute(self, executable_name: str) -> ExecutionReport:
|
||||
return Executor(self).execute(executable_name)
|
||||
|
||||
|
||||
class Executor:
|
||||
def __init__(self, profile: ExecutionProfile):
|
||||
self.profile = profile
|
||||
|
||||
def execute(self, executable_name: str) -> ExecutionReport:
|
||||
command = self.profile.language.get_command(self.profile.workdir, executable_name)
|
||||
|
||||
config_file_path = os.path.join(self.profile.workdir, 'confine.json')
|
||||
with open(config_file_path, 'w') as config_file:
|
||||
config_file.write(self.profile.as_json(executable_name))
|
||||
|
||||
try:
|
||||
subprocess.check_call([self.profile.confine_path, '-c', config_file_path, '--', *command],
|
||||
timeout=self.profile.time_limit * 2)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
return ExecutionReport.error_report()
|
||||
|
||||
with open(os.path.join(self.profile.workdir, self.profile.report_file)) as report_file:
|
||||
execution_report = ExecutionReport.from_json(report_file.read())
|
||||
return execution_report
|
||||
|
||||
|
||||
def use_tempdir(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
before_dir = os.getcwd()
|
||||
tempdir = tempfile.mkdtemp(prefix='jury-')
|
||||
os.chdir(tempdir)
|
||||
|
||||
result = func(*args, tempdir=tempdir, **kwargs)
|
||||
|
||||
os.chdir(before_dir)
|
||||
# shutil.rmtree(tempdir)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# @use_tempdir
|
||||
def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]:
|
||||
result = ExecutionResult(
|
||||
job=job,
|
||||
verdict=JobVerdict.judge_error,
|
||||
last_ran_case=0,
|
||||
execution_time=0,
|
||||
execution_memory=0,
|
||||
)
|
||||
|
||||
if job.problem.source_verifier_code and job.problem.source_verifier_language:
|
||||
source_verifier_executable = job.problem.source_verifier_language.compile(job.problem.source_verifier_code, tempdir,
|
||||
'source_verifier')
|
||||
if not source_verifier_executable:
|
||||
logger.error('Source verifier failed to compile for problem %d' % job.problem.id)
|
||||
result.verdict = JobVerdict.judge_error
|
||||
yield result
|
||||
return
|
||||
|
||||
with open(os.path.join(tempdir, 'source'), 'wb') as source_file:
|
||||
source_file.write(job.code.encode('utf-8'))
|
||||
|
||||
execution_profile = ExecutionProfile(
|
||||
confine_path=str(config.CONFINE_PATH),
|
||||
problem=job.problem,
|
||||
language=job.problem.source_verifier_language,
|
||||
workdir=tempdir,
|
||||
input_file='source',
|
||||
output_file='source_verifier_result',
|
||||
)
|
||||
|
||||
execution_result = execution_profile.execute(source_verifier_executable)
|
||||
|
||||
if not execution_result.execution_ok:
|
||||
result.verdict = JobVerdict.judge_error
|
||||
yield result
|
||||
return
|
||||
|
||||
with open(os.path.join(tempdir, 'source_verifier_result')) as source_verifier_result_file:
|
||||
if source_verifier_result_file.read().strip() != 'OK':
|
||||
result.verdict = JobVerdict.invalid_source
|
||||
yield result
|
||||
return
|
||||
|
||||
program_executable = job.language.compile(job.code, tempdir, 'program')
|
||||
if not program_executable:
|
||||
result.verdict = JobVerdict.compilation_error
|
||||
yield result
|
||||
return
|
||||
|
||||
generator_executable = job.problem.generator_language.compile(job.problem.generator_code, tempdir, 'generator')
|
||||
if not generator_executable:
|
||||
logger.error('Generator failed to compile for problem %d' % job.problem.id)
|
||||
result.verdict = JobVerdict.judge_error
|
||||
yield result
|
||||
return
|
||||
|
||||
grader_executable = job.problem.grader_language.compile(job.problem.grader_code, tempdir, 'grader')
|
||||
if not grader_executable:
|
||||
logger.error('Grader failed to compile for problem %d' % job.problem.id)
|
||||
result.verdict = JobVerdict.judge_error
|
||||
yield result
|
||||
return
|
||||
|
||||
result.verdict = None
|
||||
last_submitted_time = time.time()
|
||||
last_submitted_case = 0
|
||||
for case_number in range(1, job.problem.test_cases + 1):
|
||||
result.last_ran_case = case_number
|
||||
case_result = run_test_case(job, case_number, tempdir, program_executable, generator_executable, grader_executable)
|
||||
if case_result.verdict != JobVerdict.accepted:
|
||||
result = case_result
|
||||
break
|
||||
result.execution_time = max(result.execution_time, case_result.execution_time)
|
||||
result.execution_memory = max(result.execution_memory, case_result.execution_memory)
|
||||
|
||||
# Yield result if over threshold and is not last case
|
||||
# If verdict calculation takes time, result should be changed to yield even if is last case.
|
||||
if (time.time() - last_submitted_time > config.PARTIAL_JOB_SUBMIT_TIME_THRESHOLD or
|
||||
case_number - last_submitted_case > config.PARTIAL_JOB_SUBMIT_CASES_THRESHOLD) and \
|
||||
case_number != job.problem.test_cases + 1:
|
||||
yield result
|
||||
|
||||
# We want to let the programs run for `threshold` time before another potential pause
|
||||
last_submitted_time = time.time()
|
||||
last_submitted_case = case_number
|
||||
|
||||
if not result.verdict:
|
||||
result.verdict = JobVerdict.accepted
|
||||
|
||||
yield result
|
||||
|
||||
|
||||
def run_test_case(job: Job, case_number: int, workdir: str, program_executable: str, generator_executable: str,
|
||||
grader_executable: str) -> ExecutionResult:
|
||||
result = ExecutionResult(
|
||||
job=job,
|
||||
verdict=JobVerdict.judge_error,
|
||||
last_ran_case=case_number,
|
||||
execution_time=0,
|
||||
execution_memory=0,
|
||||
)
|
||||
|
||||
with open(os.path.join(workdir, 'case_number'), 'wb') as case_number_file:
|
||||
case_number_file.write(str(case_number).encode('utf-8'))
|
||||
|
||||
generator_execution_profile = ExecutionProfile(
|
||||
confine_path=str(config.CONFINE_PATH),
|
||||
problem=job.problem,
|
||||
language=job.problem.generator_language,
|
||||
workdir=workdir,
|
||||
input_file='case_number',
|
||||
output_file='input',
|
||||
)
|
||||
generator_result = generator_execution_profile.execute(generator_executable)
|
||||
|
||||
if not generator_result.execution_ok:
|
||||
logger.error('Generator failed for test case %d of problem %d with error %s' %
|
||||
(case_number, job.problem.id, generator_result.execution_error_code))
|
||||
return result
|
||||
|
||||
program_execution_profile = ExecutionProfile(
|
||||
confine_path=str(config.CONFINE_PATH),
|
||||
problem=job.problem,
|
||||
language=job.language,
|
||||
workdir=workdir,
|
||||
input_file='input',
|
||||
output_file='program_output',
|
||||
error_file='program_error',
|
||||
)
|
||||
execution_result = program_execution_profile.execute(program_executable)
|
||||
|
||||
result.execution_time = execution_result.realtime
|
||||
result.execution_memory = execution_result.memory
|
||||
if not execution_result.execution_ok:
|
||||
result.verdict = execution_result.execution_error_code
|
||||
return result
|
||||
|
||||
grader_execution_profile = ExecutionProfile(
|
||||
confine_path=str(config.CONFINE_PATH),
|
||||
problem=job.problem,
|
||||
language=job.problem.grader_language,
|
||||
workdir=workdir,
|
||||
input_file='input',
|
||||
output_file='grader_output',
|
||||
error_file='grader_error',
|
||||
)
|
||||
grader_result = grader_execution_profile.execute(grader_executable)
|
||||
|
||||
if not grader_result.execution_ok:
|
||||
logger.error('Grader failed for test case %d of problem %d with error %s' %
|
||||
(case_number, job.problem.id, grader_result.execution_error_code))
|
||||
result.verdict = JobVerdict.judge_error
|
||||
return result
|
||||
|
||||
with open(os.path.join(workdir, 'program_output'), 'rb') as program_output, \
|
||||
open(os.path.join(workdir, 'grader_output'), 'rb') as grader_output:
|
||||
if program_output.read().strip() == grader_output.read().strip():
|
||||
final_verdict = JobVerdict.accepted
|
||||
else:
|
||||
final_verdict = JobVerdict.wrong_answer
|
||||
|
||||
result.verdict = final_verdict
|
||||
|
||||
return result
|
76
judge/judge.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
import executor
|
||||
from api import API
|
||||
from models import Job
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
logging.info('Starting up')
|
||||
|
||||
|
||||
api = None
|
||||
judge_url = None
|
||||
current_job = None # type: Job
|
||||
|
||||
|
||||
def loop():
|
||||
global current_job
|
||||
job = api.claim()
|
||||
current_job = job
|
||||
if not job:
|
||||
logger.debug('No jobs available.')
|
||||
return False
|
||||
logger.info('Got job %d.', job.id)
|
||||
|
||||
tempdir = tempfile.mkdtemp(prefix='jury-')
|
||||
try:
|
||||
for execution_result in executor.run_job(job, tempdir):
|
||||
# execution_result is partial here
|
||||
|
||||
logger.info('Job %d partially judged; case: %d, time: %.2f, memory: %d',
|
||||
job.id, execution_result.last_ran_case, execution_result.execution_time,
|
||||
execution_result.execution_memory)
|
||||
|
||||
if execution_result.verdict:
|
||||
# This should be the last value returned by run_job
|
||||
logger.info('Job %d finished with verdict %s.' % (job.id, execution_result.verdict.value))
|
||||
|
||||
if api.submit(execution_result):
|
||||
logger.info('Job %d successfully partially submitted.' % job.id)
|
||||
else:
|
||||
logger.info('Job %d failed to partially submit.' % job.id)
|
||||
except:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
shutil.rmtree(tempdir, ignore_errors=True)
|
||||
finally:
|
||||
shutil.rmtree(tempdir, ignore_errors=True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
api_key = os.getenv("API_KEY")
|
||||
if not api_key:
|
||||
print("no api key", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
judge_url = os.getenv("JUDGE_URL")
|
||||
if not judge_url:
|
||||
print("no judge url", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
api = API(api_key, judge_url)
|
||||
while True:
|
||||
try:
|
||||
if not loop():
|
||||
time.sleep(3)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
except:
|
||||
traceback.print_exc(file=sys.stderr)
|
157
judge/languages.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
from abc import ABCMeta
|
||||
from typing import List, Dict
|
||||
|
||||
import config
|
||||
from models import JobVerdict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
logging.info('Starting up')
|
||||
|
||||
|
||||
class Language(metaclass=ABCMeta):
|
||||
@classmethod
|
||||
def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_command(cls, workdir: str, executable_name: str) -> List[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_allowed_files(cls, workdir: str, executable_name: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_allowed_file_prefixes(cls, workdir: str, executable_name: str):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CXX(Language):
|
||||
@classmethod
|
||||
def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str:
|
||||
source_file_path = os.path.join(workdir, 'source.cpp')
|
||||
with open(source_file_path, 'wb') as source_file:
|
||||
source_file.write(source_code.encode('utf-8'))
|
||||
|
||||
executable_file_path = os.path.join(workdir, executable_name)
|
||||
try:
|
||||
subprocess.check_call(['g++', '--std=c++1y', '-o', executable_file_path, source_file_path],
|
||||
timeout=config.COMPILATION_TIME_LIMIT)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
||||
return executable_name
|
||||
|
||||
@classmethod
|
||||
def get_command(cls, workdir: str, executable_name: str) -> List[str]:
|
||||
return [os.path.join(workdir, executable_name)]
|
||||
|
||||
@classmethod
|
||||
def get_allowed_files(cls, workdir: str, executable_name: str):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_allowed_file_prefixes(cls, workdir: str, executable_name: str):
|
||||
return []
|
||||
|
||||
|
||||
class Python(Language):
|
||||
language_name = 'python'
|
||||
interpreter_name = 'python'
|
||||
|
||||
@classmethod
|
||||
def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str:
|
||||
executable_name += '.py'
|
||||
executable_path = os.path.join(workdir, executable_name)
|
||||
with open(executable_path, 'wb') as executable_file:
|
||||
executable_file.write(source_code.encode('utf-8'))
|
||||
|
||||
"""try:
|
||||
subprocess.check_call([cls.interpreter_name, '-m', 'py_compile', executable_name],
|
||||
timeout=config.COMPILATION_TIME_LIMIT)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
return None"""
|
||||
|
||||
return executable_name
|
||||
|
||||
@classmethod
|
||||
def get_command(cls, workdir: str, executable_name: str) -> List[str]:
|
||||
return [os.path.join('/usr/bin', cls.interpreter_name), '-s', '-S', os.path.join(workdir, executable_name)]
|
||||
|
||||
@classmethod
|
||||
def get_allowed_files(cls, workdir: str, executable_name: str):
|
||||
return [
|
||||
'/etc/nsswitch.conf',
|
||||
'/etc/passwd',
|
||||
'/dev/urandom', # TODO: come up with random policy
|
||||
'/tmp',
|
||||
'/bin/Modules/Setup',
|
||||
workdir,
|
||||
os.path.join(workdir, executable_name),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_allowed_file_prefixes(cls, workdir: str, executable_name: str):
|
||||
return []
|
||||
|
||||
|
||||
class Java(Language):
|
||||
@classmethod
|
||||
def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str:
|
||||
source_file_path = os.path.join(workdir, 'Main.java')
|
||||
with open(source_file_path, 'wb') as source_file:
|
||||
source_file.write(source_code.encode('utf-8'))
|
||||
|
||||
executable_file_path = os.path.join(workdir, 'Main')
|
||||
try:
|
||||
subprocess.check_call(['javac', '-d', workdir, source_file_path],
|
||||
timeout=config.COMPILATION_TIME_LIMIT)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
||||
return 'Main'
|
||||
|
||||
@classmethod
|
||||
def get_command(cls, workdir: str, executable_name: str) -> List[str]:
|
||||
return ['/usr/bin/java', '-XX:-UsePerfData', '-XX:+DisableAttachMechanism', '-Xmx256m', '-Xrs', '-cp',
|
||||
workdir, executable_name]
|
||||
|
||||
@classmethod
|
||||
def get_allowed_files(cls, workdir: str, executable_name: str):
|
||||
return [
|
||||
'/etc/nsswitch.conf',
|
||||
'/etc/passwd',
|
||||
'/tmp',
|
||||
workdir,
|
||||
os.path.join(workdir, executable_name + '.class'),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_allowed_file_prefixes(cls, workdir: str, executable_name: str):
|
||||
return [
|
||||
'/etc/java-7-openjdk/',
|
||||
'/tmp/.java_pid',
|
||||
'/tmp/',
|
||||
]
|
||||
|
||||
|
||||
class Python2(Python):
|
||||
language_name = 'python2'
|
||||
interpreter_name = 'python2.7'
|
||||
|
||||
|
||||
class Python3(Python):
|
||||
language_name = 'python3'
|
||||
interpreter_name = 'python3.5'
|
||||
|
||||
|
||||
languages = {
|
||||
'cxx': CXX,
|
||||
'python2': Python2,
|
||||
'python3': Python3,
|
||||
'java': Java,
|
||||
} # type: Dict[str, Language]
|
47
judge/models.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
import enum
|
||||
|
||||
|
||||
class Problem:
|
||||
def __init__(self, id: int, test_cases: int, time_limit: float, memory_limit: int,
|
||||
generator_code: str, generator_language, grader_code: str, grader_language,
|
||||
source_verifier_code: str=None, source_verifier_language=None):
|
||||
self.id = id
|
||||
self.test_cases = test_cases
|
||||
self.time_limit = time_limit
|
||||
self.memory_limit = memory_limit
|
||||
self.generator_code = generator_code
|
||||
self.generator_language = generator_language
|
||||
self.grader_code = grader_code
|
||||
self.grader_language = grader_language
|
||||
self.source_verifier_code = source_verifier_code
|
||||
self.source_verifier_language = source_verifier_language
|
||||
|
||||
|
||||
class Job:
|
||||
def __init__(self, id: int, problem: Problem, code: str, language):
|
||||
self.id = id
|
||||
self.problem = problem
|
||||
self.code = code
|
||||
self.language = language
|
||||
|
||||
|
||||
class JobVerdict(enum.Enum):
|
||||
accepted = 'AC'
|
||||
ran = 'RAN'
|
||||
invalid_source = 'IS'
|
||||
wrong_answer = 'WA'
|
||||
time_limit_exceeded = 'TLE'
|
||||
memory_limit_exceeded = 'MLE'
|
||||
runtime_error = 'RTE'
|
||||
illegal_syscall = 'ISC'
|
||||
compilation_error = 'CE'
|
||||
judge_error = 'JE'
|
||||
|
||||
|
||||
class ExecutionResult:
|
||||
def __init__(self, job: Job, verdict: JobVerdict, last_ran_case: int, execution_time: float, execution_memory: int):
|
||||
self.job = job
|
||||
self.verdict = verdict
|
||||
self.last_ran_case = last_ran_case
|
||||
self.execution_time = execution_time
|
||||
self.execution_memory = execution_memory
|
BIN
judge/nsjail
Executable file
0
judge/output
Normal file
12
judge/systemd/judge.service
Normal file
|
@ -0,0 +1,12 @@
|
|||
[Unit]
|
||||
Description=indepedent judging unit
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
Environment=\"API_KEY=$API_KEY\"
|
||||
Environment=\"JUDGE_URL=$JUDGE_URL\"
|
||||
ExecStart=$PYTHON $PROJECT_DIRECTORY/judge.py
|
||||
ExecStop=:
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
25
nginx/easyctf.conf
Executable file
|
@ -0,0 +1,25 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost localhost.easyctf.com;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log error;
|
||||
|
||||
underscores_in_headers on;
|
||||
|
||||
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 / {
|
||||
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://easyctf:5000/;
|
||||
}
|
||||
}
|
22
nginx/easyctf.vagrant.conf
Executable file
|
@ -0,0 +1,22 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost localhost.easyctf.com;
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log error;
|
||||
|
||||
underscores_in_headers on;
|
||||
|
||||
location /static {
|
||||
root /var/opt/filestore;
|
||||
autoindex off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header HOST $http_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://localhost:5000/;
|
||||
}
|
||||
}
|
32
nginx/nginx.conf
Executable file
|
@ -0,0 +1,32 @@
|
|||
# user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile off;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
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;
|
||||
}
|
27
provision.sh
Executable file
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
git \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libmysqlclient-dev \
|
||||
libpng-dev \
|
||||
libssl-dev \
|
||||
mysql-client \
|
||||
mysql-server \
|
||||
nginx \
|
||||
openssh-client \
|
||||
pkg-config \
|
||||
python2.7 \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-nose \
|
||||
python3-pip \
|
||||
realpath \
|
||||
redis-server \
|
||||
systemd \
|
||||
|
||||
(cd server; ./cloud-provision.sh)
|
||||
|
||||
(cd filestore; ./cloud-provision.sh)
|
4
server/.gitignore
vendored
Executable file
|
@ -0,0 +1,4 @@
|
|||
__pycache__
|
||||
*.pyc
|
||||
easyctf.db
|
||||
secret.sh
|
3
server/app.py
Executable file
|
@ -0,0 +1,3 @@
|
|||
from easyctf import create_app
|
||||
|
||||
app = create_app()
|
65
server/cloud-provision.sh
Executable file
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
# run this to set up the server
|
||||
# only do this the first time
|
||||
set -e
|
||||
# set -o xtrace
|
||||
|
||||
REPOSITORY="git@github.com:iptq/easyctf-platform.git"
|
||||
PROJECT_DIRECTORY="/var/easyctf/src"
|
||||
PYTHON=python3
|
||||
|
||||
echo "installing system dependencies..."
|
||||
if [ ! -f $HOME/.installdep.server.apt ]; then
|
||||
apt-get update && apt-get install -y \
|
||||
git \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libmysqlclient-dev \
|
||||
libpng-dev \
|
||||
libssl-dev \
|
||||
mysql-client \
|
||||
openssh-client \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-nose \
|
||||
python3-pip \
|
||||
realpath \
|
||||
systemd
|
||||
touch $HOME/.installdep.server.apt
|
||||
fi
|
||||
|
||||
mkdir -p /var/easyctf
|
||||
mkdir -p /var/log/easyctf
|
||||
|
||||
if [ ! -d $PROJECT_DIRECTORY ]; then
|
||||
# why the fuck shoul i clone if i already hav this file LMAO
|
||||
b=`realpath $(basename $0)`
|
||||
c=`dirname $b`
|
||||
d=`dirname $c`
|
||||
# cp -r $d $PROJECT_DIRECTORY
|
||||
ln -s $c $PROJECT_DIRECTORY
|
||||
else
|
||||
(cd $PROJECT_DIRECTORY; git pull origin master || true)
|
||||
fi
|
||||
|
||||
echo "installing python dependencies..."
|
||||
if [ ! -f $HOME/.installdep.server.pip ]; then
|
||||
$PYTHON -m pip install -U pip
|
||||
$PYTHON -m pip install gunicorn
|
||||
$PYTHON -m pip install -r $PROJECT_DIRECTORY/requirements.txt
|
||||
touch $HOME/.installdep.server.pip
|
||||
fi
|
||||
|
||||
# dirty hack
|
||||
KILL=/bin/kill
|
||||
eval "echo \"$(< $PROJECT_DIRECTORY/systemd/easyctf.service)\"" > /etc/systemd/system/easyctf.service
|
||||
|
||||
echo "EasyCTF has been deployed!"
|
||||
echo "Modify the env file at /var/easyctf/env."
|
||||
echo "Then run"
|
||||
echo
|
||||
echo "systemctl --system daemon-reload && systemctl restart easyctf"
|
||||
echo "gucci gang"
|
||||
|
||||
cp env.example /var/easyctf/env
|
||||
systemctl --system daemon-reload && systemctl restart easyctf
|
86
server/easyctf/__init__.py
Executable file
|
@ -0,0 +1,86 @@
|
|||
from datetime import datetime
|
||||
import time
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from flask import Flask, request
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
app = Flask(__name__, static_folder="assets", static_path="/assets")
|
||||
hostname = socket.gethostname()
|
||||
|
||||
if not config:
|
||||
from easyctf.config import Config
|
||||
config = Config()
|
||||
app.config.from_object(config)
|
||||
|
||||
from easyctf.objects import cache, db, login_manager, sentry
|
||||
import easyctf.models
|
||||
cache.init_app(app)
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
if app.config.get("ENVIRONMENT") != "development":
|
||||
sentry.init_app(app, logging=True, level=logging.WARNING)
|
||||
|
||||
from easyctf.utils import filestore, to_place_str, to_timestamp
|
||||
app.jinja_env.globals.update(filestore=filestore)
|
||||
app.jinja_env.filters["to_timestamp"] = to_timestamp
|
||||
app.jinja_env.filters["to_place_str"] = to_place_str
|
||||
|
||||
from easyctf.models import Config
|
||||
|
||||
def get_competition_running():
|
||||
configs = Config.get_many(["start_time", "end_time"])
|
||||
if "start_time" not in configs or "end_time" not in configs:
|
||||
return None, None, False
|
||||
start_time_str = configs["start_time"]
|
||||
end_time_str = configs["end_time"]
|
||||
|
||||
start_time = datetime.fromtimestamp(float(start_time_str))
|
||||
end_time = datetime.fromtimestamp(float(end_time_str))
|
||||
now = datetime.utcnow()
|
||||
|
||||
competition_running = start_time < now and now < end_time
|
||||
return start_time, end_time, competition_running
|
||||
|
||||
@app.after_request
|
||||
def easter_egg_link(response):
|
||||
if not request.cookies.get("easter_egg_enabled"):
|
||||
response.set_cookie("easter_egg_enabled", "0")
|
||||
return response
|
||||
|
||||
# TODO: actually finish this
|
||||
@app.context_processor
|
||||
def inject_config():
|
||||
competition_start, competition_end, competition_running = get_competition_running()
|
||||
easter_egg_enabled = False
|
||||
if competition_running and current_user.is_authenticated:
|
||||
try:
|
||||
easter_egg_enabled = int(request.cookies.get("easter_egg_enabled")) == 1
|
||||
except:
|
||||
pass
|
||||
config = dict(
|
||||
admin_email="",
|
||||
hostname=hostname,
|
||||
competition_running=competition_running,
|
||||
competition_start=competition_start,
|
||||
competition_end=competition_end,
|
||||
ctf_name=Config.get("ctf_name", "OpenCTF"),
|
||||
easter_egg_enabled=easter_egg_enabled,
|
||||
environment=app.config.get("ENVIRONMENT", "production")
|
||||
)
|
||||
return config
|
||||
|
||||
from easyctf.views import admin, base, classroom, chals, game, judge, teams, users
|
||||
app.register_blueprint(admin.blueprint, url_prefix="/admin")
|
||||
app.register_blueprint(base.blueprint)
|
||||
app.register_blueprint(classroom.blueprint, url_prefix="/classroom")
|
||||
app.register_blueprint(chals.blueprint, url_prefix="/chals")
|
||||
app.register_blueprint(game.blueprint, url_prefix="/game")
|
||||
app.register_blueprint(judge.blueprint, url_prefix="/judge")
|
||||
app.register_blueprint(teams.blueprint, url_prefix="/teams")
|
||||
app.register_blueprint(users.blueprint, url_prefix="/users")
|
||||
|
||||
return app
|
5
server/easyctf/assets/css/bootstrap.min.css
vendored
Executable file
2337
server/easyctf/assets/css/font-awesome.css
vendored
Executable file
4
server/easyctf/assets/css/font-awesome.min.css
vendored
Executable file
213
server/easyctf/assets/css/main.css
Executable file
|
@ -0,0 +1,213 @@
|
|||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-weight: 300;
|
||||
src: url("../fonts/SourceSansPro-Light.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-weight: 400;
|
||||
src: url("../fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: "Source Sans Pro";
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, .btn {
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
min-height: 85vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
-webkit-border-radius: 0;
|
||||
}
|
||||
|
||||
.navatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 30px 0 50px 0;
|
||||
}
|
||||
|
||||
.logo-table {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.logo-table td {
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.logo-table a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo-table h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.badge a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
color: #FFF;
|
||||
background: #31abc6;
|
||||
background: -moz-linear-gradient(45deg, #31abc6 0%, #5b69bf 100%);
|
||||
background: -webkit-linear-gradient(45deg, #31abc6 0%,#5b69bf 100%);
|
||||
background: linear-gradient(45deg, #31abc6 0%,#5b69bf 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#695bbf', endColorstr='#c631ab',GradientType=1 );
|
||||
}
|
||||
|
||||
#masthead {
|
||||
min-height:40vh;
|
||||
position:relative;
|
||||
margin: 0;
|
||||
padding-top: 34px;
|
||||
padding-bottom: 42px;
|
||||
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#title h1 {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-size: 6.5em;
|
||||
}
|
||||
|
||||
#title h2 {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 2.0em;
|
||||
}
|
||||
|
||||
#title a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 1.2em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#links a {
|
||||
margin: 0 12px 0 12px;
|
||||
color: #555;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
/*margin-top: 90px;*/
|
||||
background-color: #eee;
|
||||
color: #999;
|
||||
padding: 3em 0 0;
|
||||
font-size: 14.3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-footer .footer-heading {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
margin-top: 2em;
|
||||
padding: 2em 2em;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.site-footer h5, .site-footer .h5 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.container.jumbotron {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
html, body, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: "Source Sans Pro", Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.large-text {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.tab-content > .tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content > .active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
background-color: #eee;
|
||||
color: #999;
|
||||
padding: 3em 0 0;
|
||||
font-size: 14.3px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.site-footer .footer-heading {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
margin-top: 2em;
|
||||
padding: 2em 2em;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h5, .h5 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#links a {
|
||||
margin: 0 12px 0 12px;
|
||||
color: #555;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.selectize-input {
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
-webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.selectize-control {
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.selectize-input .item {
|
||||
padding: 1px 6px !important;
|
||||
}
|
1
server/easyctf/assets/css/selectize.min.css
vendored
Normal file
BIN
server/easyctf/assets/fonts/FontAwesome.otf
Executable file
93
server/easyctf/assets/fonts/OFL.txt
Executable file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
server/easyctf/assets/fonts/SourceSansPro-Black.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-BlackItalic.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-Bold.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-BoldItalic.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-ExtraLight.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-Italic.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-Light.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-LightItalic.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-Regular.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-SemiBold.ttf
Executable file
BIN
server/easyctf/assets/fonts/SourceSansPro-SemiBoldItalic.ttf
Executable file
BIN
server/easyctf/assets/fonts/fontawesome-webfont.eot
Executable file
2671
server/easyctf/assets/fonts/fontawesome-webfont.svg
Executable file
After Width: | Height: | Size: 434 KiB |
BIN
server/easyctf/assets/fonts/fontawesome-webfont.ttf
Executable file
BIN
server/easyctf/assets/fonts/fontawesome-webfont.woff
Executable file
BIN
server/easyctf/assets/fonts/fontawesome-webfont.woff2
Executable file
4146
server/easyctf/assets/game.json
Normal file
BIN
server/easyctf/assets/images/digitalocean.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
server/easyctf/assets/images/game/090939.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
server/easyctf/assets/images/game/background.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
server/easyctf/assets/images/game/background_rain.gif
Normal file
After Width: | Height: | Size: 556 KiB |
BIN
server/easyctf/assets/images/game/char_A_scale.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
server/easyctf/assets/images/game/char_B_scale.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
server/easyctf/assets/images/game/char_C_scale.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
server/easyctf/assets/images/game/char_D_scale.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
server/easyctf/assets/images/game/char_E_scale.png
Normal file
After Width: | Height: | Size: 31 KiB |
7
server/easyctf/assets/js/bootstrap.min.js
vendored
Executable file
1
server/easyctf/assets/js/bootstrap3-typeahead.min.js
vendored
Executable file
4
server/easyctf/assets/js/jquery-2.1.4.min.js
vendored
Executable file
4
server/easyctf/assets/js/livestamp.min.js
vendored
Executable file
|
@ -0,0 +1,4 @@
|
|||
// Livestamp.js / v1.1.2 / (c) 2012 Matt Bradley / MIT License
|
||||
(function(d,g){var h=1E3,i=!1,e=d([]),j=function(b,a){var c=b.data("livestampdata");"number"==typeof a&&(a*=1E3);b.removeAttr("data-livestamp").removeData("livestamp");a=g(a);g.isMoment(a)&&!isNaN(+a)&&(c=d.extend({},{original:b.contents()},c),c.moment=g(a),b.data("livestampdata",c).empty(),e.push(b[0]))},k=function(){i||(f.update(),setTimeout(k,h))},f={update:function(){d("[data-livestamp]").each(function(){var a=d(this);j(a,a.data("livestamp"))});var b=[];e.each(function(){var a=d(this),c=a.data("livestampdata");
|
||||
if(void 0===c)b.push(this);else if(g.isMoment(c.moment)){var e=a.html(),c=c.moment.fromNow();if(e!=c){var f=d.Event("change.livestamp");a.trigger(f,[e,c]);f.isDefaultPrevented()||a.html(c)}}});e=e.not(b)},pause:function(){i=!0},resume:function(){i=!1;k()},interval:function(b){if(void 0===b)return h;h=b}},l={add:function(b,a){"number"==typeof a&&(a*=1E3);a=g(a);g.isMoment(a)&&!isNaN(+a)&&(b.each(function(){j(d(this),a)}),f.update());return b},destroy:function(b){e=e.not(b);b.each(function(){var a=
|
||||
d(this),c=a.data("livestampdata");if(void 0===c)return b;a.html(c.original?c.original:"").removeData("livestampdata")});return b},isLivestamp:function(b){return void 0!==b.data("livestampdata")}};d.livestamp=f;d(function(){f.resume()});d.fn.livestamp=function(b,a){l[b]||(a=b,b="add");return l[b](this,a)}})(jQuery,moment);
|
495
server/easyctf/assets/js/moment.min.js
vendored
Executable file
|
@ -0,0 +1,495 @@
|
|||
//! moment.js
|
||||
//! version : 2.15.1
|
||||
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
|
||||
//! license : MIT
|
||||
//! momentjs.com
|
||||
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return md.apply(null,arguments)}
|
||||
// This is done to register the method called with moment()
|
||||
// without creating circular dependencies.
|
||||
function b(a){md=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){
|
||||
// IE8 will treat undefined and null as object if it wasn't for
|
||||
// input != null
|
||||
return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a)
|
||||
// even if its not own property I'd still call it non-empty
|
||||
return!1;return!0}function f(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function g(a,b){var c,d=[];for(c=0;c<a.length;++c)d.push(b(a[c],c));return d}function h(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function i(a,b){for(var c in b)h(b,c)&&(a[c]=b[c]);return h(b,"toString")&&(a.toString=b.toString),h(b,"valueOf")&&(a.valueOf=b.valueOf),a}function j(a,b,c,d){return qb(a,b,c,d,!0).utc()}function k(){
|
||||
// We need to deep clone this object.
|
||||
return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null}}function l(a){return null==a._pf&&(a._pf=k()),a._pf}function m(a){if(null==a._isValid){var b=l(a),c=nd.call(b.parsedDateParts,function(a){return null!=a}),d=!isNaN(a._d.getTime())&&b.overflow<0&&!b.empty&&!b.invalidMonth&&!b.invalidWeekday&&!b.nullInput&&!b.invalidFormat&&!b.userInvalidated&&(!b.meridiem||b.meridiem&&c);if(a._strict&&(d=d&&0===b.charsLeftOver&&0===b.unusedTokens.length&&void 0===b.bigHour),null!=Object.isFrozen&&Object.isFrozen(a))return d;a._isValid=d}return a._isValid}function n(a){var b=j(NaN);return null!=a?i(l(b),a):l(b).userInvalidated=!0,b}function o(a){return void 0===a}function p(a,b){var c,d,e;if(o(b._isAMomentObject)||(a._isAMomentObject=b._isAMomentObject),o(b._i)||(a._i=b._i),o(b._f)||(a._f=b._f),o(b._l)||(a._l=b._l),o(b._strict)||(a._strict=b._strict),o(b._tzm)||(a._tzm=b._tzm),o(b._isUTC)||(a._isUTC=b._isUTC),o(b._offset)||(a._offset=b._offset),o(b._pf)||(a._pf=l(b)),o(b._locale)||(a._locale=b._locale),od.length>0)for(c in od)d=od[c],e=b[d],o(e)||(a[d]=e);return a}
|
||||
// Moment prototype object
|
||||
function q(b){p(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),pd===!1&&(pd=!0,a.updateOffset(this),pd=!1)}function r(a){return a instanceof q||null!=a&&null!=a._isAMomentObject}function s(a){return 0>a?Math.ceil(a)||0:Math.floor(a)}function t(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=s(b)),c}
|
||||
// compare two arrays, return the number of differences
|
||||
function u(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&t(a[d])!==t(b[d]))&&g++;return g+f}function v(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function w(b,c){var d=!0;return i(function(){if(null!=a.deprecationHandler&&a.deprecationHandler(null,b),d){for(var e,f=[],g=0;g<arguments.length;g++){if(e="","object"==typeof arguments[g]){e+="\n["+g+"] ";for(var h in arguments[0])e+=h+": "+arguments[0][h]+", ";e=e.slice(0,-2)}else e=arguments[g];f.push(e)}v(b+"\nArguments: "+Array.prototype.slice.call(f).join("")+"\n"+(new Error).stack),d=!1}return c.apply(this,arguments)},c)}function x(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),qd[b]||(v(c),qd[b]=!0)}function y(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function z(a){var b,c;for(c in a)b=a[c],y(b)?this[c]=b:this["_"+c]=b;this._config=a,
|
||||
// Lenient ordinal parsing accepts just a number in addition to
|
||||
// number + (possibly) stuff coming from _ordinalParseLenient.
|
||||
this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function A(a,b){var c,e=i({},a);for(c in b)h(b,c)&&(d(a[c])&&d(b[c])?(e[c]={},i(e[c],a[c]),i(e[c],b[c])):null!=b[c]?e[c]=b[c]:delete e[c]);for(c in a)h(a,c)&&!h(b,c)&&d(a[c])&&(
|
||||
// make sure changes to properties don't modify parent config
|
||||
e[c]=i({},e[c]));return e}function B(a){null!=a&&this.set(a)}function C(a,b,c){var d=this._calendar[a]||this._calendar.sameElse;return y(d)?d.call(b,c):d}function D(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function E(){return this._invalidDate}function F(a){return this._ordinal.replace("%d",a)}function G(a,b,c,d){var e=this._relativeTime[c];return y(e)?e(a,b,c,d):e.replace(/%d/i,a)}function H(a,b){var c=this._relativeTime[a>0?"future":"past"];return y(c)?c(b):c.replace(/%s/i,b)}function I(a,b){var c=a.toLowerCase();zd[c]=zd[c+"s"]=zd[b]=a}function J(a){return"string"==typeof a?zd[a]||zd[a.toLowerCase()]:void 0}function K(a){var b,c,d={};for(c in a)h(a,c)&&(b=J(c),b&&(d[b]=a[c]));return d}function L(a,b){Ad[a]=b}function M(a){var b=[];for(var c in a)b.push({unit:c,priority:Ad[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function N(b,c){return function(d){return null!=d?(P(this,b,d),a.updateOffset(this,c),this):O(this,b)}}function O(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function P(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}
|
||||
// MOMENTS
|
||||
function Q(a){return a=J(a),y(this[a])?this[a]():this}function R(a,b){if("object"==typeof a){a=K(a);for(var c=M(a),d=0;d<c.length;d++)this[c[d].unit](a[c[d].unit])}else if(a=J(a),y(this[a]))return this[a](b);return this}function S(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}
|
||||
// token: 'M'
|
||||
// padded: ['MM', 2]
|
||||
// ordinal: 'Mo'
|
||||
// callback: function () { this.month() + 1 }
|
||||
function T(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Ed[a]=e),b&&(Ed[b[0]]=function(){return S(e.apply(this,arguments),b[1],b[2])}),c&&(Ed[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function U(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function V(a){var b,c,d=a.match(Bd);for(b=0,c=d.length;c>b;b++)Ed[d[b]]?d[b]=Ed[d[b]]:d[b]=U(d[b]);return function(b){var e,f="";for(e=0;c>e;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}
|
||||
// format date using native date object
|
||||
function W(a,b){return a.isValid()?(b=X(b,a.localeData()),Dd[b]=Dd[b]||V(b),Dd[b](a)):a.localeData().invalidDate()}function X(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Cd.lastIndex=0;d>=0&&Cd.test(a);)a=a.replace(Cd,c),Cd.lastIndex=0,d-=1;return a}function Y(a,b,c){Wd[a]=y(b)?b:function(a,d){return a&&c?c:b}}function Z(a,b){return h(Wd,a)?Wd[a](b._strict,b._locale):new RegExp($(a))}
|
||||
// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
|
||||
function $(a){return _(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function _(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function aa(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=t(a)}),c=0;c<a.length;c++)Xd[a[c]]=d}function ba(a,b){aa(a,function(a,c,d,e){d._w=d._w||{},b(a,d._w,d,e)})}function ca(a,b,c){null!=b&&h(Xd,a)&&Xd[a](b,c._a,c,a)}function da(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function ea(a,b){return a?c(this._months)?this._months[a.month()]:this._months[(this._months.isFormat||fe).test(b)?"format":"standalone"][a.month()]:this._months}function fa(a,b){return a?c(this._monthsShort)?this._monthsShort[a.month()]:this._monthsShort[fe.test(b)?"format":"standalone"][a.month()]:this._monthsShort}function ga(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._monthsParse)for(
|
||||
// this is not used
|
||||
this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],d=0;12>d;++d)f=j([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=sd.call(this._shortMonthsParse,g),-1!==e?e:null):(e=sd.call(this._longMonthsParse,g),-1!==e?e:null):"MMM"===b?(e=sd.call(this._shortMonthsParse,g),-1!==e?e:(e=sd.call(this._longMonthsParse,g),-1!==e?e:null)):(e=sd.call(this._longMonthsParse,g),-1!==e?e:(e=sd.call(this._shortMonthsParse,g),-1!==e?e:null))}function ha(a,b,c){var d,e,f;if(this._monthsParseExact)return ga.call(this,a,b,c);
|
||||
// TODO: add sorting
|
||||
// Sorting makes sure if one month (or abbr) is a prefix of another
|
||||
// see sorting in computeMonthsParse
|
||||
for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){
|
||||
// test the regex
|
||||
if(e=j([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}
|
||||
// MOMENTS
|
||||
function ia(a,b){var c;if(!a.isValid())
|
||||
// No op
|
||||
return a;if("string"==typeof b)if(/^\d+$/.test(b))b=t(b);else
|
||||
// TODO: Another silent failure?
|
||||
if(b=a.localeData().monthsParse(b),"number"!=typeof b)return a;return c=Math.min(a.date(),da(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ja(b){return null!=b?(ia(this,b),a.updateOffset(this,!0),this):O(this,"Month")}function ka(){return da(this.year(),this.month())}function la(a){return this._monthsParseExact?(h(this,"_monthsRegex")||na.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):(h(this,"_monthsShortRegex")||(this._monthsShortRegex=ie),this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex)}function ma(a){return this._monthsParseExact?(h(this,"_monthsRegex")||na.call(this),a?this._monthsStrictRegex:this._monthsRegex):(h(this,"_monthsRegex")||(this._monthsRegex=je),this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex)}function na(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=j([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(
|
||||
// Sorting makes sure if one month (or abbr) is a prefix of another it
|
||||
// will match the longer piece.
|
||||
d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=_(d[b]),e[b]=_(e[b]);for(b=0;24>b;b++)f[b]=_(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}
|
||||
// HELPERS
|
||||
function oa(a){return pa(a)?366:365}function pa(a){return a%4===0&&a%100!==0||a%400===0}function qa(){return pa(this.year())}function ra(a,b,c,d,e,f,g){
|
||||
//can't just apply() to create a date:
|
||||
//http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
|
||||
var h=new Date(a,b,c,d,e,f,g);
|
||||
//the date constructor remaps years 0-99 to 1900-1999
|
||||
return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function sa(a){var b=new Date(Date.UTC.apply(null,arguments));
|
||||
//the Date.UTC function remaps years 0-99 to 1900-1999
|
||||
return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}
|
||||
// start-of-first-week - start-of-year
|
||||
function ta(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other)
|
||||
d=7+b-c,
|
||||
// first-week day local weekday -- which local weekday is fwd
|
||||
e=(7+sa(a,0,d).getUTCDay()-b)%7;return-e+d-1}
|
||||
//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
|
||||
function ua(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ta(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=oa(f)+j):j>oa(a)?(f=a+1,g=j-oa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function va(a,b,c){var d,e,f=ta(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+wa(e,b,c)):g>wa(a.year(),b,c)?(d=g-wa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function wa(a,b,c){var d=ta(a,b,c),e=ta(a+1,b,c);return(oa(a)-d+e)/7}
|
||||
// HELPERS
|
||||
// LOCALES
|
||||
function xa(a){return va(a,this._week.dow,this._week.doy).week}function ya(){return this._week.dow}function za(){return this._week.doy}
|
||||
// MOMENTS
|
||||
function Aa(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ba(a){var b=va(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}
|
||||
// HELPERS
|
||||
function Ca(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Da(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Ea(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Fa(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ga(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ha(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;7>d;++d)f=j([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=sd.call(this._weekdaysParse,g),-1!==e?e:null):"ddd"===b?(e=sd.call(this._shortWeekdaysParse,g),-1!==e?e:null):(e=sd.call(this._minWeekdaysParse,g),-1!==e?e:null):"dddd"===b?(e=sd.call(this._weekdaysParse,g),-1!==e?e:(e=sd.call(this._shortWeekdaysParse,g),-1!==e?e:(e=sd.call(this._minWeekdaysParse,g),-1!==e?e:null))):"ddd"===b?(e=sd.call(this._shortWeekdaysParse,g),-1!==e?e:(e=sd.call(this._weekdaysParse,g),-1!==e?e:(e=sd.call(this._minWeekdaysParse,g),-1!==e?e:null))):(e=sd.call(this._minWeekdaysParse,g),-1!==e?e:(e=sd.call(this._weekdaysParse,g),-1!==e?e:(e=sd.call(this._shortWeekdaysParse,g),-1!==e?e:null)))}function Ia(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ha.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){
|
||||
// test the regex
|
||||
if(e=j([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}
|
||||
// MOMENTS
|
||||
function Ja(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Ca(a,this.localeData()),this.add(a-b,"d")):b}function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function La(a){if(!this.isValid())return null!=a?this:NaN;
|
||||
// behaves the same as moment#day except
|
||||
// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
|
||||
// as a setter, sunday should belong to the previous week.
|
||||
if(null!=a){var b=Da(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Ma(a){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||Pa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(h(this,"_weekdaysRegex")||(this._weekdaysRegex=pe),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Na(a){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||Pa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(h(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=qe),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Oa(a){return this._weekdaysParseExact?(h(this,"_weekdaysRegex")||Pa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(h(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=re),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Pa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],k=[];for(b=0;7>b;b++)c=j([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),k.push(d),k.push(e),k.push(f);for(
|
||||
// Sorting makes sure if one weekday (or abbr) is a prefix of another it
|
||||
// will match the longer piece.
|
||||
g.sort(a),h.sort(a),i.sort(a),k.sort(a),b=0;7>b;b++)h[b]=_(h[b]),i[b]=_(i[b]),k[b]=_(k[b]);this._weekdaysRegex=new RegExp("^("+k.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}
|
||||
// FORMATTING
|
||||
function Qa(){return this.hours()%12||12}function Ra(){return this.hours()||24}function Sa(a,b){T(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}
|
||||
// PARSING
|
||||
function Ta(a,b){return b._meridiemParse}
|
||||
// LOCALES
|
||||
function Ua(a){
|
||||
// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
|
||||
// Using charAt should be more compatible.
|
||||
return"p"===(a+"").toLowerCase().charAt(0)}function Va(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Wa(a){return a?a.toLowerCase().replace("_","-"):a}
|
||||
// pick the locale from the array
|
||||
// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
|
||||
// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
|
||||
function Xa(a){for(var b,c,d,e,f=0;f<a.length;){for(e=Wa(a[f]).split("-"),b=e.length,c=Wa(a[f+1]),c=c?c.split("-"):null;b>0;){if(d=Ya(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&u(e,c,!0)>=b-1)
|
||||
//the next array item is better than a shallower substring of this one
|
||||
break;b--}f++}return null}function Ya(a){var b=null;
|
||||
// TODO: Find a better way to register and load all the locales in Node
|
||||
if(!we[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=se._abbr,require("./locale/"+a),
|
||||
// because defineLocale currently also sets the global locale, we
|
||||
// want to undo that for lazy loaded locales
|
||||
Za(b)}catch(c){}return we[a]}
|
||||
// This function will load locale and then set the global locale. If
|
||||
// no arguments are passed in, it will simply return the current global
|
||||
// locale key.
|
||||
function Za(a,b){var c;
|
||||
// moment.duration._locale = moment._locale = data;
|
||||
return a&&(c=o(b)?ab(a):$a(a,b),c&&(se=c)),se._abbr}function $a(a,b){if(null!==b){var c=ve;
|
||||
// treat as if there is no base config
|
||||
// backwards compat for now: also set the locale
|
||||
return b.abbr=a,null!=we[a]?(x("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=we[a]._config):null!=b.parentLocale&&(null!=we[b.parentLocale]?c=we[b.parentLocale]._config:x("parentLocaleUndefined","specified parentLocale is not defined yet. See http://momentjs.com/guides/#/warnings/parent-locale/")),we[a]=new B(A(c,b)),Za(a),we[a]}
|
||||
// useful for testing
|
||||
return delete we[a],null}function _a(a,b){if(null!=b){var c,d=ve;
|
||||
// MERGE
|
||||
null!=we[a]&&(d=we[a]._config),b=A(d,b),c=new B(b),c.parentLocale=we[a],we[a]=c,
|
||||
// backwards compat for now: also set the locale
|
||||
Za(a)}else
|
||||
// pass null for config to unupdate, useful for tests
|
||||
null!=we[a]&&(null!=we[a].parentLocale?we[a]=we[a].parentLocale:null!=we[a]&&delete we[a]);return we[a]}
|
||||
// returns locale data
|
||||
function ab(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return se;if(!c(a)){if(b=Ya(a))return b;a=[a]}return Xa(a)}function bb(){return rd(we)}function cb(a){var b,c=a._a;return c&&-2===l(a).overflow&&(b=c[Zd]<0||c[Zd]>11?Zd:c[$d]<1||c[$d]>da(c[Yd],c[Zd])?$d:c[_d]<0||c[_d]>24||24===c[_d]&&(0!==c[ae]||0!==c[be]||0!==c[ce])?_d:c[ae]<0||c[ae]>59?ae:c[be]<0||c[be]>59?be:c[ce]<0||c[ce]>999?ce:-1,l(a)._overflowDayOfYear&&(Yd>b||b>$d)&&(b=$d),l(a)._overflowWeeks&&-1===b&&(b=de),l(a)._overflowWeekday&&-1===b&&(b=ee),l(a).overflow=b),a}
|
||||
// date from iso format
|
||||
function db(a){var b,c,d,e,f,g,h=a._i,i=xe.exec(h)||ye.exec(h);if(i){for(l(a).iso=!0,b=0,c=Ae.length;c>b;b++)if(Ae[b][1].exec(i[1])){e=Ae[b][0],d=Ae[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=Be.length;c>b;b++)if(Be[b][1].exec(i[3])){
|
||||
// match[2] should be 'T' or space
|
||||
f=(i[2]||" ")+Be[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!ze.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),jb(a)}else a._isValid=!1}
|
||||
// date from iso format or fallback
|
||||
function eb(b){var c=Ce.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(db(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}
|
||||
// Pick the first defined of two or three arguments.
|
||||
function fb(a,b,c){return null!=a?a:null!=b?b:c}function gb(b){
|
||||
// hooks is actually the exported moment object
|
||||
var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}
|
||||
// convert an array to a date.
|
||||
// the array should mirror the parameters below
|
||||
// note: all values past the year are optional and will default to the lowest possible value.
|
||||
// [year, month, day , hour, minute, second, millisecond]
|
||||
function hb(a){var b,c,d,e,f=[];if(!a._d){
|
||||
// Default to current date.
|
||||
// * if no year, month, day of month are given, default to today
|
||||
// * if day of month is given, default month and year
|
||||
// * if month is given, default only year
|
||||
// * if year is given, don't default anything
|
||||
for(d=gb(a),a._w&&null==a._a[$d]&&null==a._a[Zd]&&ib(a),a._dayOfYear&&(e=fb(a._a[Yd],d[Yd]),a._dayOfYear>oa(e)&&(l(a)._overflowDayOfYear=!0),c=sa(e,0,a._dayOfYear),a._a[Zd]=c.getUTCMonth(),a._a[$d]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];
|
||||
// Zero out whatever was not defaulted, including time
|
||||
for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];
|
||||
// Check for 24:00:00.000
|
||||
24===a._a[_d]&&0===a._a[ae]&&0===a._a[be]&&0===a._a[ce]&&(a._nextDay=!0,a._a[_d]=0),a._d=(a._useUTC?sa:ra).apply(null,f),
|
||||
// Apply timezone offset from input. The actual utcOffset can be changed
|
||||
// with parseZone.
|
||||
null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[_d]=24)}}function ib(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=fb(b.GG,a._a[Yd],va(rb(),1,4).year),d=fb(b.W,1),e=fb(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=fb(b.gg,a._a[Yd],va(rb(),f,g).year),d=fb(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>wa(c,f,g)?l(a)._overflowWeeks=!0:null!=i?l(a)._overflowWeekday=!0:(h=ua(c,d,e,f,g),a._a[Yd]=h.year,a._dayOfYear=h.dayOfYear)}
|
||||
// date from string and format string
|
||||
function jb(b){
|
||||
// TODO: Move this to another part of the creation flow to prevent circular deps
|
||||
if(b._f===a.ISO_8601)return void db(b);b._a=[],l(b).empty=!0;
|
||||
// This array is used to make a Date, either with `new Date` or `Date.UTC`
|
||||
var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=X(b._f,b._locale).match(Bd)||[],c=0;c<e.length;c++)f=e[c],d=(h.match(Z(f,b))||[])[0],d&&(g=h.substr(0,h.indexOf(d)),g.length>0&&l(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),Ed[f]?(d?l(b).empty=!1:l(b).unusedTokens.push(f),ca(f,d,b)):b._strict&&!d&&l(b).unusedTokens.push(f);
|
||||
// add remaining unparsed input length to the string
|
||||
l(b).charsLeftOver=i-j,h.length>0&&l(b).unusedInput.push(h),
|
||||
// clear _12h flag if hour is <= 12
|
||||
b._a[_d]<=12&&l(b).bigHour===!0&&b._a[_d]>0&&(l(b).bigHour=void 0),l(b).parsedDateParts=b._a.slice(0),l(b).meridiem=b._meridiem,
|
||||
// handle meridiem
|
||||
b._a[_d]=kb(b._locale,b._a[_d],b._meridiem),hb(b),cb(b)}function kb(a,b,c){var d;
|
||||
// Fallback
|
||||
return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}
|
||||
// date from string and array of format strings
|
||||
function lb(a){var b,c,d,e,f;if(0===a._f.length)return l(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e<a._f.length;e++)f=0,b=p({},a),null!=a._useUTC&&(b._useUTC=a._useUTC),b._f=a._f[e],jb(b),m(b)&&(f+=l(b).charsLeftOver,f+=10*l(b).unusedTokens.length,l(b).score=f,(null==d||d>f)&&(d=f,c=b));i(a,c||b)}function mb(a){if(!a._d){var b=K(a._i);a._a=g([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),hb(a)}}function nb(a){var b=new q(cb(ob(a)));
|
||||
// Adding is smart enough around DST
|
||||
return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function ob(a){var b=a._i,d=a._f;return a._locale=a._locale||ab(a._l),null===b||void 0===d&&""===b?n({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),r(b)?new q(cb(b)):(c(d)?lb(a):f(b)?a._d=b:d?jb(a):pb(a),m(a)||(a._d=null),a))}function pb(b){var d=b._i;void 0===d?b._d=new Date(a.now()):f(d)?b._d=new Date(d.valueOf()):"string"==typeof d?eb(b):c(d)?(b._a=g(d.slice(0),function(a){return parseInt(a,10)}),hb(b)):"object"==typeof d?mb(b):"number"==typeof d?
|
||||
// from milliseconds
|
||||
b._d=new Date(d):a.createFromInputFallback(b)}function qb(a,b,f,g,h){var i={};
|
||||
// object construction must be done this way.
|
||||
// https://github.com/moment/moment/issues/1423
|
||||
return"boolean"==typeof f&&(g=f,f=void 0),(d(a)&&e(a)||c(a)&&0===a.length)&&(a=void 0),i._isAMomentObject=!0,i._useUTC=i._isUTC=h,i._l=f,i._i=a,i._f=b,i._strict=g,nb(i)}function rb(a,b,c,d){return qb(a,b,c,d,!1)}
|
||||
// Pick a moment m from moments so that m[fn](other) is true for all
|
||||
// other. This relies on the function fn to be transitive.
|
||||
//
|
||||
// moments should either be an array of moment objects or an array, whose
|
||||
// first element is an array of moment objects.
|
||||
function sb(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return rb();for(d=b[0],e=1;e<b.length;++e)b[e].isValid()&&!b[e][a](d)||(d=b[e]);return d}
|
||||
// TODO: Use [].sort instead?
|
||||
function tb(){var a=[].slice.call(arguments,0);return sb("isBefore",a)}function ub(){var a=[].slice.call(arguments,0);return sb("isAfter",a)}function vb(a){var b=K(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;
|
||||
// representation for dateAddRemove
|
||||
this._milliseconds=+k+1e3*j+// 1000
|
||||
6e4*i+// 1000 * 60
|
||||
1e3*h*60*60,//using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
|
||||
// Because of dateAddRemove treats 24 hours as different from a
|
||||
// day when working around DST, we need to store them separately
|
||||
this._days=+g+7*f,
|
||||
// It is impossible translate months into days without knowing
|
||||
// which months you are are talking about, so we have to store
|
||||
// it separately.
|
||||
this._months=+e+3*d+12*c,this._data={},this._locale=ab(),this._bubble()}function wb(a){return a instanceof vb}function xb(a){return 0>a?-1*Math.round(-1*a):Math.round(a)}
|
||||
// FORMATTING
|
||||
function yb(a,b){T(a,0,0,function(){var a=this.utcOffset(),c="+";return 0>a&&(a=-a,c="-"),c+S(~~(a/60),2)+b+S(~~a%60,2)})}function zb(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(Ge)||["-",0,0],f=+(60*e[1])+t(e[2]);return"+"===e[0]?f:-f}
|
||||
// Return a moment from input, that is local/utc/zone equivalent to model.
|
||||
function Ab(b,c){var d,e;
|
||||
// Use low-level api, because this fn is low-level api.
|
||||
return c._isUTC?(d=c.clone(),e=(r(b)||f(b)?b.valueOf():rb(b).valueOf())-d.valueOf(),d._d.setTime(d._d.valueOf()+e),a.updateOffset(d,!1),d):rb(b).local()}function Bb(a){
|
||||
// On Firefox.24 Date#getTimezoneOffset returns a floating point.
|
||||
// https://github.com/moment/moment/pull/1871
|
||||
return 15*-Math.round(a._d.getTimezoneOffset()/15)}
|
||||
// MOMENTS
|
||||
// keepLocalTime = true means only change the timezone, without
|
||||
// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
|
||||
// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
|
||||
// +0200, so we adjust the time as needed, to be valid.
|
||||
//
|
||||
// Keeping the time actually adds/subtracts (one hour)
|
||||
// from the actual represented time. That is why we call updateOffset
|
||||
// a second time. In case it wants us to change the offset again
|
||||
// _changeInProgress == true case, then we have to adjust, because
|
||||
// there is no such time in the given timezone.
|
||||
function Cb(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=zb(Td,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Bb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Sb(this,Nb(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Bb(this):null!=b?this:NaN}function Db(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Eb(a){return this.utcOffset(0,a)}function Fb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Bb(this),"m")),this}function Gb(){if(this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=zb(Sd,this._i);0===a?this.utcOffset(0,!0):this.utcOffset(zb(Sd,this._i))}return this}function Hb(a){return this.isValid()?(a=a?rb(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function Ib(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Jb(){if(!o(this._isDSTShifted))return this._isDSTShifted;var a={};if(p(a,this),a=ob(a),a._a){var b=a._isUTC?j(a._a):rb(a._a);this._isDSTShifted=this.isValid()&&u(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kb(){return this.isValid()?!this._isUTC:!1}function Lb(){return this.isValid()?this._isUTC:!1}function Mb(){return this.isValid()?this._isUTC&&0===this._offset:!1}function Nb(a,b){var c,d,e,f=a,
|
||||
// matching against regexp is expensive, do it on demand
|
||||
g=null;// checks for null or undefined
|
||||
return wb(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(f={},b?f[b]=a:f.milliseconds=a):(g=He.exec(a))?(c="-"===g[1]?-1:1,f={y:0,d:t(g[$d])*c,h:t(g[_d])*c,m:t(g[ae])*c,s:t(g[be])*c,ms:t(xb(1e3*g[ce]))*c}):(g=Ie.exec(a))?(c="-"===g[1]?-1:1,f={y:Ob(g[2],c),M:Ob(g[3],c),w:Ob(g[4],c),d:Ob(g[5],c),h:Ob(g[6],c),m:Ob(g[7],c),s:Ob(g[8],c)}):null==f?f={}:"object"==typeof f&&("from"in f||"to"in f)&&(e=Qb(rb(f.from),rb(f.to)),f={},f.ms=e.milliseconds,f.M=e.months),d=new vb(f),wb(a)&&h(a,"_locale")&&(d._locale=a._locale),d}function Ob(a,b){
|
||||
// We'd normally use ~~inp for this, but unfortunately it also
|
||||
// converts floats to ints.
|
||||
// inp may be undefined, so careful calling replace on it.
|
||||
var c=a&&parseFloat(a.replace(",","."));
|
||||
// apply sign while we're at it
|
||||
return(isNaN(c)?0:c)*b}function Pb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Qb(a,b){var c;return a.isValid()&&b.isValid()?(b=Ab(b,a),a.isBefore(b)?c=Pb(a,b):(c=Pb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}
|
||||
// TODO: remove 'name' arg after deprecation is removed
|
||||
function Rb(a,b){return function(c,d){var e,f;
|
||||
//invert the arguments, but complain about it
|
||||
return null===d||isNaN(+d)||(x(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Nb(c,d),Sb(this,e,a),this}}function Sb(b,c,d,e){var f=c._milliseconds,g=xb(c._days),h=xb(c._months);b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&P(b,"Date",O(b,"Date")+g*d),h&&ia(b,O(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Tb(a,b){var c=a.diff(b,"days",!0);return-6>c?"sameElse":-1>c?"lastWeek":0>c?"lastDay":1>c?"sameDay":2>c?"nextDay":7>c?"nextWeek":"sameElse"}function Ub(b,c){
|
||||
// We want to compare the start of today, vs this.
|
||||
// Getting start-of-today depends on whether we're local/utc/offset or not.
|
||||
var d=b||rb(),e=Ab(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(y(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,rb(d)))}function Vb(){return new q(this)}function Wb(a,b){var c=r(a)?a:rb(a);return this.isValid()&&c.isValid()?(b=J(o(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()<this.clone().startOf(b).valueOf()):!1}function Xb(a,b){var c=r(a)?a:rb(a);return this.isValid()&&c.isValid()?(b=J(o(b)?"millisecond":b),"millisecond"===b?this.valueOf()<c.valueOf():this.clone().endOf(b).valueOf()<c.valueOf()):!1}function Yb(a,b,c,d){return d=d||"()",("("===d[0]?this.isAfter(a,c):!this.isBefore(a,c))&&(")"===d[1]?this.isBefore(b,c):!this.isAfter(b,c))}function Zb(a,b){var c,d=r(a)?a:rb(a);return this.isValid()&&d.isValid()?(b=J(b||"millisecond"),"millisecond"===b?this.valueOf()===d.valueOf():(c=d.valueOf(),this.clone().startOf(b).valueOf()<=c&&c<=this.clone().endOf(b).valueOf())):!1}function $b(a,b){return this.isSame(a,b)||this.isAfter(a,b)}function _b(a,b){return this.isSame(a,b)||this.isBefore(a,b)}function ac(a,b,c){var d,e,f,g;// 1000
|
||||
// 1000 * 60
|
||||
// 1000 * 60 * 60
|
||||
// 1000 * 60 * 60 * 24, negate dst
|
||||
// 1000 * 60 * 60 * 24 * 7, negate dst
|
||||
return this.isValid()?(d=Ab(a,this),d.isValid()?(e=6e4*(d.utcOffset()-this.utcOffset()),b=J(b),"year"===b||"month"===b||"quarter"===b?(g=bc(this,d),"quarter"===b?g/=3:"year"===b&&(g/=12)):(f=this-d,g="second"===b?f/1e3:"minute"===b?f/6e4:"hour"===b?f/36e5:"day"===b?(f-e)/864e5:"week"===b?(f-e)/6048e5:f),c?g:s(g)):NaN):NaN}function bc(a,b){
|
||||
// difference in months
|
||||
var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),
|
||||
// b is in (anchor - 1 month, anchor + 1 month)
|
||||
f=a.clone().add(e,"months");
|
||||
//check for negative zero, return zero if negative zero
|
||||
// linear across the month
|
||||
// linear across the month
|
||||
return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function cc(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function dc(){var a=this.clone().utc();return 0<a.year()&&a.year()<=9999?y(Date.prototype.toISOString)?this.toDate().toISOString():W(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):W(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function ec(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=W(this,b);return this.localeData().postformat(c)}function fc(a,b){return this.isValid()&&(r(a)&&a.isValid()||rb(a).isValid())?Nb({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function gc(a){return this.from(rb(),a)}function hc(a,b){return this.isValid()&&(r(a)&&a.isValid()||rb(a).isValid())?Nb({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function ic(a){return this.to(rb(),a)}
|
||||
// If passed a locale key, it will set the locale for this
|
||||
// instance. Otherwise, it will return the locale configuration
|
||||
// variables for this instance.
|
||||
function jc(a){var b;return void 0===a?this._locale._abbr:(b=ab(a),null!=b&&(this._locale=b),this)}function kc(){return this._locale}function lc(a){
|
||||
// the following switch intentionally omits break keywords
|
||||
// to utilize falling through the cases.
|
||||
switch(a=J(a)){case"year":this.month(0);/* falls through */
|
||||
case"quarter":case"month":this.date(1);/* falls through */
|
||||
case"week":case"isoWeek":case"day":case"date":this.hours(0);/* falls through */
|
||||
case"hour":this.minutes(0);/* falls through */
|
||||
case"minute":this.seconds(0);/* falls through */
|
||||
case"second":this.milliseconds(0)}
|
||||
// weeks are a special case
|
||||
// quarters are also special
|
||||
return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function mc(a){
|
||||
// 'date' is an alias for 'day', so it should be considered as such.
|
||||
return a=J(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function nc(){return this._d.valueOf()-6e4*(this._offset||0)}function oc(){return Math.floor(this.valueOf()/1e3)}function pc(){return new Date(this.valueOf())}function qc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function rc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function sc(){
|
||||
// new Date(NaN).toJSON() === null
|
||||
return this.isValid()?this.toISOString():null}function tc(){return m(this)}function uc(){return i({},l(this))}function vc(){return l(this).overflow}function wc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function xc(a,b){T(0,[a,a.length],0,b)}
|
||||
// MOMENTS
|
||||
function yc(a){return Cc.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function zc(a){return Cc.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Ac(){return wa(this.year(),1,4)}function Bc(){var a=this.localeData()._week;return wa(this.year(),a.dow,a.doy)}function Cc(a,b,c,d,e){var f;return null==a?va(this,d,e).year:(f=wa(a,d,e),b>f&&(b=f),Dc.call(this,a,b,c,d,e))}function Dc(a,b,c,d,e){var f=ua(a,b,c,d,e),g=sa(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}
|
||||
// MOMENTS
|
||||
function Ec(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}
|
||||
// HELPERS
|
||||
// MOMENTS
|
||||
function Fc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Gc(a,b){b[ce]=t(1e3*("0."+a))}
|
||||
// MOMENTS
|
||||
function Hc(){return this._isUTC?"UTC":""}function Ic(){return this._isUTC?"Coordinated Universal Time":""}function Jc(a){return rb(1e3*a)}function Kc(){return rb.apply(null,arguments).parseZone()}function Lc(a){return a}function Mc(a,b,c,d){var e=ab(),f=j().set(d,b);return e[c](f,a)}function Nc(a,b,c){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return Mc(a,b,c,"month");var d,e=[];for(d=0;12>d;d++)e[d]=Mc(a,d,c,"month");return e}
|
||||
// ()
|
||||
// (5)
|
||||
// (fmt, 5)
|
||||
// (fmt)
|
||||
// (true)
|
||||
// (true, 5)
|
||||
// (true, fmt, 5)
|
||||
// (true, fmt)
|
||||
function Oc(a,b,c,d){"boolean"==typeof a?("number"==typeof b&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,"number"==typeof b&&(c=b,b=void 0),b=b||"");var e=ab(),f=a?e._week.dow:0;if(null!=c)return Mc(b,(c+f)%7,d,"day");var g,h=[];for(g=0;7>g;g++)h[g]=Mc(b,(g+f)%7,d,"day");return h}function Pc(a,b){return Nc(a,b,"months")}function Qc(a,b){return Nc(a,b,"monthsShort")}function Rc(a,b,c){return Oc(a,b,c,"weekdays")}function Sc(a,b,c){return Oc(a,b,c,"weekdaysShort")}function Tc(a,b,c){return Oc(a,b,c,"weekdaysMin")}function Uc(){var a=this._data;return this._milliseconds=Ue(this._milliseconds),this._days=Ue(this._days),this._months=Ue(this._months),a.milliseconds=Ue(a.milliseconds),a.seconds=Ue(a.seconds),a.minutes=Ue(a.minutes),a.hours=Ue(a.hours),a.months=Ue(a.months),a.years=Ue(a.years),this}function Vc(a,b,c,d){var e=Nb(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}
|
||||
// supports only 2.0-style add(1, 's') or add(duration)
|
||||
function Wc(a,b){return Vc(this,a,b,1)}
|
||||
// supports only 2.0-style subtract(1, 's') or subtract(duration)
|
||||
function Xc(a,b){return Vc(this,a,b,-1)}function Yc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Zc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;
|
||||
// if we have a mix of positive and negative values, bubble down first
|
||||
// check: https://github.com/moment/moment/issues/2166
|
||||
// The following code bubbles up values, see the tests for
|
||||
// examples of what that means.
|
||||
// convert days to months
|
||||
// 12 months -> 1 year
|
||||
return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Yc(_c(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=s(f/1e3),i.seconds=a%60,b=s(a/60),i.minutes=b%60,c=s(b/60),i.hours=c%24,g+=s(c/24),e=s($c(g)),h+=e,g-=Yc(_c(e)),d=s(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function $c(a){
|
||||
// 400 years have 146097 days (taking into account leap year rules)
|
||||
// 400 years have 12 months === 4800
|
||||
return 4800*a/146097}function _c(a){
|
||||
// the reverse of daysToMonths
|
||||
return 146097*a/4800}function ad(a){var b,c,d=this._milliseconds;if(a=J(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+$c(b),"month"===a?c:c/12;switch(b=this._days+Math.round(_c(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;
|
||||
// Math.floor prevents floating point math errors here
|
||||
case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}
|
||||
// TODO: Use this.as('ms')?
|
||||
function bd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*t(this._months/12)}function cd(a){return function(){return this.as(a)}}function dd(a){return a=J(a),this[a+"s"]()}function ed(a){return function(){return this._data[a]}}function fd(){return s(this.days()/7)}
|
||||
// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
|
||||
function gd(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function hd(a,b,c){var d=Nb(a).abs(),e=jf(d.as("s")),f=jf(d.as("m")),g=jf(d.as("h")),h=jf(d.as("d")),i=jf(d.as("M")),j=jf(d.as("y")),k=e<kf.s&&["s",e]||1>=f&&["m"]||f<kf.m&&["mm",f]||1>=g&&["h"]||g<kf.h&&["hh",g]||1>=h&&["d"]||h<kf.d&&["dd",h]||1>=i&&["M"]||i<kf.M&&["MM",i]||1>=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,gd.apply(null,k)}
|
||||
// This function allows you to set the rounding function for relative time strings
|
||||
function id(a){return void 0===a?jf:"function"==typeof a?(jf=a,!0):!1}
|
||||
// This function allows you to set a threshold for relative time strings
|
||||
function jd(a,b){return void 0===kf[a]?!1:void 0===b?kf[a]:(kf[a]=b,!0)}function kd(a){var b=this.localeData(),c=hd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function ld(){
|
||||
// for ISO strings we do not use the normal bubbling rules:
|
||||
// * milliseconds bubble up until they become hours
|
||||
// * days do not bubble at all
|
||||
// * months bubble up until they become years
|
||||
// This is because there is no context-free conversion between hours and days
|
||||
// (think of clock changes)
|
||||
// and also not between days and months (28-31 days per month)
|
||||
var a,b,c,d=lf(this._milliseconds)/1e3,e=lf(this._days),f=lf(this._months);a=s(d/60),b=s(a/60),d%=60,a%=60,c=s(f/12),f%=12;
|
||||
// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
|
||||
var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var md,nd;nd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;c>d;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};
|
||||
// Plugins that add properties should also add the key here (null value),
|
||||
// so we can properly clone ourselves.
|
||||
var od=a.momentProperties=[],pd=!1,qd={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var rd;rd=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)h(a,b)&&c.push(b);return c};var sd,td={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},ud={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},vd="Invalid date",wd="%d",xd=/\d{1,2}/,yd={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},zd={},Ad={},Bd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Cd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Dd={},Ed={},Fd=/\d/,Gd=/\d\d/,Hd=/\d{3}/,Id=/\d{4}/,Jd=/[+-]?\d{6}/,Kd=/\d\d?/,Ld=/\d\d\d\d?/,Md=/\d\d\d\d\d\d?/,Nd=/\d{1,3}/,Od=/\d{1,4}/,Pd=/[+-]?\d{1,6}/,Qd=/\d+/,Rd=/[+-]?\d+/,Sd=/Z|[+-]\d\d:?\d\d/gi,Td=/Z|[+-]\d\d(?::?\d\d)?/gi,Ud=/[+-]?\d+(\.\d{1,3})?/,Vd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Wd={},Xd={},Yd=0,Zd=1,$d=2,_d=3,ae=4,be=5,ce=6,de=7,ee=8;sd=Array.prototype.indexOf?Array.prototype.indexOf:function(a){
|
||||
// I know
|
||||
var b;for(b=0;b<this.length;++b)if(this[b]===a)return b;return-1},T("M",["MM",2],"Mo",function(){return this.month()+1}),T("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),T("MMMM",0,0,function(a){return this.localeData().months(this,a)}),I("month","M"),L("month",8),Y("M",Kd),Y("MM",Kd,Gd),Y("MMM",function(a,b){return b.monthsShortRegex(a)}),Y("MMMM",function(a,b){return b.monthsRegex(a)}),aa(["M","MM"],function(a,b){b[Zd]=t(a)-1}),aa(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[Zd]=e:l(c).invalidMonth=a});
|
||||
// LOCALES
|
||||
var fe=/D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/,ge="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),he="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),ie=Vd,je=Vd;
|
||||
// FORMATTING
|
||||
T("Y",0,0,function(){var a=this.year();return 9999>=a?""+a:"+"+a}),T(0,["YY",2],0,function(){return this.year()%100}),T(0,["YYYY",4],0,"year"),T(0,["YYYYY",5],0,"year"),T(0,["YYYYYY",6,!0],0,"year"),
|
||||
// ALIASES
|
||||
I("year","y"),
|
||||
// PRIORITIES
|
||||
L("year",1),
|
||||
// PARSING
|
||||
Y("Y",Rd),Y("YY",Kd,Gd),Y("YYYY",Od,Id),Y("YYYYY",Pd,Jd),Y("YYYYYY",Pd,Jd),aa(["YYYYY","YYYYYY"],Yd),aa("YYYY",function(b,c){c[Yd]=2===b.length?a.parseTwoDigitYear(b):t(b)}),aa("YY",function(b,c){c[Yd]=a.parseTwoDigitYear(b)}),aa("Y",function(a,b){b[Yd]=parseInt(a,10)}),
|
||||
// HOOKS
|
||||
a.parseTwoDigitYear=function(a){return t(a)+(t(a)>68?1900:2e3)};
|
||||
// MOMENTS
|
||||
var ke=N("FullYear",!0);
|
||||
// FORMATTING
|
||||
T("w",["ww",2],"wo","week"),T("W",["WW",2],"Wo","isoWeek"),
|
||||
// ALIASES
|
||||
I("week","w"),I("isoWeek","W"),
|
||||
// PRIORITIES
|
||||
L("week",5),L("isoWeek",5),
|
||||
// PARSING
|
||||
Y("w",Kd),Y("ww",Kd,Gd),Y("W",Kd),Y("WW",Kd,Gd),ba(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=t(a)});var le={dow:0,// Sunday is the first day of the week.
|
||||
doy:6};
|
||||
// FORMATTING
|
||||
T("d",0,"do","day"),T("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),T("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),T("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),T("e",0,0,"weekday"),T("E",0,0,"isoWeekday"),
|
||||
// ALIASES
|
||||
I("day","d"),I("weekday","e"),I("isoWeekday","E"),
|
||||
// PRIORITY
|
||||
L("day",11),L("weekday",11),L("isoWeekday",11),
|
||||
// PARSING
|
||||
Y("d",Kd),Y("e",Kd),Y("E",Kd),Y("dd",function(a,b){return b.weekdaysMinRegex(a)}),Y("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Y("dddd",function(a,b){return b.weekdaysRegex(a)}),ba(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);
|
||||
// if we didn't get a weekday name, mark the date as invalid
|
||||
null!=e?b.d=e:l(c).invalidWeekday=a}),ba(["d","e","E"],function(a,b,c,d){b[d]=t(a)});
|
||||
// LOCALES
|
||||
var me="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ne="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),oe="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),pe=Vd,qe=Vd,re=Vd;T("H",["HH",2],0,"hour"),T("h",["hh",2],0,Qa),T("k",["kk",2],0,Ra),T("hmm",0,0,function(){return""+Qa.apply(this)+S(this.minutes(),2)}),T("hmmss",0,0,function(){return""+Qa.apply(this)+S(this.minutes(),2)+S(this.seconds(),2)}),T("Hmm",0,0,function(){return""+this.hours()+S(this.minutes(),2)}),T("Hmmss",0,0,function(){return""+this.hours()+S(this.minutes(),2)+S(this.seconds(),2)}),Sa("a",!0),Sa("A",!1),
|
||||
// ALIASES
|
||||
I("hour","h"),
|
||||
// PRIORITY
|
||||
L("hour",13),Y("a",Ta),Y("A",Ta),Y("H",Kd),Y("h",Kd),Y("HH",Kd,Gd),Y("hh",Kd,Gd),Y("hmm",Ld),Y("hmmss",Md),Y("Hmm",Ld),Y("Hmmss",Md),aa(["H","HH"],_d),aa(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),aa(["h","hh"],function(a,b,c){b[_d]=t(a),l(c).bigHour=!0}),aa("hmm",function(a,b,c){var d=a.length-2;b[_d]=t(a.substr(0,d)),b[ae]=t(a.substr(d)),l(c).bigHour=!0}),aa("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[_d]=t(a.substr(0,d)),b[ae]=t(a.substr(d,2)),b[be]=t(a.substr(e)),l(c).bigHour=!0}),aa("Hmm",function(a,b,c){var d=a.length-2;b[_d]=t(a.substr(0,d)),b[ae]=t(a.substr(d))}),aa("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[_d]=t(a.substr(0,d)),b[ae]=t(a.substr(d,2)),b[be]=t(a.substr(e))});var se,te=/[ap]\.?m?\.?/i,ue=N("Hours",!0),ve={calendar:td,longDateFormat:ud,invalidDate:vd,ordinal:wd,ordinalParse:xd,relativeTime:yd,months:ge,monthsShort:he,week:le,weekdays:me,weekdaysMin:oe,weekdaysShort:ne,meridiemParse:te},we={},xe=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,ye=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,ze=/Z|[+-]\d\d(?::?\d\d)?/,Ae=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],
|
||||
// YYYYMM is NOT allowed by the standard
|
||||
["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Be=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ce=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=w("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),
|
||||
// constant that refers to the ISO standard
|
||||
a.ISO_8601=function(){};var De=w("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=rb.apply(null,arguments);return this.isValid()&&a.isValid()?this>a?this:a:n()}),Ee=w("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=rb.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:n()}),Fe=function(){return Date.now?Date.now():+new Date};yb("Z",":"),yb("ZZ",""),
|
||||
// PARSING
|
||||
Y("Z",Td),Y("ZZ",Td),aa(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=zb(Td,a)});
|
||||
// HELPERS
|
||||
// timezone chunker
|
||||
// '+10:00' > ['10', '00']
|
||||
// '-1530' > ['-15', '30']
|
||||
var Ge=/([\+\-]|\d\d)/gi;
|
||||
// HOOKS
|
||||
// This function will be called whenever a moment is mutated.
|
||||
// It is intended to keep the offset in sync with the timezone.
|
||||
a.updateOffset=function(){};
|
||||
// ASP.NET json date format regex
|
||||
var He=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Ie=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Nb.fn=vb.prototype;var Je=Rb(1,"add"),Ke=Rb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Le=w("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});
|
||||
// FORMATTING
|
||||
T(0,["gg",2],0,function(){return this.weekYear()%100}),T(0,["GG",2],0,function(){return this.isoWeekYear()%100}),xc("gggg","weekYear"),xc("ggggg","weekYear"),xc("GGGG","isoWeekYear"),xc("GGGGG","isoWeekYear"),
|
||||
// ALIASES
|
||||
I("weekYear","gg"),I("isoWeekYear","GG"),
|
||||
// PRIORITY
|
||||
L("weekYear",1),L("isoWeekYear",1),
|
||||
// PARSING
|
||||
Y("G",Rd),Y("g",Rd),Y("GG",Kd,Gd),Y("gg",Kd,Gd),Y("GGGG",Od,Id),Y("gggg",Od,Id),Y("GGGGG",Pd,Jd),Y("ggggg",Pd,Jd),ba(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=t(a)}),ba(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),
|
||||
// FORMATTING
|
||||
T("Q",0,"Qo","quarter"),
|
||||
// ALIASES
|
||||
I("quarter","Q"),
|
||||
// PRIORITY
|
||||
L("quarter",7),
|
||||
// PARSING
|
||||
Y("Q",Fd),aa("Q",function(a,b){b[Zd]=3*(t(a)-1)}),
|
||||
// FORMATTING
|
||||
T("D",["DD",2],"Do","date"),
|
||||
// ALIASES
|
||||
I("date","D"),
|
||||
// PRIOROITY
|
||||
L("date",9),
|
||||
// PARSING
|
||||
Y("D",Kd),Y("DD",Kd,Gd),Y("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),aa(["D","DD"],$d),aa("Do",function(a,b){b[$d]=t(a.match(Kd)[0],10)});
|
||||
// MOMENTS
|
||||
var Me=N("Date",!0);
|
||||
// FORMATTING
|
||||
T("DDD",["DDDD",3],"DDDo","dayOfYear"),
|
||||
// ALIASES
|
||||
I("dayOfYear","DDD"),
|
||||
// PRIORITY
|
||||
L("dayOfYear",4),
|
||||
// PARSING
|
||||
Y("DDD",Nd),Y("DDDD",Hd),aa(["DDD","DDDD"],function(a,b,c){c._dayOfYear=t(a)}),
|
||||
// FORMATTING
|
||||
T("m",["mm",2],0,"minute"),
|
||||
// ALIASES
|
||||
I("minute","m"),
|
||||
// PRIORITY
|
||||
L("minute",14),
|
||||
// PARSING
|
||||
Y("m",Kd),Y("mm",Kd,Gd),aa(["m","mm"],ae);
|
||||
// MOMENTS
|
||||
var Ne=N("Minutes",!1);
|
||||
// FORMATTING
|
||||
T("s",["ss",2],0,"second"),
|
||||
// ALIASES
|
||||
I("second","s"),
|
||||
// PRIORITY
|
||||
L("second",15),
|
||||
// PARSING
|
||||
Y("s",Kd),Y("ss",Kd,Gd),aa(["s","ss"],be);
|
||||
// MOMENTS
|
||||
var Oe=N("Seconds",!1);
|
||||
// FORMATTING
|
||||
T("S",0,0,function(){return~~(this.millisecond()/100)}),T(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),T(0,["SSS",3],0,"millisecond"),T(0,["SSSS",4],0,function(){return 10*this.millisecond()}),T(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),T(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),T(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),T(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),T(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),
|
||||
// ALIASES
|
||||
I("millisecond","ms"),
|
||||
// PRIORITY
|
||||
L("millisecond",16),
|
||||
// PARSING
|
||||
Y("S",Nd,Fd),Y("SS",Nd,Gd),Y("SSS",Nd,Hd);var Pe;for(Pe="SSSS";Pe.length<=9;Pe+="S")Y(Pe,Qd);for(Pe="S";Pe.length<=9;Pe+="S")aa(Pe,Gc);
|
||||
// MOMENTS
|
||||
var Qe=N("Milliseconds",!1);
|
||||
// FORMATTING
|
||||
T("z",0,0,"zoneAbbr"),T("zz",0,0,"zoneName");var Re=q.prototype;Re.add=Je,Re.calendar=Ub,Re.clone=Vb,Re.diff=ac,Re.endOf=mc,Re.format=ec,Re.from=fc,Re.fromNow=gc,Re.to=hc,Re.toNow=ic,Re.get=Q,Re.invalidAt=vc,Re.isAfter=Wb,Re.isBefore=Xb,Re.isBetween=Yb,Re.isSame=Zb,Re.isSameOrAfter=$b,Re.isSameOrBefore=_b,Re.isValid=tc,Re.lang=Le,Re.locale=jc,Re.localeData=kc,Re.max=Ee,Re.min=De,Re.parsingFlags=uc,Re.set=R,Re.startOf=lc,Re.subtract=Ke,Re.toArray=qc,Re.toObject=rc,Re.toDate=pc,Re.toISOString=dc,Re.toJSON=sc,Re.toString=cc,Re.unix=oc,Re.valueOf=nc,Re.creationData=wc,
|
||||
// Year
|
||||
Re.year=ke,Re.isLeapYear=qa,
|
||||
// Week Year
|
||||
Re.weekYear=yc,Re.isoWeekYear=zc,
|
||||
// Quarter
|
||||
Re.quarter=Re.quarters=Ec,
|
||||
// Month
|
||||
Re.month=ja,Re.daysInMonth=ka,
|
||||
// Week
|
||||
Re.week=Re.weeks=Aa,Re.isoWeek=Re.isoWeeks=Ba,Re.weeksInYear=Bc,Re.isoWeeksInYear=Ac,
|
||||
// Day
|
||||
Re.date=Me,Re.day=Re.days=Ja,Re.weekday=Ka,Re.isoWeekday=La,Re.dayOfYear=Fc,
|
||||
// Hour
|
||||
Re.hour=Re.hours=ue,
|
||||
// Minute
|
||||
Re.minute=Re.minutes=Ne,
|
||||
// Second
|
||||
Re.second=Re.seconds=Oe,
|
||||
// Millisecond
|
||||
Re.millisecond=Re.milliseconds=Qe,
|
||||
// Offset
|
||||
Re.utcOffset=Cb,Re.utc=Eb,Re.local=Fb,Re.parseZone=Gb,Re.hasAlignedHourOffset=Hb,Re.isDST=Ib,Re.isLocal=Kb,Re.isUtcOffset=Lb,Re.isUtc=Mb,Re.isUTC=Mb,
|
||||
// Timezone
|
||||
Re.zoneAbbr=Hc,Re.zoneName=Ic,
|
||||
// Deprecations
|
||||
Re.dates=w("dates accessor is deprecated. Use date instead.",Me),Re.months=w("months accessor is deprecated. Use month instead",ja),Re.years=w("years accessor is deprecated. Use year instead",ke),Re.zone=w("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Db),Re.isDSTShifted=w("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Jb);var Se=Re,Te=B.prototype;Te.calendar=C,Te.longDateFormat=D,Te.invalidDate=E,Te.ordinal=F,Te.preparse=Lc,Te.postformat=Lc,Te.relativeTime=G,Te.pastFuture=H,Te.set=z,
|
||||
// Month
|
||||
Te.months=ea,Te.monthsShort=fa,Te.monthsParse=ha,Te.monthsRegex=ma,Te.monthsShortRegex=la,
|
||||
// Week
|
||||
Te.week=xa,Te.firstDayOfYear=za,Te.firstDayOfWeek=ya,
|
||||
// Day of Week
|
||||
Te.weekdays=Ea,Te.weekdaysMin=Ga,Te.weekdaysShort=Fa,Te.weekdaysParse=Ia,Te.weekdaysRegex=Ma,Te.weekdaysShortRegex=Na,Te.weekdaysMinRegex=Oa,
|
||||
// Hours
|
||||
Te.isPM=Ua,Te.meridiem=Va,Za("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===t(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),
|
||||
// Side effect imports
|
||||
a.lang=w("moment.lang is deprecated. Use moment.locale instead.",Za),a.langData=w("moment.langData is deprecated. Use moment.localeData instead.",ab);var Ue=Math.abs,Ve=cd("ms"),We=cd("s"),Xe=cd("m"),Ye=cd("h"),Ze=cd("d"),$e=cd("w"),_e=cd("M"),af=cd("y"),bf=ed("milliseconds"),cf=ed("seconds"),df=ed("minutes"),ef=ed("hours"),ff=ed("days"),gf=ed("months"),hf=ed("years"),jf=Math.round,kf={s:45,// seconds to minute
|
||||
m:45,// minutes to hour
|
||||
h:22,// hours to day
|
||||
d:26,// days to month
|
||||
M:11},lf=Math.abs,mf=vb.prototype;mf.abs=Uc,mf.add=Wc,mf.subtract=Xc,mf.as=ad,mf.asMilliseconds=Ve,mf.asSeconds=We,mf.asMinutes=Xe,mf.asHours=Ye,mf.asDays=Ze,mf.asWeeks=$e,mf.asMonths=_e,mf.asYears=af,mf.valueOf=bd,mf._bubble=Zc,mf.get=dd,mf.milliseconds=bf,mf.seconds=cf,mf.minutes=df,mf.hours=ef,mf.days=ff,mf.weeks=fd,mf.months=gf,mf.years=hf,mf.humanize=kd,mf.toISOString=ld,mf.toString=ld,mf.toJSON=ld,mf.locale=jc,mf.localeData=kc,
|
||||
// Deprecations
|
||||
mf.toIsoString=w("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ld),mf.lang=Le,
|
||||
// Side effect imports
|
||||
// FORMATTING
|
||||
T("X",0,0,"unix"),T("x",0,0,"valueOf"),
|
||||
// PARSING
|
||||
Y("x",Rd),Y("X",Ud),aa("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),aa("x",function(a,b,c){c._d=new Date(t(a))}),
|
||||
// Side effect imports
|
||||
a.version="2.15.1",b(rb),a.fn=Se,a.min=tb,a.max=ub,a.now=Fe,a.utc=j,a.unix=Jc,a.months=Pc,a.isDate=f,a.locale=Za,a.invalid=n,a.duration=Nb,a.isMoment=r,a.weekdays=Rc,a.parseZone=Kc,a.localeData=ab,a.isDuration=wb,a.monthsShort=Qc,a.weekdaysMin=Tc,a.defineLocale=$a,a.updateLocale=_a,a.locales=bb,a.weekdaysShort=Sc,a.normalizeUnits=J,a.relativeTimeRounding=id,a.relativeTimeThreshold=jd,a.calendarFormat=Tb,a.prototype=Se;var nf=a;return nf});
|
3
server/easyctf/assets/js/selectize.min.js
vendored
Normal file
2
server/easyctf/assets/js/smooth-scroll.min.js
vendored
Executable file
|
@ -0,0 +1,2 @@
|
|||
/*! smooth-scroll v7.1.1 | (c) 2015 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */
|
||||
!function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.smoothScroll=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var t,n,o,r,a={},u="querySelector"in document&&"addEventListener"in e,c={selector:"[data-scroll]",selectorHeader:"[data-scroll-header]",speed:500,easing:"easeInOutCubic",offset:0,updateURL:!0,callback:function(){}},i=function(){var e={},t=!1,n=0,o=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],n++);for(var r=function(n){for(var o in n)Object.prototype.hasOwnProperty.call(n,o)&&(t&&"[object Object]"===Object.prototype.toString.call(n[o])?e[o]=i(!0,e[o],n[o]):e[o]=n[o])};o>n;n++){var a=arguments[n];r(a)}return e},s=function(e){return Math.max(e.scrollHeight,e.offsetHeight,e.clientHeight)},l=function(e,t){var n,o,r=t.charAt(0),a="classList"in document.documentElement;for("["===r&&(t=t.substr(1,t.length-2),n=t.split("="),n.length>1&&(o=!0,n[1]=n[1].replace(/"/g,"").replace(/'/g,"")));e&&e!==document;e=e.parentNode){if("."===r)if(a){if(e.classList.contains(t.substr(1)))return e}else if(new RegExp("(^|\\s)"+t.substr(1)+"(\\s|$)").test(e.className))return e;if("#"===r&&e.id===t.substr(1))return e;if("["===r&&e.hasAttribute(n[0])){if(!o)return e;if(e.getAttribute(n[0])===n[1])return e}if(e.tagName.toLowerCase()===t)return e}return null},f=function(e){for(var t,n=String(e),o=n.length,r=-1,a="",u=n.charCodeAt(0);++r<o;){if(t=n.charCodeAt(r),0===t)throw new InvalidCharacterError("Invalid character: the input contains U+0000.");a+=t>=1&&31>=t||127==t||0===r&&t>=48&&57>=t||1===r&&t>=48&&57>=t&&45===u?"\\"+t.toString(16)+" ":t>=128||45===t||95===t||t>=48&&57>=t||t>=65&&90>=t||t>=97&&122>=t?n.charAt(r):"\\"+n.charAt(r)}return a},d=function(e,t){var n;return"easeInQuad"===e&&(n=t*t),"easeOutQuad"===e&&(n=t*(2-t)),"easeInOutQuad"===e&&(n=.5>t?2*t*t:-1+(4-2*t)*t),"easeInCubic"===e&&(n=t*t*t),"easeOutCubic"===e&&(n=--t*t*t+1),"easeInOutCubic"===e&&(n=.5>t?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1),"easeInQuart"===e&&(n=t*t*t*t),"easeOutQuart"===e&&(n=1- --t*t*t*t),"easeInOutQuart"===e&&(n=.5>t?8*t*t*t*t:1-8*--t*t*t*t),"easeInQuint"===e&&(n=t*t*t*t*t),"easeOutQuint"===e&&(n=1+--t*t*t*t*t),"easeInOutQuint"===e&&(n=.5>t?16*t*t*t*t*t:1+16*--t*t*t*t*t),n||t},m=function(e,t,n){var o=0;if(e.offsetParent)do o+=e.offsetTop,e=e.offsetParent;while(e);return o=o-t-n,o>=0?o:0},h=function(){return Math.max(e.document.body.scrollHeight,e.document.documentElement.scrollHeight,e.document.body.offsetHeight,e.document.documentElement.offsetHeight,e.document.body.clientHeight,e.document.documentElement.clientHeight)},p=function(e){return e&&"object"==typeof JSON&&"function"==typeof JSON.parse?JSON.parse(e):{}},g=function(t,n){e.history.pushState&&(n||"true"===n)&&"file:"!==e.location.protocol&&e.history.pushState(null,null,[e.location.protocol,"//",e.location.host,e.location.pathname,e.location.search,t].join(""))},b=function(e){return null===e?0:s(e)+e.offsetTop};a.animateScroll=function(t,n,a){var u=p(t?t.getAttribute("data-options"):null),s=i(s||c,a||{},u);n="#"+f(n.substr(1));var l="#"===n?e.document.documentElement:e.document.querySelector(n),v=e.pageYOffset;o||(o=e.document.querySelector(s.selectorHeader)),r||(r=b(o));var y,O,S,I=m(l,r,parseInt(s.offset,10)),H=I-v,E=h(),L=0;g(n,s.updateURL);var j=function(o,r,a){var u=e.pageYOffset;(o==r||u==r||e.innerHeight+u>=E)&&(clearInterval(a),l.focus(),s.callback(t,n))},w=function(){L+=16,O=L/parseInt(s.speed,10),O=O>1?1:O,S=v+H*d(s.easing,O),e.scrollTo(0,Math.floor(S)),j(S,I,y)},C=function(){y=setInterval(w,16)};0===e.pageYOffset&&e.scrollTo(0,0),C()};var v=function(e){var n=l(e.target,t.selector);n&&"a"===n.tagName.toLowerCase()&&(e.preventDefault(),a.animateScroll(n,n.hash,t))},y=function(e){n||(n=setTimeout(function(){n=null,r=b(o)},66))};return a.destroy=function(){t&&(e.document.removeEventListener("click",v,!1),e.removeEventListener("resize",y,!1),t=null,n=null,o=null,r=null)},a.init=function(n){u&&(a.destroy(),t=i(c,n||{}),o=e.document.querySelector(t.selectorHeader),r=b(o),e.document.addEventListener("click",v,!1),o&&e.addEventListener("resize",y,!1))},a});
|
984
server/easyctf/assets/js/vn.js
Normal file
|
@ -0,0 +1,984 @@
|
|||
var __extends = (this && this.__extends) || function (d, b) {
|
||||
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
function get_child_node_of_class(node, cls) {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
if (node.childNodes[i].className == cls) {
|
||||
return node.childNodes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
var DialogueAction = (function () {
|
||||
function DialogueAction() {
|
||||
}
|
||||
return DialogueAction;
|
||||
}());
|
||||
var ShowCharacterAction = (function () {
|
||||
function ShowCharacterAction() {
|
||||
}
|
||||
return ShowCharacterAction;
|
||||
}());
|
||||
var HideCharacterAction = (function () {
|
||||
function HideCharacterAction() {
|
||||
}
|
||||
return HideCharacterAction;
|
||||
}());
|
||||
var ShowProblemAction = (function () {
|
||||
function ShowProblemAction() {
|
||||
}
|
||||
return ShowProblemAction;
|
||||
}());
|
||||
// TODO: actually deal with network errors
|
||||
var ServerAPI = (function () {
|
||||
function ServerAPI(urls, csrf_token) {
|
||||
if (csrf_token === void 0) { csrf_token = null; }
|
||||
this.urls = urls;
|
||||
this.csrf_token = csrf_token;
|
||||
}
|
||||
ServerAPI.prototype.get = function (url, callback) {
|
||||
var r = new XMLHttpRequest();
|
||||
r.open("GET", url, true);
|
||||
r.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var obj = JSON.parse(this.responseText);
|
||||
callback(obj);
|
||||
}
|
||||
};
|
||||
r.send();
|
||||
};
|
||||
ServerAPI.prototype.post = function (url, data, callback) {
|
||||
data['csrf_token'] = this.csrf_token; // TODO: actually copy the dict
|
||||
var r = new XMLHttpRequest();
|
||||
r.open("POST", url, true);
|
||||
r.setRequestHeader("Content-type", "application/json");
|
||||
r.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var obj = JSON.parse(this.responseText);
|
||||
callback(obj);
|
||||
}
|
||||
};
|
||||
r.send(JSON.stringify(data));
|
||||
};
|
||||
ServerAPI.prototype.get_game = function (callback) {
|
||||
this.get(this.urls['game'], callback);
|
||||
};
|
||||
ServerAPI.prototype.get_problems = function (callback) {
|
||||
this.get(this.urls['problems'], callback);
|
||||
};
|
||||
ServerAPI.prototype.get_state = function (callback) {
|
||||
this.get(this.urls['state'], callback);
|
||||
};
|
||||
ServerAPI.prototype.submit_flag = function (pid, flag, callback) {
|
||||
this.post(this.urls['submit'], { 'pid': pid, 'flag': flag }, function (obj) {
|
||||
callback(obj['result'], obj['message']);
|
||||
});
|
||||
};
|
||||
ServerAPI.prototype.upload_game_state = function (state, callback) {
|
||||
if (callback === void 0) { callback = null; }
|
||||
this.post(this.urls['game_state_update'], { 'state': JSON.stringify(state) }, function (obj) {
|
||||
if (callback !== null) {
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
return ServerAPI;
|
||||
}());
|
||||
var DebugServerAPI = (function (_super) {
|
||||
__extends(DebugServerAPI, _super);
|
||||
function DebugServerAPI(urls) {
|
||||
_super.call(this, urls);
|
||||
this.solved_problems = [];
|
||||
this.game_state = '{}';
|
||||
}
|
||||
DebugServerAPI.prototype.submit_flag = function (pid, flag, callback) {
|
||||
this.solved_problems.push(pid);
|
||||
callback('success', 'gj');
|
||||
};
|
||||
DebugServerAPI.prototype.upload_game_state = function (state, callback) {
|
||||
this.game_state = JSON.stringify(state);
|
||||
if (callback !== null) {
|
||||
callback(true);
|
||||
}
|
||||
};
|
||||
DebugServerAPI.prototype.get_problems = function (callback) {
|
||||
var _this = this;
|
||||
_super.prototype.get_problems.call(this, function (problems_obj) {
|
||||
for (var problem in problems_obj) {
|
||||
if (_this.solved_problems.indexOf(parseInt(problem)) !== -1) {
|
||||
problems_obj[problem]['solved'] = 1;
|
||||
}
|
||||
}
|
||||
callback(problems_obj);
|
||||
});
|
||||
};
|
||||
DebugServerAPI.prototype.get_state = function (callback) {
|
||||
callback(JSON.parse(this.game_state));
|
||||
};
|
||||
return DebugServerAPI;
|
||||
}(ServerAPI));
|
||||
var Master = (function () {
|
||||
function Master(server_api, debug) {
|
||||
if (debug === void 0) { debug = false; }
|
||||
this.element = document.getElementById('vn');
|
||||
this.scenes = {};
|
||||
this.current_scene = null;
|
||||
this.current_scene_name = null;
|
||||
this.current_scene_type = null;
|
||||
this.route = null;
|
||||
this.problem_triggers = {};
|
||||
this.routes = {};
|
||||
this.layout = new Layout();
|
||||
this.game_interface = new GameInterface(this.layout);
|
||||
this.problems = {};
|
||||
this.solved_problems = [];
|
||||
this.viewed_scenes = [];
|
||||
this.completed_routes = [];
|
||||
this.server_api = server_api;
|
||||
this.debug = debug;
|
||||
}
|
||||
Master.prototype.from_json = function (obj) {
|
||||
this.images = obj['images'] || {};
|
||||
this.layout.problem_list_widget.category_icons = obj['category_icons'] || {};
|
||||
this.scenes_from_json(obj['scenes'] || {});
|
||||
this.problem_triggers = obj['problem_triggers'] || {};
|
||||
this.routes = obj['routes'] || {};
|
||||
};
|
||||
Master.prototype.scenes_from_json = function (obj) {
|
||||
for (var key in obj) {
|
||||
var scene_data = obj[key];
|
||||
var scene = new Scene(this.layout, this);
|
||||
scene.id = key;
|
||||
scene.images = this.images;
|
||||
scene.background_image = scene_data['background_image'] || null;
|
||||
scene.choreography = scene_data['choreography'];
|
||||
scene.trigger_spec = scene_data['on'];
|
||||
this.scenes[key] = scene;
|
||||
}
|
||||
};
|
||||
// NEEDS TRIGGERS FIRST
|
||||
Master.prototype.problems_from_json = function (obj) {
|
||||
this.problems = {};
|
||||
this.solved_problems = [];
|
||||
for (var key in obj) {
|
||||
var problem = obj[key];
|
||||
if (this.problem_triggers[key] !== undefined && 'route' in this.problem_triggers[key]) {
|
||||
problem.is_route = true;
|
||||
}
|
||||
this.problems[problem.pid] = problem;
|
||||
if (obj[key]['solved']) {
|
||||
this.solved_problems.push(problem.pid);
|
||||
}
|
||||
}
|
||||
};
|
||||
Master.prototype.load_resources = function (callback) {
|
||||
var _this = this;
|
||||
this.server_api.get_game(function (obj) {
|
||||
_this.from_json(obj);
|
||||
var images_to_load = [];
|
||||
for (var image in _this.images) {
|
||||
images_to_load.push(_this.images[image]);
|
||||
}
|
||||
var progress_counter = new ProgressCounter(images_to_load.length);
|
||||
progress_counter.progress_callback = function (current, total) {
|
||||
document.getElementById('loading_display').innerHTML = 'Loading... ' + current + '/' + total;
|
||||
};
|
||||
progress_counter.done_callback = callback;
|
||||
for (var _i = 0, images_to_load_1 = images_to_load; _i < images_to_load_1.length; _i++) {
|
||||
var image_to_load = images_to_load_1[_i];
|
||||
var temp_img = new Image();
|
||||
// TODO: handle error
|
||||
temp_img.onload = function (e) {
|
||||
progress_counter.advance();
|
||||
};
|
||||
temp_img.src = image_to_load;
|
||||
}
|
||||
});
|
||||
};
|
||||
Master.prototype.fetch_problems = function (callback) {
|
||||
var _this = this;
|
||||
this.server_api.get_problems(function (obj) {
|
||||
_this.problems_from_json(obj);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
Master.prototype.update_problems_list = function () {
|
||||
var unsolved_problems = [];
|
||||
var solved_problems = [];
|
||||
for (var problem in this.problems) {
|
||||
if (!this.is_triggered(this.problem_triggers[problem])) {
|
||||
continue;
|
||||
}
|
||||
if (this.solved_problems.indexOf(parseInt(problem)) != -1) {
|
||||
solved_problems.push(this.problems[problem]);
|
||||
}
|
||||
else {
|
||||
unsolved_problems.push(this.problems[problem]);
|
||||
}
|
||||
}
|
||||
this.game_interface.set_problems(solved_problems, unsolved_problems);
|
||||
};
|
||||
Master.prototype.refresh_problems = function (network, callback) {
|
||||
var _this = this;
|
||||
if (network === void 0) { network = true; }
|
||||
var after = function () {
|
||||
if (_this.current_scene_type !== "dialogue") {
|
||||
_this.update_problems_list();
|
||||
}
|
||||
callback();
|
||||
};
|
||||
if (network) {
|
||||
this.fetch_problems(after);
|
||||
}
|
||||
else {
|
||||
after();
|
||||
}
|
||||
};
|
||||
Master.prototype.fetch_game_state = function (callback) {
|
||||
var _this = this;
|
||||
this.server_api.get_state(function (obj) {
|
||||
_this.viewed_scenes = (obj['scenes'] || []);
|
||||
_this.route = obj['route'] || null;
|
||||
_this.completed_routes = (obj['completed_routes'] || []);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
Master.prototype.generate_game_state = function () {
|
||||
return {
|
||||
'scenes': this.viewed_scenes,
|
||||
'route': this.route,
|
||||
'completed_routes': this.completed_routes
|
||||
};
|
||||
};
|
||||
Master.prototype.upload_game_state = function (callback) {
|
||||
if (callback === void 0) { callback = null; }
|
||||
this.server_api.upload_game_state(this.generate_game_state(), callback);
|
||||
};
|
||||
Master.prototype.refresh = function (callback, network) {
|
||||
var _this = this;
|
||||
if (network === void 0) { network = true; }
|
||||
var after = function () {
|
||||
_this.refresh_problems(network, function () {
|
||||
if (_this.check_for_route_trigger()) {
|
||||
_this.refresh(callback);
|
||||
return;
|
||||
}
|
||||
if (_this.check_for_route_completion()) {
|
||||
_this.refresh(callback);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
});
|
||||
};
|
||||
if (network) {
|
||||
this.fetch_game_state(after);
|
||||
}
|
||||
else {
|
||||
after();
|
||||
}
|
||||
};
|
||||
Master.prototype.set_scene = function (scene_name, scene_type) {
|
||||
this.current_scene_name = scene_name;
|
||||
this.current_scene = this.scenes[scene_name];
|
||||
this.current_scene_type = scene_type;
|
||||
};
|
||||
Master.prototype.start_problem_scene = function (scene_name) {
|
||||
var _this = this;
|
||||
if (this.is_dialogue_scene_active()) {
|
||||
return;
|
||||
}
|
||||
this.clear_scene();
|
||||
this.layout.character_elements[0].style.opacity = '0.7';
|
||||
this.set_scene(scene_name, "problem");
|
||||
this.current_scene.start(function () {
|
||||
// TODO: move this to be right after last dialogue of problem scene finishes
|
||||
_this.layout.dialogue_widget.dialogue_next_arrow_element.style.visibility = 'hidden';
|
||||
});
|
||||
};
|
||||
Master.prototype.start_dialogue_scene = function (scene_name) {
|
||||
var _this = this;
|
||||
if (this.is_dialogue_scene_active()) {
|
||||
return;
|
||||
}
|
||||
this.clear_scene();
|
||||
this.game_interface.disable();
|
||||
this.layout.problem_display_widget.hide();
|
||||
this.layout.background_element.style.opacity = '0.5';
|
||||
this.set_scene(scene_name, "dialogue");
|
||||
var current_scene = this.current_scene;
|
||||
this.current_scene.start(function () {
|
||||
_this.scene_finished(current_scene);
|
||||
});
|
||||
};
|
||||
Master.prototype.scene_finished = function (scene) {
|
||||
var _this = this;
|
||||
this.viewed_scenes.push(scene.id);
|
||||
this.clear_scene();
|
||||
this.upload_game_state(function () {
|
||||
_this.refresh(function () {
|
||||
if (!_this.check_for_scene_trigger()) {
|
||||
_this.game_interface.reset_display();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
Master.prototype.clear_scene = function () {
|
||||
this.layout.reset_scene_elements();
|
||||
this.current_scene = null;
|
||||
this.current_scene_name = null;
|
||||
this.current_scene_type = null;
|
||||
this.game_interface.reset_display();
|
||||
};
|
||||
Master.prototype.is_scene_active = function () {
|
||||
return this.current_scene !== null;
|
||||
};
|
||||
Master.prototype.is_problem_scene_active = function () {
|
||||
return this.current_scene_type === "problem";
|
||||
};
|
||||
Master.prototype.is_dialogue_scene_active = function () {
|
||||
return this.current_scene_type === "dialogue";
|
||||
};
|
||||
Master.prototype.check_for_scene_trigger = function () {
|
||||
for (var scene in this.scenes) {
|
||||
if (this.viewed_scenes.indexOf(scene) != -1)
|
||||
continue;
|
||||
if (this.is_triggered(this.scenes[scene].trigger_spec)) {
|
||||
// handle if is dialogue or not
|
||||
this.start_dialogue_scene(scene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Master.prototype.check_for_problem_scene = function (problem_id) {
|
||||
for (var scene in this.scenes) {
|
||||
if (this.is_triggered_for_problem(this.scenes[scene].trigger_spec, problem_id)) {
|
||||
this.start_problem_scene(scene);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Master.prototype.check_for_route_trigger = function () {
|
||||
if (this.route !== null) {
|
||||
return false;
|
||||
}
|
||||
for (var route in this.routes) {
|
||||
if (this.completed_routes.indexOf(parseInt(route)) !== -1) {
|
||||
continue;
|
||||
}
|
||||
if (this.is_triggered(this.routes[route]['trigger_spec'])) {
|
||||
this.route = parseInt(route);
|
||||
this.upload_game_state();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Master.prototype.check_for_route_completion = function () {
|
||||
if (this.route === null) {
|
||||
return false;
|
||||
}
|
||||
if (this.is_triggered(this.routes[this.route]['release_spec'])) {
|
||||
this.completed_routes.push(this.route);
|
||||
this.route = null;
|
||||
this.upload_game_state();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// passive triggering
|
||||
// all trigger keys are ANDed
|
||||
Master.prototype.is_triggered = function (trigger) {
|
||||
var _this = this;
|
||||
if (trigger === undefined)
|
||||
return false;
|
||||
var trigger_handlers = {
|
||||
'after_scenes': function (scenes) {
|
||||
for (var _i = 0, scenes_1 = scenes; _i < scenes_1.length; _i++) {
|
||||
var scene = scenes_1[_i];
|
||||
if (_this.viewed_scenes.indexOf(scene) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
'problems_solved': function (spec) {
|
||||
var solved = 0;
|
||||
var problems = spec['problems'] || [];
|
||||
var categories = spec['categories'] || [];
|
||||
for (var _i = 0, _a = _this.solved_problems; _i < _a.length; _i++) {
|
||||
var pid = _a[_i];
|
||||
if (!_this.is_triggered(_this.problem_triggers[pid])) {
|
||||
continue;
|
||||
}
|
||||
if (problems.indexOf(pid) !== -1 || categories.indexOf(_this.problems[pid].category) !== -1) {
|
||||
solved++;
|
||||
}
|
||||
}
|
||||
return solved >= spec['thresh'];
|
||||
},
|
||||
'return': function (result) {
|
||||
return result;
|
||||
},
|
||||
'route_active': function (route) {
|
||||
return _this.route === route;
|
||||
},
|
||||
'route': function (route) {
|
||||
return _this.route === route || _this.completed_routes.indexOf(route) !== -1;
|
||||
}
|
||||
};
|
||||
var result = true;
|
||||
var processed_triggers = 0;
|
||||
for (var trigger_key in trigger_handlers) {
|
||||
if (trigger[trigger_key] !== undefined) {
|
||||
if (!trigger_handlers[trigger_key](trigger[trigger_key])) {
|
||||
result = false;
|
||||
}
|
||||
processed_triggers++;
|
||||
}
|
||||
}
|
||||
return processed_triggers > 0 && result;
|
||||
};
|
||||
Master.prototype.is_triggered_for_problem = function (trigger, problem_id) {
|
||||
return trigger !== undefined && trigger['problem'] == problem_id;
|
||||
};
|
||||
Master.prototype.init = function () {
|
||||
var _this = this;
|
||||
this.register_event_listener();
|
||||
this.game_interface.flag_submitted_callback = function (problem, flag) {
|
||||
_this.server_api.submit_flag(problem.pid, flag, function (result, message) {
|
||||
if (result === 'success') {
|
||||
_this.layout.problem_display_widget.set_submit_response_color('#0f0');
|
||||
}
|
||||
else if (result === 'error') {
|
||||
_this.layout.problem_display_widget.set_submit_response_color('#ff0');
|
||||
}
|
||||
else if (result === 'failure') {
|
||||
_this.layout.problem_display_widget.set_submit_response_color('#f00');
|
||||
}
|
||||
setTimeout(function () {
|
||||
_this.layout.problem_display_widget.clear_submit_response_color();
|
||||
if (result === "success" && _this.layout.problem_display_widget.get_current_pid() === problem.pid) {
|
||||
_this.layout.problem_display_widget.set_problem(null);
|
||||
}
|
||||
}, 2000);
|
||||
_this.refresh(function () {
|
||||
_this.check_for_scene_trigger();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
Master.prototype.start = function () {
|
||||
var _this = this;
|
||||
this.load_resources(function () {
|
||||
_this.refresh(function () {
|
||||
document.getElementById('loading_screen').style.display = "none";
|
||||
document.getElementById('vn_scene').style.display = "initial";
|
||||
_this.layout.reset_scene_elements();
|
||||
_this.game_interface.disable();
|
||||
_this.game_interface.images = _this.images;
|
||||
_this.game_interface.problem_selected_callback = function (problem) {
|
||||
_this.clear_scene();
|
||||
_this.layout.problem_display_widget.clear_submit_response_color();
|
||||
_this.check_for_problem_scene(problem.pid);
|
||||
};
|
||||
if (!_this.check_for_scene_trigger()) {
|
||||
_this.game_interface.reset_display();
|
||||
}
|
||||
/*this.current_scene.start(() => {
|
||||
this.scene_finished(this.current_scene)
|
||||
});*/
|
||||
});
|
||||
});
|
||||
};
|
||||
Master.prototype.keydown = function (event) {
|
||||
var handled = false;
|
||||
if (this.current_scene !== null) {
|
||||
if (this.current_scene.keydown(event)) {
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
Master.prototype.register_event_listener = function () {
|
||||
var _this = this;
|
||||
document.addEventListener('keydown', function (event) {
|
||||
_this.keydown(event);
|
||||
}, false);
|
||||
};
|
||||
return Master;
|
||||
}());
|
||||
var ProgressCounter = (function () {
|
||||
function ProgressCounter(total) {
|
||||
this.done = false;
|
||||
this.current = 0;
|
||||
this.total = total;
|
||||
}
|
||||
ProgressCounter.prototype.log = function () {
|
||||
console.log(this.current + '/' + this.total);
|
||||
};
|
||||
ProgressCounter.prototype.advance = function () {
|
||||
this.current++;
|
||||
this.progress_callback(this.current, this.total);
|
||||
if (this.current >= this.total) {
|
||||
this.finish();
|
||||
}
|
||||
};
|
||||
ProgressCounter.prototype.finish = function () {
|
||||
this.done = true;
|
||||
if (this.done_callback !== undefined) {
|
||||
this.done_callback();
|
||||
}
|
||||
};
|
||||
return ProgressCounter;
|
||||
}());
|
||||
var Layout = (function () {
|
||||
function Layout() {
|
||||
this.element = document.getElementById('vn_scene');
|
||||
this.background_element = document.getElementById('scene_background');
|
||||
this.character_elements = [
|
||||
document.getElementById('character_1'),
|
||||
document.getElementById('character_2'),
|
||||
document.getElementById('character_3'),
|
||||
];
|
||||
this.speaker_element = document.getElementById('speaker_display_inner');
|
||||
this.dialogue_widget = new TextWidget(function () {
|
||||
});
|
||||
this.problem_display_widget = new ProblemDisplayWidget();
|
||||
this.problem_list_widget = new ProblemListWidget(this.problem_display_widget);
|
||||
}
|
||||
Layout.prototype.reset_scene_elements = function () {
|
||||
for (var _i = 0, _a = this.character_elements; _i < _a.length; _i++) {
|
||||
var element = _a[_i];
|
||||
element.style.display = 'none';
|
||||
element.style.opacity = '1.0';
|
||||
}
|
||||
//this.background_element.style.backgroundImage = 'url("' + this.images[this.background_image] + '")';
|
||||
this.background_element.style.opacity = '1.0';
|
||||
this.dialogue_widget.setText('');
|
||||
this.dialogue_widget.finish_now();
|
||||
this.speaker_element.parentElement.style.display = 'none';
|
||||
this.dialogue_widget.dialogue_next_arrow_element.style.visibility = 'hidden';
|
||||
};
|
||||
Layout.prototype.set_background = function (image_url) {
|
||||
this.background_element.style.backgroundImage = 'url("' + image_url + '")';
|
||||
};
|
||||
return Layout;
|
||||
}());
|
||||
var GameInterface = (function () {
|
||||
function GameInterface(layout) {
|
||||
this.background_image = 'game_interface_bg';
|
||||
this.layout = layout;
|
||||
this.solved_problems = [];
|
||||
this.unsolved_problems = [];
|
||||
}
|
||||
GameInterface.prototype.repopulate = function () {
|
||||
this.layout.problem_list_widget.populate(this.unsolved_problems);
|
||||
};
|
||||
GameInterface.prototype.set_problems = function (solved, unsolved) {
|
||||
this.solved_problems = solved;
|
||||
this.unsolved_problems = unsolved;
|
||||
this.repopulate();
|
||||
};
|
||||
GameInterface.prototype.reset_background = function () {
|
||||
this.layout.set_background(this.images[this.background_image]);
|
||||
};
|
||||
GameInterface.prototype.reset_display = function () {
|
||||
this.enable();
|
||||
this.show();
|
||||
this.repopulate();
|
||||
this.reset_background();
|
||||
// reset problem view too
|
||||
};
|
||||
// TODO: actually show/hide things
|
||||
GameInterface.prototype.show = function () {
|
||||
this.layout.problem_list_widget.show();
|
||||
this.layout.problem_display_widget.show();
|
||||
};
|
||||
GameInterface.prototype.hide = function () {
|
||||
this.layout.problem_list_widget.hide();
|
||||
this.layout.problem_display_widget.hide();
|
||||
};
|
||||
GameInterface.prototype.enable = function () {
|
||||
this.layout.problem_list_widget.enable();
|
||||
this.layout.problem_display_widget.enable();
|
||||
};
|
||||
GameInterface.prototype.disable = function () {
|
||||
this.layout.problem_list_widget.disable();
|
||||
this.layout.problem_display_widget.disable();
|
||||
};
|
||||
Object.defineProperty(GameInterface.prototype, "problem_selected_callback", {
|
||||
set: function (callback) {
|
||||
this.layout.problem_list_widget.problem_selected_callback = callback;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(GameInterface.prototype, "flag_submitted_callback", {
|
||||
set: function (callback) {
|
||||
this.layout.problem_display_widget.flag_submitted_callback = callback;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
return GameInterface;
|
||||
}());
|
||||
var Scene = (function () {
|
||||
function Scene(layout, master) {
|
||||
this.started = false;
|
||||
this.done = false;
|
||||
this.pos = 0;
|
||||
this.layout = layout;
|
||||
this.choreography = [];
|
||||
this.background_image = null;
|
||||
this.master = master;
|
||||
}
|
||||
Scene.prototype.keydown = function (event) {
|
||||
if (!this.started)
|
||||
return false;
|
||||
switch (event.keyCode) {
|
||||
case 32:
|
||||
case 39:
|
||||
case 40:
|
||||
if (!this.layout.dialogue_widget.done) {
|
||||
this.layout.dialogue_widget.finish_now();
|
||||
}
|
||||
else {
|
||||
this.nextAction();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Scene.prototype.nextAction = function () {
|
||||
if (this.pos == this.choreography.length) {
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
var current_action = this.choreography[this.pos];
|
||||
this.pos++;
|
||||
if (current_action.action == 'dialogue') {
|
||||
var action = current_action;
|
||||
this.layout.speaker_element.innerHTML = action.speaker;
|
||||
if (action.speaker === null) {
|
||||
this.layout.speaker_element.parentElement.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
this.layout.speaker_element.parentElement.style.display = 'initial';
|
||||
}
|
||||
this.layout.dialogue_widget.setText(action.text);
|
||||
this.layout.dialogue_widget.start(30);
|
||||
}
|
||||
else if (current_action.action == 'show_character') {
|
||||
var action = current_action;
|
||||
this.layout.character_elements[action.position - 1].style.backgroundImage = 'url("' + this.images[action.sprite] + '")';
|
||||
this.layout.character_elements[action.position - 1].style.display = 'initial';
|
||||
this.nextAction();
|
||||
}
|
||||
else if (current_action.action == 'hide_character') {
|
||||
var action = current_action;
|
||||
this.layout.character_elements[action.position - 1].style.display = 'none';
|
||||
this.nextAction();
|
||||
}
|
||||
else if (current_action.action == 'show_problem') {
|
||||
var action = current_action;
|
||||
this.layout.problem_list_widget.add_highlighted_problem(this.master.problems[action.problem_id]);
|
||||
this.layout.problem_display_widget.set_problem(this.master.problems[action.problem_id]);
|
||||
this.nextAction();
|
||||
}
|
||||
else {
|
||||
this.nextAction();
|
||||
}
|
||||
};
|
||||
Scene.prototype.finish = function () {
|
||||
this.done = true;
|
||||
if (this.done_callback !== undefined) {
|
||||
this.done_callback();
|
||||
}
|
||||
};
|
||||
Scene.prototype.reset_display = function () {
|
||||
for (var _i = 0, _a = this.layout.character_elements; _i < _a.length; _i++) {
|
||||
var element = _a[_i];
|
||||
element.style.display = 'none';
|
||||
}
|
||||
if (this.background_image !== null) {
|
||||
this.layout.background_element.style.backgroundImage = 'url("' + this.images[this.background_image] + '")';
|
||||
}
|
||||
this.layout.dialogue_widget.setText('');
|
||||
this.layout.dialogue_widget.finish_now();
|
||||
this.layout.speaker_element.parentElement.style.display = 'none';
|
||||
//this.speaker_element.innerHTML = '';
|
||||
};
|
||||
Scene.prototype.reset = function () {
|
||||
this.done = false;
|
||||
this.pos = 0;
|
||||
};
|
||||
Scene.prototype.start = function (done_callback) {
|
||||
if (done_callback === void 0) { done_callback = undefined; }
|
||||
this.done_callback = done_callback;
|
||||
this.reset();
|
||||
this.started = true;
|
||||
this.reset_display();
|
||||
this.nextAction();
|
||||
};
|
||||
return Scene;
|
||||
}());
|
||||
var TextWidget = (function () {
|
||||
function TextWidget(finished_callback) {
|
||||
if (finished_callback === void 0) { finished_callback = null; }
|
||||
this.done = false;
|
||||
this.started = false;
|
||||
this.pos = 0;
|
||||
this.lastTick = -1;
|
||||
this.element = document.getElementById('dialogue_box_inner');
|
||||
this.dialogue_next_arrow_element = document.getElementById('dialogue_next_arrow');
|
||||
this.finished_callback = finished_callback;
|
||||
}
|
||||
TextWidget.prototype.setText = function (text) {
|
||||
this.text = text;
|
||||
this.pos = 0;
|
||||
};
|
||||
TextWidget.prototype.start = function (tick_ms) {
|
||||
var _this = this;
|
||||
if (this.pos == this.text.length) {
|
||||
this.finish();
|
||||
}
|
||||
else {
|
||||
this.started = true;
|
||||
this.done = false;
|
||||
this.lastTick = performance.now();
|
||||
this.dialogue_next_arrow_element.style.visibility = 'hidden';
|
||||
window.requestAnimationFrame(function (now) { return _this.tick(now, tick_ms); });
|
||||
}
|
||||
};
|
||||
TextWidget.prototype.finish = function () {
|
||||
this.done = true;
|
||||
this.dialogue_next_arrow_element.style.visibility = 'inherit';
|
||||
if (this.finished_callback !== null) {
|
||||
this.finished_callback();
|
||||
}
|
||||
};
|
||||
TextWidget.prototype.finish_now = function () {
|
||||
this.finish();
|
||||
this.pos = this.text.length;
|
||||
this.element.innerHTML = this.text;
|
||||
};
|
||||
// TODO: longer wait times on punctuation
|
||||
TextWidget.prototype.tick = function (now, tick_ms) {
|
||||
var _this = this;
|
||||
if ((now - this.lastTick) / tick_ms >= 1) {
|
||||
this.pos = Math.min(this.pos + Math.floor((now - this.lastTick) / tick_ms), this.text.length);
|
||||
this.element.innerHTML = this.text.substring(0, this.pos);
|
||||
this.lastTick = now;
|
||||
}
|
||||
if (this.pos == this.text.length) {
|
||||
if (!this.done) {
|
||||
this.finish();
|
||||
}
|
||||
}
|
||||
else {
|
||||
window.requestAnimationFrame(function (now) { return _this.tick(now, tick_ms); });
|
||||
}
|
||||
};
|
||||
return TextWidget;
|
||||
}());
|
||||
var Problem = (function () {
|
||||
function Problem() {
|
||||
this.is_route = false;
|
||||
}
|
||||
return Problem;
|
||||
}());
|
||||
var ProblemListWidget = (function () {
|
||||
function ProblemListWidget(problem_display_widget) {
|
||||
this.element = document.getElementById('problem_list_box_inner');
|
||||
this.problem_list_element = document.getElementById('problem_list');
|
||||
this.problem_list_route_element = document.getElementById('problem_list_route');
|
||||
this.problem_list_normal_element = document.getElementById('problem_list_normal');
|
||||
this.template_entry_element = document.getElementById('problem_entry_template');
|
||||
this.problem_hover_label_element = document.getElementById('problem_hover_label');
|
||||
this.problem_display_widget = problem_display_widget;
|
||||
this.problems = [];
|
||||
this.highlighted = [];
|
||||
this.enabled = true;
|
||||
}
|
||||
Object.defineProperty(ProblemListWidget.prototype, "problem_selected_callback", {
|
||||
set: function (value) {
|
||||
this._problem_selected_callback = value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
ProblemListWidget.prototype.show = function () {
|
||||
this.element.style.visibility = 'visible';
|
||||
};
|
||||
ProblemListWidget.prototype.hide = function () {
|
||||
this.element.style.visibility = 'hidden';
|
||||
};
|
||||
ProblemListWidget.prototype.enable = function () {
|
||||
this.enabled = true;
|
||||
};
|
||||
ProblemListWidget.prototype.disable = function () {
|
||||
this.enabled = false;
|
||||
};
|
||||
ProblemListWidget.prototype.populate = function (problems, highlghted) {
|
||||
if (highlghted === void 0) { highlghted = null; }
|
||||
if (highlghted === null) {
|
||||
highlghted = [];
|
||||
}
|
||||
this.problems = problems;
|
||||
this.highlighted = highlghted;
|
||||
this.repopulate();
|
||||
};
|
||||
ProblemListWidget.prototype.repopulate = function () {
|
||||
var _this = this;
|
||||
this.problems.sort(function (p1, p2) {
|
||||
return p1.value - p2.value;
|
||||
});
|
||||
this.problem_list_route_element.innerHTML = '';
|
||||
this.problem_list_normal_element.innerHTML = '';
|
||||
this.problem_list_route_element.style.display = 'none';
|
||||
var _loop_1 = function(problem) {
|
||||
var problem_element = this_1.template_entry_element.cloneNode(true);
|
||||
problem_element.style.visibility = 'inherit';
|
||||
problem_element.style.backgroundColor = 'blue';
|
||||
if (this_1.highlighted.indexOf(problem.pid) !== -1) {
|
||||
problem_element.style.border = '2px yellow solid';
|
||||
}
|
||||
//problem_element.style.backgroundImage = 'url(' + this.category_icons[problem.category] + ')';
|
||||
var point_value_label = get_child_node_of_class(problem_element, 'point_value_label');
|
||||
point_value_label.innerText = problem.value.toString();
|
||||
problem_element.addEventListener('mouseenter', function (e) {
|
||||
// TODO: make this disappear when mouse leaves
|
||||
_this.problem_hover_label_element.innerText = problem.title + ' - ' + problem.category + ' ' + problem.value;
|
||||
});
|
||||
problem_element.addEventListener('mouseleave', function (e) {
|
||||
// TODO: make this disappear when mouse leaves
|
||||
_this.problem_hover_label_element.innerText = '';
|
||||
});
|
||||
problem_element.addEventListener('click', function (e) {
|
||||
if (_this.enabled) {
|
||||
_this.problem_display_widget.set_problem(problem);
|
||||
_this._problem_selected_callback(problem);
|
||||
}
|
||||
});
|
||||
if (problem.is_route) {
|
||||
this_1.problem_list_route_element.appendChild(problem_element);
|
||||
this_1.problem_list_route_element.style.display = 'block';
|
||||
}
|
||||
else {
|
||||
this_1.problem_list_normal_element.appendChild(problem_element);
|
||||
}
|
||||
};
|
||||
var this_1 = this;
|
||||
for (var _i = 0, _a = this.problems; _i < _a.length; _i++) {
|
||||
var problem = _a[_i];
|
||||
_loop_1(problem);
|
||||
}
|
||||
};
|
||||
ProblemListWidget.prototype.add_highlighted_problem = function (problem) {
|
||||
this.highlighted.push(problem.pid);
|
||||
this.problems.push(problem);
|
||||
this.repopulate();
|
||||
};
|
||||
return ProblemListWidget;
|
||||
}());
|
||||
var ProblemDisplayWidget = (function () {
|
||||
function ProblemDisplayWidget() {
|
||||
var _this = this;
|
||||
this.problem = null;
|
||||
this._flag_submitted_callback = null;
|
||||
this.element = document.getElementById('problem_box_inner');
|
||||
this.title_element = document.getElementById('problem_title');
|
||||
this.body_element = document.getElementById('problem_body');
|
||||
this.flag_box_element = document.getElementById('flag_box');
|
||||
this.flag_input_box_element = document.getElementById('flag_input_box');
|
||||
this.input_element = document.getElementById('flag_input');
|
||||
this.submit_element = document.getElementById('flag_submit_button');
|
||||
this.programming_button_element = document.getElementById('flag_programming_button');
|
||||
this.programming_button_form_element = document.getElementById('flag_programming_form_button');
|
||||
this.input_element.addEventListener('keydown', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
_this.handle_submit();
|
||||
}
|
||||
});
|
||||
this.submit_element.addEventListener('click', function (e) {
|
||||
_this.handle_submit();
|
||||
});
|
||||
this.hide();
|
||||
}
|
||||
ProblemDisplayWidget.prototype.set_problem = function (problem) {
|
||||
this.problem = problem;
|
||||
this.input_element.value = '';
|
||||
if (problem === null) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
// TODO: prettier display
|
||||
this.title_element.innerHTML = problem.title + ' - ' + problem.category + ' ' + problem.value;
|
||||
this.body_element.innerHTML = problem.description;
|
||||
if (this.problem.programming) {
|
||||
this.programming_button_form_element.action = '/chals/programming/' + problem.pid;
|
||||
this.programming_button_element.style.visibility = 'inherit';
|
||||
this.flag_input_box_element.style.visibility = 'hidden';
|
||||
}
|
||||
else {
|
||||
this.programming_button_element.style.visibility = 'hidden';
|
||||
this.flag_input_box_element.style.visibility = 'inherit';
|
||||
}
|
||||
this.show();
|
||||
};
|
||||
ProblemDisplayWidget.prototype.get_current_pid = function () {
|
||||
if (this.problem === null) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return this.problem.pid;
|
||||
}
|
||||
};
|
||||
ProblemDisplayWidget.prototype.show = function () {
|
||||
if (this.problem === null) {
|
||||
return;
|
||||
}
|
||||
this.element.style.visibility = 'visible';
|
||||
};
|
||||
ProblemDisplayWidget.prototype.hide = function () {
|
||||
this.element.style.visibility = 'hidden';
|
||||
};
|
||||
ProblemDisplayWidget.prototype.enable = function () {
|
||||
this.input_element.disabled = false;
|
||||
this.submit_element.disabled = false;
|
||||
this.programming_button_element.disabled = false;
|
||||
};
|
||||
ProblemDisplayWidget.prototype.disable = function () {
|
||||
this.input_element.disabled = true;
|
||||
this.submit_element.disabled = true;
|
||||
this.programming_button_element.disabled = true;
|
||||
};
|
||||
Object.defineProperty(ProblemDisplayWidget.prototype, "flag_submitted_callback", {
|
||||
set: function (callback) {
|
||||
this._flag_submitted_callback = callback;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
ProblemDisplayWidget.prototype.handle_submit = function () {
|
||||
if (this._flag_submitted_callback !== null) {
|
||||
this._flag_submitted_callback(this.problem, this.input_element.value);
|
||||
}
|
||||
};
|
||||
ProblemDisplayWidget.prototype.set_submit_response_color = function (color) {
|
||||
this.input_element.style.borderColor = color;
|
||||
this.submit_element.style.borderColor = color;
|
||||
};
|
||||
ProblemDisplayWidget.prototype.clear_submit_response_color = function () {
|
||||
this.input_element.style.borderColor = '#fff';
|
||||
this.submit_element.style.borderColor = '#fff';
|
||||
};
|
||||
return ProblemDisplayWidget;
|
||||
}());
|
81
server/easyctf/config.py
Executable file
|
@ -0,0 +1,81 @@
|
|||
import pickle
|
||||
import os
|
||||
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)
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self, app_root=None, testing=False):
|
||||
if app_root is None:
|
||||
self.app_root = pathlib.Path(
|
||||
os.path.dirname(os.path.abspath(__file__)))
|
||||
else:
|
||||
self.app_root = pathlib.Path(app_root)
|
||||
|
||||
self.TESTING = False
|
||||
self.SECRET_KEY = self._load_secret_key()
|
||||
self.SQLALCHEMY_DATABASE_URI = self._get_database_url()
|
||||
self.SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
self.PREFERRED_URL_SCHEME = "https"
|
||||
|
||||
self.CACHE_TYPE = "easyctf.config.cache"
|
||||
self.CACHE_REDIS_HOST = os.getenv("CACHE_REDIS_HOST", "redis")
|
||||
|
||||
self.ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
|
||||
self.EMAIL_VERIFICATION_REQUIRED = int(os.getenv(
|
||||
"EMAIL_VERIFICATION_REQUIRED", "1" if self.ENVIRONMENT == "production" else "0"))
|
||||
|
||||
self.FILESTORE_SAVE_ENDPOINT = os.getenv(
|
||||
"FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save")
|
||||
self.FILESTORE_STATIC = os.getenv("FILESTORE_STATIC", "/static")
|
||||
|
||||
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", "")
|
||||
|
||||
self.ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "")
|
||||
self.MAILGUN_URL = os.getenv("MAILGUN_URL", "")
|
||||
self.MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
|
||||
|
||||
if self.ENVIRONMENT == "development":
|
||||
self.DEBUG = True
|
||||
self.TEMPLATES_AUTO_RELOAD = True
|
||||
|
||||
if testing or self.ENVIRONMENT == "testing":
|
||||
test_db_path = os.path.join(os.path.dirname(__file__), "test.db")
|
||||
self.SQLALCHEMY_DATABASE_URI = "sqlite:///%s" % test_db_path
|
||||
if not os.path.exists(test_db_path):
|
||||
with open(test_db_path, "a"):
|
||||
os.utime(test_db_path, None)
|
||||
self.TESTING = True
|
||||
self.WTF_CSRF_ENABLED = False
|
||||
|
||||
def _load_secret_key(self):
|
||||
key = os.environ.get("SECRET_KEY")
|
||||
if key:
|
||||
return key
|
||||
logging.fatal("No SECRET_KEY specified. Exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
@staticmethod
|
||||
def _get_database_url():
|
||||
url = os.getenv("DATABASE_URL")
|
||||
if url:
|
||||
return url
|
||||
return "mysql://root:%s@db/%s" % (os.getenv("MYSQL_ROOT_PASSWORD"), os.getenv("MYSQL_DATABASE"))
|
14
server/easyctf/constants.py
Executable file
|
@ -0,0 +1,14 @@
|
|||
FORGOT_EMAIL_TEMPLATE = open("forgot.mail").read()
|
||||
REGISTRATION_EMAIL_TEMPLATE = open("registration.mail").read()
|
||||
|
||||
USER_LEVELS = ["Administrator", "Student", "Observer", "Teacher"]
|
||||
USER_REGULAR = 1
|
||||
USER_OBSERVER = 2
|
||||
USER_TEACHER = 3
|
||||
|
||||
SUPPORTED_LANGUAGES = {
|
||||
"cxx": "C++",
|
||||
"python2": "Python 2",
|
||||
"python3": "Python 3",
|
||||
"java": "Java",
|
||||
}
|
89
server/easyctf/decorators.py
Executable file
|
@ -0,0 +1,89 @@
|
|||
from datetime import datetime
|
||||
from functools import wraps, update_wrapper
|
||||
|
||||
from flask import abort, flash, redirect, url_for, session, make_response
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from easyctf.models import Config
|
||||
|
||||
|
||||
def email_verification_required(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not (current_user.is_authenticated and current_user.email_verified):
|
||||
session.pop("_flashes", None)
|
||||
flash("You need to verify your email first.", "warning")
|
||||
return redirect(url_for("users.settings"))
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def admin_required(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not (current_user.is_authenticated and current_user.admin):
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def teacher_required(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not (current_user.is_authenticated and current_user.level == 3):
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def block_before_competition(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start_time = Config.get("start_time")
|
||||
if not current_user.is_authenticated or not (current_user.admin or (start_time and current_user.is_authenticated and datetime.utcnow() >= datetime.fromtimestamp(int(start_time)))):
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def block_after_competition(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
end_time = Config.get("end_time")
|
||||
if not current_user.is_authenticated or not (current_user.admin or (end_time and current_user.is_authenticated and datetime.utcnow() <= datetime.fromtimestamp(int(end_time)))):
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def team_required(func):
|
||||
@wraps(func)
|
||||
@login_required
|
||||
def wrapper(*args, **kwargs):
|
||||
if not hasattr(current_user, "team") or not current_user.tid:
|
||||
flash("You need a team to view this page!", "info")
|
||||
return redirect(url_for("teams.create"))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def is_team_captain(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not(current_user.is_authenticated and current_user.tid and current_user.team.owner == current_user.uid):
|
||||
return abort(403)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def no_cache(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
response = make_response(func(*args, **kwargs))
|
||||
response.headers['Last-Modified'] = datetime.now()
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '-1'
|
||||
return response
|
||||
|
||||
return update_wrapper(wrapper, func)
|
1
server/easyctf/forms/__init__.py
Executable file
|
@ -0,0 +1 @@
|
|||
#
|
94
server/easyctf/forms/admin.py
Executable file
|
@ -0,0 +1,94 @@
|
|||
import imp
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import and_
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import (BooleanField, FloatField, HiddenField,
|
||||
IntegerField, StringField, SubmitField,
|
||||
TextAreaField)
|
||||
from wtforms.fields.html5 import DateTimeLocalField
|
||||
from wtforms.validators import InputRequired, NumberRange, Optional
|
||||
|
||||
from easyctf.models import Problem
|
||||
from easyctf.utils import VALID_PROBLEM_NAME, generate_string
|
||||
|
||||
|
||||
class DateTimeField(DateTimeLocalField):
|
||||
def _value(self):
|
||||
if not self.data:
|
||||
return ""
|
||||
return datetime.fromtimestamp(float(self.data)).strftime("%Y-%m-%dT%H:%M")
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
value = valuelist[0]
|
||||
obj = datetime.strptime(value, "%Y-%m-%dT%H:%M")
|
||||
self.data = time.mktime(obj.timetuple())
|
||||
|
||||
|
||||
class ProblemForm(FlaskForm):
|
||||
author = StringField("Problem Author", validators=[InputRequired("Please enter the author.")])
|
||||
title = StringField("Problem Title", validators=[InputRequired("Please enter a problem title.")])
|
||||
name = StringField("Problem Name (slug)", validators=[InputRequired("Please enter a problem name.")])
|
||||
category = StringField("Problem Category", validators=[InputRequired("Please enter a problem category.")])
|
||||
description = TextAreaField("Description", validators=[InputRequired("Please enter a description.")])
|
||||
value = IntegerField("Value", validators=[InputRequired("Please enter a value.")])
|
||||
programming = BooleanField(default=False, validators=[Optional()])
|
||||
|
||||
autogen = BooleanField("Autogen", validators=[Optional()])
|
||||
grader = TextAreaField("Grader", validators=[InputRequired("Please enter a grader.")])
|
||||
generator = TextAreaField("Generator", validators=[Optional()])
|
||||
source_verifier = TextAreaField("Source Verifier", validators=[Optional()])
|
||||
|
||||
test_cases = IntegerField("Test Cases", validators=[Optional()])
|
||||
time_limit = FloatField("Time Limit", validators=[Optional()])
|
||||
memory_limit = IntegerField("Memory Limit", validators=[Optional()])
|
||||
|
||||
submit = SubmitField("Submit")
|
||||
|
||||
def validate_name(self, field):
|
||||
if not VALID_PROBLEM_NAME.match(field.data):
|
||||
raise ValidationError("Problem name must be an all-lowercase, slug-style string.")
|
||||
# if Problem.query.filter(Problem.name == field.data).count():
|
||||
# raise ValidationError("That problem name already exists.")
|
||||
|
||||
def validate_grader(self, field):
|
||||
grader = imp.new_module("grader")
|
||||
if self.programming.data:
|
||||
# TODO validation
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
exec(field.data, grader.__dict__)
|
||||
assert hasattr(grader, "grade"), \
|
||||
"Grader is missing a 'grade' function."
|
||||
if self.autogen.data:
|
||||
assert hasattr(grader, "generate"), "Grader is missing a 'generate' function."
|
||||
seed1 = generate_string()
|
||||
import random
|
||||
random.seed(seed1)
|
||||
data = grader.generate(random)
|
||||
assert type(data) is dict, "'generate' must return dict"
|
||||
else:
|
||||
result = grader.grade(None, "")
|
||||
assert type(result) is tuple, "'grade' must return (correct, message)"
|
||||
correct, message = result
|
||||
assert type(correct) is bool, "'correct' must be a boolean."
|
||||
assert type(message) is str, "'message' must be a string."
|
||||
except Exception as e:
|
||||
raise ValidationError("%s: %s" % (e.__class__.__name__, str(e)))
|
||||
|
||||
|
||||
class SettingsForm(FlaskForm):
|
||||
team_size = IntegerField("Team Size", default=5, validators=[NumberRange(min=1), InputRequired("Please enter a max team size.")])
|
||||
ctf_name = StringField("CTF Name", default="OpenCTF", validators=[InputRequired("Please enter a CTF name.")])
|
||||
start_time = DateTimeField("Start Time", validators=[InputRequired("Please enter a CTF start time.")])
|
||||
end_time = DateTimeField("End Time", validators=[InputRequired("Please enter a CTF end time.")])
|
||||
judge_api_key = StringField("Judge API Key", validators=[Optional()])
|
||||
|
||||
submit = SubmitField("Save Settings")
|
||||
|
||||
def validate_start_time(self, field):
|
||||
import logging
|
||||
logging.error("lol {}".format(field.data))
|
27
server/easyctf/forms/chals.py
Executable file
|
@ -0,0 +1,27 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import HiddenField, StringField, TextAreaField
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
from easyctf.constants import SUPPORTED_LANGUAGES
|
||||
|
||||
|
||||
class ProblemSubmitForm(FlaskForm):
|
||||
pid = HiddenField("Problem ID")
|
||||
flag = StringField("Flag",
|
||||
validators=[InputRequired("Please enter a flag.")])
|
||||
|
||||
|
||||
class ProgrammingSubmitForm(FlaskForm):
|
||||
pid = HiddenField()
|
||||
code = TextAreaField("Code",
|
||||
validators=[InputRequired("Please enter code.")])
|
||||
language = HiddenField()
|
||||
|
||||
def validate_language(self, field):
|
||||
if field.data not in SUPPORTED_LANGUAGES:
|
||||
raise ValidationError("Invalid language.")
|
||||
|
||||
def validate_code(self, field):
|
||||
if len(field.data) > 65536:
|
||||
raise ValidationError("Code too large! (64KB max)")
|
21
server/easyctf/forms/classroom.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import func
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from easyctf.models import Team
|
||||
|
||||
|
||||
class NewClassroomForm(FlaskForm):
|
||||
name = StringField("Classroom Name", validators=[InputRequired()])
|
||||
submit = SubmitField("Create")
|
||||
|
||||
|
||||
class AddTeamForm(FlaskForm):
|
||||
name = StringField("Team Name", validators=[InputRequired()])
|
||||
submit = SubmitField("Add Team")
|
||||
|
||||
def validate_name(self, field):
|
||||
if not Team.query.filter(func.lower(Team.teamname) == field.data.lower()).count():
|
||||
raise ValidationError("Team does not exist!")
|
16
server/easyctf/forms/game.py
Executable file
|
@ -0,0 +1,16 @@
|
|||
import json
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import StringField
|
||||
from wtforms.validators import Length
|
||||
|
||||
|
||||
class GameStateUpdateForm(FlaskForm):
|
||||
state = StringField("state", validators=[Length(max=4096)])
|
||||
|
||||
def validate_state(self, field):
|
||||
try:
|
||||
json.loads(field.data)
|
||||
except:
|
||||
raise ValidationError('invalid json!')
|
83
server/easyctf/forms/teams.py
Executable file
|
@ -0,0 +1,83 @@
|
|||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import func, and_
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import BooleanField, FileField, StringField, SubmitField
|
||||
from wtforms.validators import InputRequired, Length
|
||||
|
||||
from easyctf.forms.validators import TeamLengthValidator
|
||||
from easyctf.models import Config, Team, User
|
||||
|
||||
|
||||
class AddMemberForm(FlaskForm):
|
||||
username = StringField("Username", validators=[InputRequired(
|
||||
"Please enter the username of the person you would like to add.")])
|
||||
submit = SubmitField("Add")
|
||||
|
||||
def get_user(self):
|
||||
query = User.query.filter(
|
||||
func.lower(User.username) == self.username.data.lower())
|
||||
return query.first()
|
||||
|
||||
def validate_username(self, field):
|
||||
if not current_user.team:
|
||||
raise ValidationError("You must belong to a team.")
|
||||
if current_user.team.owner != current_user.uid:
|
||||
raise ValidationError("Only the team captain can invite new members.")
|
||||
if len(current_user.team.outgoing_invitations) >= Config.get_team_size():
|
||||
raise ValidationError("You've already sent the maximum number of invitations.")
|
||||
user = User.query.filter(
|
||||
func.lower(User.username) == field.data.lower()).first()
|
||||
if user is None:
|
||||
raise ValidationError("This user doesn't exist.")
|
||||
if user.tid is not None:
|
||||
raise ValidationError("This user is already a part of a team.")
|
||||
if user in current_user.team.outgoing_invitations:
|
||||
raise ValidationError("You've already invited this member.")
|
||||
|
||||
|
||||
class CreateTeamForm(FlaskForm):
|
||||
teamname = StringField("Team Name", validators=[InputRequired("Please create a team name."), TeamLengthValidator])
|
||||
school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")])
|
||||
submit = SubmitField("Create Team")
|
||||
|
||||
def validate_teamname(self, field):
|
||||
if current_user.tid is not None:
|
||||
raise ValidationError("You are already in a team.")
|
||||
if Team.query.filter(func.lower(Team.teamname) == field.data.lower()).count():
|
||||
raise ValidationError("Team name is taken.")
|
||||
|
||||
|
||||
class DisbandTeamForm(FlaskForm):
|
||||
teamname = StringField("Confirm Team Name")
|
||||
submit = SubmitField("Delete Team")
|
||||
|
||||
def validate_teamname(self, field):
|
||||
if not current_user.team:
|
||||
raise ValidationError("You must belong to a team.")
|
||||
if current_user.team.owner != current_user.uid:
|
||||
raise ValidationError("Only the team captain can disband the team.")
|
||||
if field.data != current_user.team.teamname:
|
||||
raise ValidationError("Incorrect confirmation.")
|
||||
|
||||
|
||||
class ManageTeamForm(FlaskForm):
|
||||
teamname = StringField("Team Name", validators=[InputRequired("Please create a team name."), TeamLengthValidator])
|
||||
school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")])
|
||||
submit = SubmitField("Update")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tid = kwargs.get("tid", None)
|
||||
|
||||
def validate_teamname(self, field):
|
||||
if Team.query.filter(and_(func.lower(Team.teamname) == field.data.lower(), Team.tid != self.tid)).count():
|
||||
raise ValidationError("Team name is taken.")
|
||||
|
||||
|
||||
class ProfileEditForm(FlaskForm):
|
||||
teamname = StringField("Team Name", validators=[InputRequired("Please enter a team name."), TeamLengthValidator])
|
||||
school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")])
|
||||
avatar = FileField("Avatar")
|
||||
remove_avatar = BooleanField("Remove Avatar")
|
||||
submit = SubmitField("Update Profile")
|
114
server/easyctf/forms/users.py
Executable file
|
@ -0,0 +1,114 @@
|
|||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import func
|
||||
from wtforms import ValidationError
|
||||
from wtforms.fields import (BooleanField, FileField, IntegerField,
|
||||
PasswordField, RadioField, StringField,
|
||||
SubmitField)
|
||||
from wtforms.validators import Email, EqualTo, InputRequired, Length, Optional
|
||||
from wtforms.widgets.html5 import NumberInput
|
||||
|
||||
from easyctf.forms.validators import UsernameLengthValidator
|
||||
from easyctf.models import User
|
||||
from easyctf.utils import VALID_USERNAME
|
||||
|
||||
|
||||
class ChangeLoginForm(FlaskForm):
|
||||
email = StringField("Email", validators=[InputRequired("Please enter your email."), Email()])
|
||||
old_password = PasswordField("Current Password", validators=[InputRequired("Please enter your current password.")])
|
||||
password = PasswordField("Password", validators=[Optional()])
|
||||
confirm_password = PasswordField("Confirm Password", validators=[Optional(), EqualTo("password", "Please enter the same password.")])
|
||||
submit = SubmitField("Update Login Information")
|
||||
|
||||
def validate_old_password(self, field):
|
||||
if not current_user.check_password(field.data):
|
||||
raise ValidationError("Old password doesn't match.")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField("Username", validators=[InputRequired("Please enter your username."), UsernameLengthValidator])
|
||||
password = PasswordField("Password", validators=[InputRequired("Please enter your password.")])
|
||||
code = IntegerField("Two-Factor Token", validators=[Optional()])
|
||||
remember = BooleanField("Remember Me")
|
||||
submit = SubmitField("Login")
|
||||
|
||||
def get_user(self):
|
||||
query = User.query.filter(func.lower(User.username) == self.username.data.lower())
|
||||
return query.first()
|
||||
|
||||
def validate_username(self, field):
|
||||
if self.get_user() is None:
|
||||
raise ValidationError("This user doesn't exist.")
|
||||
|
||||
def validate_password(self, field):
|
||||
user = self.get_user()
|
||||
if not user:
|
||||
return
|
||||
if not user.check_password(field.data):
|
||||
raise ValidationError("Check your password again.")
|
||||
|
||||
|
||||
class ProfileEditForm(FlaskForm):
|
||||
name = StringField("Name", validators=[InputRequired("Please enter a name.")])
|
||||
avatar = FileField("Avatar")
|
||||
remove_avatar = BooleanField("Remove Avatar")
|
||||
submit = SubmitField("Update Profile")
|
||||
|
||||
|
||||
class PasswordForgotForm(FlaskForm):
|
||||
email = StringField("Email", validators=[InputRequired("Please enter your email."), Email("Please enter a valid email.")])
|
||||
submit = SubmitField("Send Recovery Email")
|
||||
|
||||
def __init__(self):
|
||||
super(PasswordForgotForm, self).__init__()
|
||||
self._user = None
|
||||
self._user_cached = False
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
if not self._user_cached:
|
||||
self._user = User.query.filter(
|
||||
func.lower(User.email) == self.email.data.lower()).first()
|
||||
self._user_cached = True
|
||||
return self._user
|
||||
|
||||
|
||||
class PasswordResetForm(FlaskForm):
|
||||
password = PasswordField("Password", validators=[InputRequired("Please enter a password.")])
|
||||
confirm_password = PasswordField("Confirm Password", validators=[InputRequired("Please confirm your password."), EqualTo("password", "Please enter the same password.")])
|
||||
submit = SubmitField("Change Password")
|
||||
|
||||
|
||||
class RegisterForm(FlaskForm):
|
||||
name = StringField("Name", validators=[InputRequired("Please enter a name.")])
|
||||
username = StringField("Username", validators=[InputRequired("Please enter a username."), UsernameLengthValidator])
|
||||
email = StringField("Email", validators=[InputRequired("Please enter an email."), Email("Please enter a valid email.")])
|
||||
password = PasswordField("Password", validators=[InputRequired("Please enter a password.")])
|
||||
confirm_password = PasswordField("Confirm Password", validators=[InputRequired("Please confirm your password."), EqualTo("password", "Please enter the same password.")])
|
||||
level = RadioField("Who are you?", choices=[("1", "Student"), ("2", "Observer"), ("3", "Teacher")])
|
||||
submit = SubmitField("Register")
|
||||
|
||||
def validate_username(self, field):
|
||||
if not VALID_USERNAME.match(field.data):
|
||||
raise ValidationError("Username must be contain letters, numbers, or _, and not start with a number.")
|
||||
if User.query.filter(func.lower(User.username) == field.data.lower()).count():
|
||||
raise ValidationError("Username is taken.")
|
||||
|
||||
def validate_email(self, field):
|
||||
if User.query.filter(func.lower(User.email) == field.data.lower()).count():
|
||||
raise ValidationError("Email is taken.")
|
||||
|
||||
|
||||
class TwoFactorAuthSetupForm(FlaskForm):
|
||||
code = IntegerField("Code", validators=[InputRequired("Please enter the code.")], widget=NumberInput())
|
||||
password = PasswordField("Password", validators=[
|
||||
InputRequired("Please enter your password.")])
|
||||
submit = SubmitField("Confirm")
|
||||
|
||||
def validate_code(self, field):
|
||||
if not current_user.verify_totp(field.data):
|
||||
raise ValidationError("Incorrect code.")
|
||||
|
||||
def validate_password(self, field):
|
||||
if not current_user.check_password(field.data):
|
||||
raise ValidationError("Incorrect password.")
|
4
server/easyctf/forms/validators.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from wtforms.validators import Length
|
||||
|
||||
UsernameLengthValidator = Length(3, 16, message="Usernames must be between 3 to 16 characters long.")
|
||||
TeamLengthValidator = Length(3, 32, message="Usernames must be between 3 to 32 characters long.")
|
895
server/easyctf/models.py
Executable file
|
@ -0,0 +1,895 @@
|
|||
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_login import current_user
|
||||
from markdown2 import markdown
|
||||
from passlib.hash import bcrypt
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import backref
|
||||
from sqlalchemy.sql.expression import union_all
|
||||
|
||||
from easyctf.config import Config as AppConfig
|
||||
from easyctf.constants import USER_REGULAR
|
||||
from easyctf.objects import cache, db, login_manager
|
||||
from easyctf.utils import (generate_identicon, generate_short_string,
|
||||
generate_string, save_file)
|
||||
|
||||
config = AppConfig()
|
||||
SEED = "OPENCTF_PROBLEM_SEED_PREFIX_%s" % config.SECRET_KEY
|
||||
login_manager.login_view = "users.login"
|
||||
login_manager.login_message_category = "danger"
|
||||
|
||||
new_user_pattern = re.compile(r"::(user[\d]{5}):([a-zA-Z0-9]+)")
|
||||
|
||||
|
||||
def filename_filter(name):
|
||||
return re.sub("[^a-zA-Z0-9]+", "_", name)
|
||||
|
||||
|
||||
team_classroom = db.Table("team_classroom",
|
||||
db.Column("team_id", db.Integer, db.ForeignKey(
|
||||
"teams.tid"), nullable=False),
|
||||
db.Column("classroom_id", db.Integer, db.ForeignKey(
|
||||
"classrooms.id"), nullable=False),
|
||||
db.PrimaryKeyConstraint("team_id", "classroom_id"))
|
||||
classroom_invitation = db.Table("classroom_invitation",
|
||||
db.Column("team_id", db.Integer, db.ForeignKey(
|
||||
"teams.tid"), nullable=False),
|
||||
db.Column("classroom_id", db.Integer, db.ForeignKey(
|
||||
"classrooms.id"), nullable=False),
|
||||
db.PrimaryKeyConstraint("team_id", "classroom_id"))
|
||||
|
||||
team_player_invitation = db.Table("team_player_invitation",
|
||||
db.Column("team_id", db.Integer, db.ForeignKey(
|
||||
"teams.tid", primary_key=True)),
|
||||
db.Column("user_id", db.Integer, db.ForeignKey("users.uid", primary_key=True)))
|
||||
|
||||
player_team_invitation = db.Table("player_team_invitation",
|
||||
db.Column("user_id", db.Integer, db.ForeignKey(
|
||||
"users.uid", primary_key=True)),
|
||||
db.Column("team_id", db.Integer, db.ForeignKey("teams.tid", primary_key=True)))
|
||||
|
||||
|
||||
class Config(db.Model):
|
||||
__tablename__ = "config"
|
||||
cid = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.Unicode(32), index=True)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def get_competition_window(cls):
|
||||
return (0, 0)
|
||||
|
||||
@classmethod
|
||||
def get_team_size(cls):
|
||||
# TODO: actually implement this
|
||||
return 5
|
||||
|
||||
@classmethod
|
||||
def get(cls, key, default=None):
|
||||
config = cls.query.filter_by(key=key).first()
|
||||
if config is None:
|
||||
return default
|
||||
return str(config.value)
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value):
|
||||
config = cls.query.filter_by(key=key).first()
|
||||
if config is None:
|
||||
config = Config(key, value)
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_many(cls, keys):
|
||||
items = cls.query.filter(cls.key.in_(keys)).all()
|
||||
return dict([(item.key, item.value) for item in items])
|
||||
|
||||
@classmethod
|
||||
def set_many(cls, configs):
|
||||
for key, value in list(configs.items()):
|
||||
config = cls.query.filter_by(key=key).first()
|
||||
if config is None:
|
||||
config = Config(key, value)
|
||||
config.value = value
|
||||
db.session.add(config)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_ssh_keys(cls):
|
||||
private_key = cls.get("private_key")
|
||||
public_key = cls.get("public_key")
|
||||
if not (private_key and public_key):
|
||||
key = RSA.generate(2048)
|
||||
private_key = key.exportKey("PEM")
|
||||
public_key = key.publickey().exportKey("OpenSSH")
|
||||
cls.set_many({
|
||||
"private_key": str(private_key, "utf-8"),
|
||||
"public_key": str(public_key, "utf-8")
|
||||
})
|
||||
return private_key, public_key
|
||||
|
||||
def __repr__(self):
|
||||
return "Config({}={})".format(self.key, self.value)
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
uid = db.Column(db.Integer, index=True, primary_key=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"))
|
||||
name = db.Column(db.Unicode(32))
|
||||
easyctf = db.Column(db.Boolean, index=True, default=False)
|
||||
username = db.Column(db.String(16), unique=True, index=True)
|
||||
email = db.Column(db.String(128), unique=True)
|
||||
_password = db.Column("password", db.String(128))
|
||||
admin = db.Column(db.Boolean, default=False)
|
||||
level = db.Column(db.Integer)
|
||||
_register_time = db.Column("register_time", db.DateTime, default=datetime.utcnow)
|
||||
reset_token = db.Column(db.String(32))
|
||||
otp_secret = db.Column(db.String(16))
|
||||
otp_confirmed = db.Column(db.Boolean, default=False)
|
||||
email_token = db.Column(db.String(32))
|
||||
email_verified = db.Column(db.Boolean, default=False)
|
||||
|
||||
team = db.relationship("Team", back_populates="members")
|
||||
solves = db.relationship("Solve", backref="user", lazy=True)
|
||||
jobs = db.relationship("Job", backref="user", lazy=True)
|
||||
_avatar = db.Column("avatar", db.String(128))
|
||||
|
||||
outgoing_invitations = db.relationship("Team", secondary=player_team_invitation, lazy="subquery", backref=db.backref("incoming_invitations", lazy=True))
|
||||
|
||||
@property
|
||||
def avatar(self):
|
||||
if not self._avatar:
|
||||
avatar_file = BytesIO()
|
||||
avatar = generate_identicon("user%s" % self.uid)
|
||||
avatar.save(avatar_file, format="PNG")
|
||||
avatar_file.seek(0)
|
||||
response = save_file(
|
||||
avatar_file, prefix="team_avatar_", suffix=".png")
|
||||
if response.status_code == 200:
|
||||
self._avatar = response.text
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return self._avatar or "" # just so the frontend doesnt 500
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, User):
|
||||
return self.uid == other.uid
|
||||
return NotImplemented
|
||||
|
||||
def __str__(self):
|
||||
return "<User %s>" % self.uid
|
||||
|
||||
def check_password(self, password):
|
||||
return bcrypt.verify(password, self.password)
|
||||
|
||||
def get_id(self):
|
||||
return str(self.uid)
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@login_manager.user_loader
|
||||
def get_by_id(id):
|
||||
query_results = User.query.filter_by(uid=id)
|
||||
return query_results.first()
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
# TODO This will be based off account standing.
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
@hybrid_property
|
||||
def password(self):
|
||||
return self._password
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
self._password = bcrypt.encrypt(password, rounds=10)
|
||||
|
||||
@hybrid_property
|
||||
def register_time(self):
|
||||
return int(time.mktime(self._register_time.timetuple()))
|
||||
|
||||
@hybrid_property
|
||||
def username_lower(self):
|
||||
return self.username.lower()
|
||||
|
||||
def get_totp_uri(self):
|
||||
if self.otp_secret is None:
|
||||
secret = base64.b32encode(os.urandom(10)).decode("utf-8").lower()
|
||||
self.otp_secret = secret
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
service_name = Config.get("ctf_name")
|
||||
return "otpauth://totp/%s:%s?secret=%s&issuer=%s" % (service_name, self.username, self.otp_secret, service_name)
|
||||
|
||||
def verify_totp(self, token):
|
||||
return onetimepass.valid_totp(token, self.otp_secret)
|
||||
|
||||
@cache.memoize(timeout=120)
|
||||
def points(self):
|
||||
points = 0
|
||||
for solve in self.solves:
|
||||
points += solve.problem.value
|
||||
return points
|
||||
|
||||
|
||||
class Problem(db.Model):
|
||||
__tablename__ = "problems"
|
||||
pid = db.Column(db.Integer, index=True, primary_key=True)
|
||||
author = db.Column(db.Unicode(32))
|
||||
name = db.Column(db.String(32), unique=True)
|
||||
title = db.Column(db.Unicode(64))
|
||||
description = db.Column(db.Text)
|
||||
hint = db.Column(db.Text)
|
||||
category = db.Column(db.Unicode(64))
|
||||
value = db.Column(db.Integer)
|
||||
|
||||
grader = db.Column(db.UnicodeText)
|
||||
autogen = db.Column(db.Boolean)
|
||||
programming = db.Column(db.Boolean)
|
||||
threshold = db.Column(db.Integer)
|
||||
weightmap = db.Column(db.PickleType)
|
||||
solves = db.relationship("Solve", backref="problem", lazy=True)
|
||||
jobs = db.relationship("Job", backref="problem", lazy=True)
|
||||
|
||||
test_cases = db.Column(db.Integer)
|
||||
time_limit = db.Column(db.Integer) # in seconds
|
||||
memory_limit = db.Column(db.Integer) # in kb
|
||||
generator = db.Column(db.Text)
|
||||
# will use the same grader as regular problems
|
||||
source_verifier = db.Column(db.Text) # may be implemented (possibly)
|
||||
path = db.Column(db.String(128)) # path to problem source code
|
||||
|
||||
files = db.relationship("File", backref="problem", lazy=True)
|
||||
autogen_files = db.relationship(
|
||||
"AutogenFile", backref="problem", lazy=True)
|
||||
|
||||
@staticmethod
|
||||
def validate_problem(path, name):
|
||||
files = os.listdir(path)
|
||||
valid = True
|
||||
for required_file in ["grader.py", "problem.yml", "description.md"]:
|
||||
if required_file not in files:
|
||||
print("\t* Missing {}".format(required_file))
|
||||
valid = False
|
||||
|
||||
if not valid:
|
||||
return valid
|
||||
|
||||
metadata = yaml.load(open(os.path.join(path, "problem.yml")))
|
||||
if metadata.get("programming", False):
|
||||
if "generator.py" not in files:
|
||||
print("\t* Missing generator.py")
|
||||
valid = False
|
||||
|
||||
for required_key in ["test_cases", "time_limit", "memory_limit"]:
|
||||
if required_key not in metadata:
|
||||
print("\t* Expected required key {} in 'problem.yml'".format(required_key))
|
||||
valid = False
|
||||
|
||||
return valid
|
||||
|
||||
@staticmethod
|
||||
def import_problem(path, name):
|
||||
print(" - {}".format(name))
|
||||
if not Problem.validate_problem(path, name):
|
||||
return
|
||||
|
||||
problem = Problem.query.filter_by(name=name).first()
|
||||
if not problem:
|
||||
problem = Problem(name=name)
|
||||
|
||||
metadata = yaml.load(open(os.path.join(path, "problem.yml")))
|
||||
problem.author = metadata.get("author", "")
|
||||
problem.title = metadata.get("title", "")
|
||||
problem.category = metadata.get("category", "")
|
||||
problem.value = int(metadata.get("value", "0"))
|
||||
problem.hint = metadata.get("hint", "")
|
||||
problem.autogen = metadata.get("autogen", False)
|
||||
problem.programming = metadata.get("programming", False)
|
||||
|
||||
problem.description = open(os.path.join(path, "description.md")).read()
|
||||
problem.grader = open(os.path.join(path, "grader.py")).read()
|
||||
problem.path = os.path.realpath(path)
|
||||
|
||||
if metadata.get("threshold") and type(metadata.get("threshold")) is int:
|
||||
problem.threshold = metadata.get("threshold")
|
||||
problem.weightmap = metadata.get("weightmap", {})
|
||||
|
||||
if problem.programming:
|
||||
problem.test_cases = int(metadata.get("test_cases"))
|
||||
problem.time_limit = int(metadata.get("time_limit"))
|
||||
problem.memory_limit = int(metadata.get("memory_limit"))
|
||||
problem.generator = open(os.path.join(path, "generator.py")).read()
|
||||
|
||||
db.session.add(problem)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
|
||||
files = metadata.get("files", [])
|
||||
for filename in files:
|
||||
file_path = os.path.join(path, filename)
|
||||
if not os.path.isfile(file_path):
|
||||
print("\t* File '{}' doesn't exist".format(filename, name))
|
||||
continue
|
||||
|
||||
source = open(file_path, "rb")
|
||||
file = File(pid=problem.pid, filename=filename, data=source)
|
||||
|
||||
existing = File.query.filter_by(pid=problem.pid, filename=filename).first()
|
||||
# Update existing file url
|
||||
if existing:
|
||||
existing.url = file.url
|
||||
db.session.add(existing)
|
||||
else:
|
||||
db.session.add(file)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def import_repository(path):
|
||||
if not (os.path.realpath(path) and os.path.exists(path) and os.path.isdir(path)):
|
||||
print("this isn't a path")
|
||||
sys.exit(1)
|
||||
path = os.path.realpath(path)
|
||||
names = os.listdir(path)
|
||||
for name in names:
|
||||
if name.startswith("."):
|
||||
continue
|
||||
problem_dir = os.path.join(path, name)
|
||||
if not os.path.isdir(problem_dir):
|
||||
continue
|
||||
problem_name = os.path.basename(problem_dir)
|
||||
Problem.import_problem(problem_dir, problem_name)
|
||||
|
||||
@classmethod
|
||||
def categories(cls):
|
||||
def f(c): return c[0]
|
||||
categories = map(f, db.session.query(Problem.category).distinct().all())
|
||||
return list(categories)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(id):
|
||||
query_results = Problem.query.filter_by(pid=id)
|
||||
return query_results.first() if query_results.count() else None
|
||||
|
||||
@property
|
||||
def solved(self):
|
||||
return Solve.query.filter_by(pid=self.pid, tid=current_user.tid).count()
|
||||
|
||||
def get_grader(self):
|
||||
grader = imp.new_module("grader")
|
||||
curr = os.getcwd()
|
||||
if self.path:
|
||||
os.chdir(self.path)
|
||||
exec(self.grader, grader.__dict__)
|
||||
os.chdir(curr)
|
||||
return grader
|
||||
|
||||
def get_autogen(self, tid):
|
||||
autogen = __import__("random")
|
||||
autogen.seed("%s_%s_%s" % (SEED, self.pid, tid))
|
||||
return autogen
|
||||
|
||||
def render_description(self, tid):
|
||||
description = markdown(self.description, extras=["fenced-code-blocks"])
|
||||
try:
|
||||
variables = {}
|
||||
template = Template(description)
|
||||
if self.autogen:
|
||||
autogen = self.get_autogen(tid)
|
||||
grader = self.get_grader()
|
||||
generated_problem = grader.generate(autogen)
|
||||
if "variables" in generated_problem:
|
||||
variables.update(generated_problem["variables"])
|
||||
if "files" in generated_problem:
|
||||
for file in generated_problem["files"]:
|
||||
url = url_for("chals.autogen", pid=self.pid, filename=file)
|
||||
variables[File.clean_name(file)] = url
|
||||
static_files = File.query.filter_by(pid=self.pid).all()
|
||||
if static_files is not None:
|
||||
for file in static_files:
|
||||
url = "{}/{}".format(app.config["FILESTORE_STATIC"], file.url)
|
||||
variables[File.clean_name(file.filename)] = url
|
||||
description = template.safe_substitute(variables)
|
||||
except Exception as e:
|
||||
description += "<!-- parsing error: {} -->".format(traceback.format_exc())
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
description = description.replace("${", "{")
|
||||
return description
|
||||
|
||||
# TODO: clean up the shitty string-enum return
|
||||
# the shitty return is used directly in game.py
|
||||
def try_submit(self, flag):
|
||||
solved = Solve.query.filter_by(tid=current_user.tid, pid=self.pid).first()
|
||||
if solved:
|
||||
return "error", "You've already solved this problem"
|
||||
already_tried = WrongFlag.query.filter_by(tid=current_user.tid, pid=self.pid, flag=flag).count()
|
||||
if already_tried:
|
||||
return "error", "You've already tried this flag"
|
||||
random = None
|
||||
if self.autogen:
|
||||
random = self.get_autogen(current_user.tid)
|
||||
|
||||
grader = self.get_grader()
|
||||
correct, message = grader.grade(random, flag)
|
||||
if correct:
|
||||
submission = Solve(pid=self.pid, tid=current_user.tid, uid=current_user.uid, flag=flag)
|
||||
db.session.add(submission)
|
||||
db.session.commit()
|
||||
else:
|
||||
if len(flag) < 256:
|
||||
submission = WrongFlag(pid=self.pid, tid=current_user.tid, uid=current_user.uid,
|
||||
flag=flag)
|
||||
db.session.add(submission)
|
||||
db.session.commit()
|
||||
else:
|
||||
# fuck you
|
||||
pass
|
||||
|
||||
cache.delete_memoized(current_user.team.place)
|
||||
cache.delete_memoized(current_user.team.points)
|
||||
cache.delete_memoized(current_user.team.get_last_solved)
|
||||
cache.delete_memoized(current_user.team.get_score_progression)
|
||||
|
||||
return "success" if correct else "failure", message
|
||||
|
||||
def api_summary(self):
|
||||
summary = {field: getattr(self, field) for field in ['pid', 'author', 'name', 'title', 'hint',
|
||||
'category', 'value', 'solved', 'programming']}
|
||||
summary['description'] = self.render_description(current_user.tid)
|
||||
return summary
|
||||
|
||||
|
||||
class File(db.Model):
|
||||
__tablename__ = "files"
|
||||
id = db.Column(db.Integer, index=True, primary_key=True)
|
||||
pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True)
|
||||
filename = db.Column(db.Unicode(64))
|
||||
url = db.Column(db.String(128))
|
||||
|
||||
@staticmethod
|
||||
def clean_name(name):
|
||||
return filename_filter(name)
|
||||
|
||||
def __init__(self, pid, filename, data):
|
||||
self.pid = pid
|
||||
self.filename = filename
|
||||
data.seek(0)
|
||||
if not app.config.get("TESTING"):
|
||||
response = save_file(data, suffix="_" + filename)
|
||||
if response.status_code == 200:
|
||||
self.url = response.text
|
||||
|
||||
|
||||
class AutogenFile(db.Model):
|
||||
__tablename__ = "autogen_files"
|
||||
id = db.Column(db.Integer, index=True, primary_key=True)
|
||||
pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True)
|
||||
tid = db.Column(db.Integer, index=True)
|
||||
filename = db.Column(db.Unicode(64), index=True)
|
||||
url = db.Column(db.String(128))
|
||||
|
||||
@staticmethod
|
||||
def clean_name(name):
|
||||
return filename_filter(name)
|
||||
|
||||
def __init__(self, pid, tid, filename, data):
|
||||
self.pid = pid
|
||||
self.tid = tid
|
||||
self.filename = filename
|
||||
data.seek(0)
|
||||
if not app.config.get("TESTING"):
|
||||
response = save_file(data, suffix="_" + filename)
|
||||
if response.status_code == 200:
|
||||
self.url = response.text
|
||||
|
||||
|
||||
class PasswordResetToken(db.Model):
|
||||
__tablename__ = "password_reset_tokens"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
active = db.Column(db.Boolean)
|
||||
token = db.Column(db.String(32), default=generate_short_string)
|
||||
email = db.Column(db.Unicode(128))
|
||||
expire = db.Column(db.DateTime)
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return datetime.utcnow() >= self.expire
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return User.get_by_id(self.uid)
|
||||
|
||||
|
||||
class Solve(db.Model):
|
||||
__tablename__ = "solves"
|
||||
__table_args__ = (db.UniqueConstraint('pid', 'tid'),)
|
||||
id = db.Column(db.Integer, index=True, primary_key=True)
|
||||
pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
_date = db.Column("date", db.DateTime, default=datetime.utcnow)
|
||||
flag = db.Column(db.Unicode(256))
|
||||
|
||||
@hybrid_property
|
||||
def date(self):
|
||||
return int(time.mktime(self._date.timetuple()))
|
||||
|
||||
@date.expression
|
||||
def date_expression(self):
|
||||
return self._date
|
||||
|
||||
|
||||
class WrongFlag(db.Model):
|
||||
__tablename__ = "wrong_flags"
|
||||
id = db.Column(db.Integer, index=True, primary_key=True)
|
||||
pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
_date = db.Column("date", db.DateTime, default=datetime.utcnow)
|
||||
flag = db.Column(db.Unicode(256), index=True)
|
||||
|
||||
@hybrid_property
|
||||
def date(self):
|
||||
return int(time.mktime(self._date.timetuple()))
|
||||
|
||||
@date.expression
|
||||
def date_expression(self):
|
||||
return self._date
|
||||
|
||||
|
||||
class Team(db.Model):
|
||||
__tablename__ = "teams"
|
||||
tid = db.Column(db.Integer, primary_key=True, index=True)
|
||||
teamname = db.Column(db.Unicode(32), unique=True)
|
||||
school = db.Column(db.Unicode(64))
|
||||
owner = db.Column(db.Integer)
|
||||
classrooms = db.relationship("Classroom", secondary=team_classroom, backref="classrooms")
|
||||
classroom_invites = db.relationship("Classroom", secondary=classroom_invitation, backref="classroom_invites")
|
||||
members = db.relationship("User", back_populates="team")
|
||||
admin = db.Column(db.Boolean, default=False)
|
||||
shell_user = db.Column(db.String(16), unique=True)
|
||||
shell_pass = db.Column(db.String(32))
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
solves = db.relationship("Solve", backref="team", lazy=True)
|
||||
jobs = db.relationship("Job", backref="team", lazy=True)
|
||||
_avatar = db.Column("avatar", db.String(128))
|
||||
|
||||
outgoing_invitations = db.relationship("User", secondary=team_player_invitation, lazy="subquery", backref=db.backref("incoming_invitations", lazy=True))
|
||||
|
||||
def __repr__(self):
|
||||
return "%s_%s" % (self.__class__.__name__, self.tid)
|
||||
|
||||
def __str__(self):
|
||||
return "<Team %s>" % self.tid
|
||||
|
||||
@property
|
||||
def avatar(self):
|
||||
if not self._avatar:
|
||||
avatar_file = BytesIO()
|
||||
avatar = generate_identicon("team%s" % self.tid)
|
||||
avatar.save(avatar_file, format="PNG")
|
||||
avatar_file.seek(0)
|
||||
response = save_file(
|
||||
avatar_file, prefix="user_avatar_", suffix=".png")
|
||||
if response.status_code == 200:
|
||||
self._avatar = response.text
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
return self._avatar
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(id):
|
||||
query_results = Team.query.filter_by(tid=id)
|
||||
return query_results.first()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
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()
|
||||
|
||||
@hybrid_property
|
||||
def prop_points(self):
|
||||
return sum(problem.value
|
||||
for problem, solve in
|
||||
db.session.query(Problem, Solve).filter(Solve.tid == self.tid).filter(Problem.pid == Solve.tid).all())
|
||||
|
||||
@prop_points.expression
|
||||
def prop_points(self):
|
||||
return db.session.query(Problem, Solve).filter(Solve.tid == self.tid).filter(Problem.pid == Solve.tid)\
|
||||
.with_entities(func.sum(Problem.value)).scalar()
|
||||
|
||||
@cache.memoize(timeout=120)
|
||||
def points(self):
|
||||
points = 0
|
||||
solves = self.solves
|
||||
solves.sort(key=lambda s: s.date, reverse=True)
|
||||
for solve in solves:
|
||||
problem = Problem.query.filter_by(pid=solve.pid).first()
|
||||
points += problem.value
|
||||
return points
|
||||
|
||||
@cache.memoize(timeout=120)
|
||||
def place(self):
|
||||
scoreboard = Team.scoreboard()
|
||||
if not self.observer:
|
||||
scoreboard = [team for team in scoreboard if not team.observer]
|
||||
i = 0
|
||||
for i in range(len(scoreboard)):
|
||||
if scoreboard[i].tid == self.tid:
|
||||
break
|
||||
i += 1
|
||||
return i
|
||||
|
||||
@hybrid_property
|
||||
def prop_last_solved(self):
|
||||
solve = Solve.query.filter_by(
|
||||
tid=self.tid).order_by(Solve.date).first()
|
||||
if not solve:
|
||||
return 0
|
||||
return solve.date
|
||||
|
||||
@cache.memoize(timeout=120)
|
||||
def get_last_solved(self):
|
||||
solves = self.solves
|
||||
solves.sort(key=lambda s: s.date, reverse=True)
|
||||
if solves:
|
||||
solve = solves[0]
|
||||
return solve.date if solve else 0
|
||||
return 0
|
||||
|
||||
def has_unlocked(self, problem):
|
||||
solves = self.solves
|
||||
if not problem.weightmap:
|
||||
return True
|
||||
current = sum(
|
||||
[problem.weightmap.get(solve.problem.name, 0) for solve in solves])
|
||||
return current >= problem.threshold
|
||||
|
||||
def get_unlocked_problems(self, admin=False, programming=None):
|
||||
if admin:
|
||||
return Problem.query.order_by(Problem.value).all()
|
||||
match = Problem.value > 0
|
||||
if programming is not None:
|
||||
match = and_(match, Problem.programming == programming)
|
||||
problems = Problem.query.filter(match).order_by(Problem.value).all()
|
||||
solves = self.solves
|
||||
|
||||
def unlocked(problem):
|
||||
if not problem.weightmap:
|
||||
return True
|
||||
current = sum([problem.weightmap.get(solve.problem.name, 0) for solve in solves])
|
||||
return current >= problem.threshold
|
||||
return list(filter(unlocked, problems))
|
||||
|
||||
def get_jobs(self):
|
||||
return Job.query.filter_by(tid=self.tid).order_by(
|
||||
Job.completion_time.desc()).all()
|
||||
|
||||
def has_solved(self, pid):
|
||||
return Solve.query.filter_by(tid=self.tid, pid=pid).count() > 0
|
||||
|
||||
@classmethod
|
||||
@cache.memoize(timeout=60)
|
||||
def scoreboard(cls):
|
||||
# credit: https://github.com/CTFd/CTFd/blob/master/CTFd/scoreboard.py
|
||||
uniq = db.session\
|
||||
.query(Solve.tid.label("tid"), Solve.pid.label("pid"))\
|
||||
.distinct()\
|
||||
.subquery()
|
||||
# flash("uniq: " + str(uniq).replace("\n", ""), "info")
|
||||
scores = db.session\
|
||||
.query(
|
||||
# uniq.columns.tid.label("tid"),
|
||||
Solve.tid.label("tid"),
|
||||
db.func.max(Solve.pid).label("pid"),
|
||||
db.func.sum(Problem.value).label("score"),
|
||||
db.func.max(Solve.date).label("date"))\
|
||||
.join(Problem)\
|
||||
.group_by(Solve.tid)
|
||||
# flash("scores: " + str(scores).replace("\n", ""), "info")
|
||||
results = union_all(scores).alias("results")
|
||||
sumscores = db.session\
|
||||
.query(results.columns.tid, db.func.sum(results.columns.score).label("score"), db.func.max(results.columns.pid), db.func.max(results.columns.date).label("date"))\
|
||||
.group_by(results.columns.tid)\
|
||||
.subquery()
|
||||
query = db.session\
|
||||
.query(Team, Team.tid.label("tid"), sumscores.columns.score, sumscores.columns.date)\
|
||||
.filter(Team.banned == False)\
|
||||
.join(sumscores, Team.tid == sumscores.columns.tid)\
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.date)
|
||||
# flash("full query: " + str(query).replace("\n", ""), "info")
|
||||
return query.all()
|
||||
|
||||
@cache.memoize(timeout=120)
|
||||
def get_score_progression(self):
|
||||
def convert_to_time(time):
|
||||
m, s = divmod(time, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
if d > 0:
|
||||
return "%d:%02d:%02d:%02d" % (d, h, m, s)
|
||||
return "%d:%02d:%02d" % (h, m, s)
|
||||
|
||||
solves = self.solves
|
||||
solves.sort(key=lambda s: s.date)
|
||||
progression = [["Time", "Score"], [convert_to_time(0), 0]]
|
||||
score = 0
|
||||
start_time = int(Config.get("start_time", default=0))
|
||||
for solve in solves:
|
||||
score += solve.problem.value
|
||||
frame = [convert_to_time(solve.date - start_time), score]
|
||||
progression.append(frame)
|
||||
|
||||
progression.append([convert_to_time(time.time() - start_time), score])
|
||||
|
||||
return progression
|
||||
|
||||
def credentials(self):
|
||||
host = app.config.get("SHELL_HOST")
|
||||
if not host:
|
||||
return None
|
||||
print("host:", host)
|
||||
private_key_contents, _ = Config.get_ssh_keys()
|
||||
private_key = paramiko.rsakey.RSAKey(file_obj=StringIO(private_key_contents))
|
||||
if not private_key:
|
||||
return None
|
||||
print("private key:", private_key)
|
||||
if not self.shell_user or not self.shell_pass:
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(host, username="ctfadmin", pkey=private_key, look_for_keys=False)
|
||||
stdin, stdout, stderr = client.exec_command("\n")
|
||||
data = stdout.read().decode("utf-8").split("\n")
|
||||
for line in data:
|
||||
match = new_user_pattern.match(line)
|
||||
if match:
|
||||
username, password = match.group(1), match.group(2)
|
||||
break
|
||||
else:
|
||||
return None
|
||||
self.shell_user = username
|
||||
self.shell_pass = password
|
||||
db.session.commit()
|
||||
return (username, password)
|
||||
return (self.shell_user, self.shell_pass)
|
||||
|
||||
|
||||
class Classroom(db.Model):
|
||||
__tablename__ = "classrooms"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.Unicode(64), nullable=False)
|
||||
owner = db.Column(db.Integer)
|
||||
teams = db.relationship("Team", passive_deletes=True, secondary=team_classroom, backref="teams")
|
||||
invites = db.relationship("Team", passive_deletes=True, secondary=classroom_invitation, backref="invites")
|
||||
|
||||
def __contains__(self, obj):
|
||||
if isinstance(obj, Team):
|
||||
return obj in self.teams
|
||||
return False
|
||||
|
||||
@property
|
||||
def teacher(self):
|
||||
return User.query.filter_by(uid=self.owner).first()
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return len(self.teams)
|
||||
|
||||
@property
|
||||
def scoreboard(self):
|
||||
return sorted(self.teams, key=lambda team: (team.points(), -team.get_last_solved()), reverse=True)
|
||||
|
||||
|
||||
class Egg(db.Model):
|
||||
__tablename__ = "eggs"
|
||||
eid = db.Column(db.Integer, primary_key=True)
|
||||
flag = db.Column(db.Unicode(64), nullable=False, unique=True, index=True)
|
||||
solves = db.relationship("EggSolve", backref="egg", lazy=True)
|
||||
|
||||
|
||||
class EggSolve(db.Model):
|
||||
__tablename__ = "egg_solves"
|
||||
sid = db.Column(db.Integer, primary_key=True)
|
||||
eid = db.Column(db.Integer, db.ForeignKey("eggs.eid"), index=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class WrongEgg(db.Model):
|
||||
__tablename__ = "wrong_egg"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
eid = db.Column(db.Integer, db.ForeignKey("eggs.eid"), index=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
submission = db.Column(db.Unicode(64))
|
||||
|
||||
# judge stuff
|
||||
|
||||
|
||||
class JudgeKey(db.Model):
|
||||
__tablename__ = "judge_api_keys"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.String(64), index=True, default=generate_string)
|
||||
ip = db.Column(db.Integer) # maybe?
|
||||
|
||||
|
||||
class Job(db.Model):
|
||||
__tablename__ = "jobs"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True)
|
||||
tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), index=True)
|
||||
|
||||
submitted = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
claimed = db.Column(db.DateTime)
|
||||
completed = db.Column(db.DateTime)
|
||||
|
||||
execution_time = db.Column(db.Float)
|
||||
execution_memory = db.Column(db.Float)
|
||||
|
||||
language = db.Column(db.String(16), nullable=False)
|
||||
contents = db.Column(db.Text, nullable=False)
|
||||
feedback = db.Column(db.Text)
|
||||
|
||||
# 0 = waiting
|
||||
# 1 = progress
|
||||
# 2 = done
|
||||
# 3 = errored
|
||||
status = db.Column(db.Integer, index=True, nullable=False, default=0)
|
||||
|
||||
verdict = db.Column(db.String(8))
|
||||
last_ran_case = db.Column(db.Integer)
|
||||
|
||||
|
||||
class GameState(db.Model):
|
||||
__tablename__ = "game_states"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.Integer, db.ForeignKey("users.uid"), unique=True)
|
||||
last_updated = db.Column(db.DateTime, server_default=func.now(), onupdate=func.current_timestamp(), unique=True)
|
||||
|
||||
state = db.Column(db.UnicodeText, nullable=False, default="{}")
|
12
server/easyctf/objects.py
Executable file
|
@ -0,0 +1,12 @@
|
|||
from random import SystemRandom
|
||||
|
||||
from flask_caching import Cache
|
||||
from flask_login import LoginManager
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from raven.contrib.flask import Sentry
|
||||
|
||||
random = SystemRandom()
|
||||
cache = Cache()
|
||||
login_manager = LoginManager()
|
||||
db = SQLAlchemy()
|
||||
sentry = Sentry()
|
96
server/easyctf/templates/admin/problems.html
Executable file
|
@ -0,0 +1,96 @@
|
|||
{% from "templates.html" import render_field, render_generic_field, render_editor %}
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Problem Editor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.2/ace.js" integrity="sha384-niN+bRlaXMBoq/f5L4zKOEYGxuD0YRGBocto9LP2fB1UiMfxrymATRN4GqjVUt6J" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
.ace_editor {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
</style>
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>Problem Editor</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="tabbable">
|
||||
<ul class="nav nav-pills nav-stacked col-md-3">
|
||||
<li{% if not current_problem %} class="active"{% endif %}>
|
||||
<a href="{{ url_for("admin.problems") }}"><i class="fa fa-fw fa-plus"></i> New</a>
|
||||
</li>
|
||||
{% for _problem in problems %}
|
||||
<li{% if _problem.pid == current_problem.pid %} class="active"{% endif %}>
|
||||
<a href="{{ url_for("admin.problems", pid=_problem.pid) }}">{{ _problem.title }} ({{ _problem.value }} points)</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">{% if not current_problem %}New Problem{% else %}Editing
|
||||
<b>{{ current_problem.title }}</b>{% endif %}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="add-status"></div>
|
||||
<form method="POST">
|
||||
{{ problem_form.csrf_token }}
|
||||
<fieldset>
|
||||
{{ render_field(problem_form.author) }}
|
||||
{{ render_field(problem_form.title) }}
|
||||
{{ render_field(problem_form.name) }}
|
||||
{{ render_field(problem_form.category) }}
|
||||
{{ render_field(problem_form.value) }}
|
||||
{{ render_editor(problem_form.description, "markdown") }}
|
||||
{{ problem_form.programming(style="display: none;") }}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation"
|
||||
{% if not problem_form.programming.data %} class="active"{% endif %}>
|
||||
<a href="#normal-problem" class="programming" data-programming="0" aria-controls="normal-problem" role="tab" data-toggle="tab">Standard Problem</a>
|
||||
</li>
|
||||
<li role="presentation"
|
||||
{% if problem_form.programming.data %} class="active"{% endif %}>
|
||||
<a href="#programming-problem" class="programming" data-programming="1" aria-controls="programming-problem" role="tab" data-toggle="tab">Programming Problem</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane{% if not problem_form.programming.data %} active{% endif %}" id="normal-problem">
|
||||
{{ render_generic_field(problem_form.autogen) }}
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane{% if problem_form.programming.data %} active{% endif %}" id="programming-problem">
|
||||
{{ render_field(problem_form.test_cases) }}
|
||||
{{ render_field(problem_form.time_limit) }}
|
||||
{{ render_field(problem_form.memory_limit) }}
|
||||
{{ render_editor(problem_form.generator, "python") }}
|
||||
</div>
|
||||
{{ render_editor(problem_form.grader, "python") }}
|
||||
</div>
|
||||
</fieldset>
|
||||
{{ problem_form.submit(class_="btn btn-primary") }}
|
||||
</form>
|
||||
{% if current_problem %}
|
||||
<form method="POST" action="{{ url_for("admin.delete_problem", pid=current_problem.pid) }}">
|
||||
<input type="submit" class="btn btn-danger" value="Delete"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$("a.programming").on("shown.bs.tab", function (e) {
|
||||
$("input#programming").prop("checked", $(e.target).data("programming") == true);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
42
server/easyctf/templates/admin/settings.html
Executable file
|
@ -0,0 +1,42 @@
|
|||
{% from "templates.html" import render_field %}
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}CTF Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST">
|
||||
{{ settings_form.csrf_token }}
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<div style="float: right;">
|
||||
{{ settings_form.submit(class_="btn btn-lg btn-success") }}
|
||||
</div>
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">General Settings</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<fieldset class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 form-group">
|
||||
{{ render_field(settings_form.ctf_name) }}
|
||||
{{ render_field(settings_form.team_size) }}
|
||||
{{ render_field(settings_form.start_time) }}
|
||||
{{ render_field(settings_form.end_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
47
server/easyctf/templates/base/about.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}About{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>About</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="page-header">
|
||||
<h2>What? Hacking?</h2>
|
||||
</div>
|
||||
|
||||
<p>Yup. You heard that right. EasyCTF is a online hacking/cybersecurity contest targeted at middle and high school students. Like many similar CTF competitions, participants will have to crack, decompile, decrypt, etc. through many defenses in order to find a secret message, known as the "flag". The challenges presented are designed with the intent of being hacked, making it an excellent and legal way for students to get some great hands-on experience.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>How many people per team?</h2>
|
||||
</div>
|
||||
|
||||
<p>Teams can consist of up to 5 members. There's no advantage to having less people on your team, but it's definitely to your advantage to have more people helping you, so invite your friends to play along! Only teams that consist of middle or high school students are eligible for prizes. Please see our <a href="{{ url_for('base.rules') }}">rules</a> page to review what constitutes an eligible team.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Help, I don't know anything!</h2>
|
||||
</div>
|
||||
|
||||
<p>That's completely fine! Like its name suggests, EasyCTF is an intro-level CTF contest, but can be enjoyable for new and veteran hackers alike. We don't expect you to know anything, but we do expect you to learn! With the "programming" category, you can practice some of the foundational skills required to solve some of the later challenges.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Wait, can't I just look this stuff up?</h2>
|
||||
</div>
|
||||
|
||||
<p>Absolutely. In fact, we encourage you to use whatever resources you have, as long as it complies with our rules. CTF contests are created with the intention of allowing participants to look up information about topics they aren't familiar with. Our goal here is to open the door to cybersecurity and computer science for young and prospective hackers, and being able to find the information you want through searching the Internet is a great skill to have. Please see our <a href="{{ url_for('base.rules') }}">rules</a> page to review what resources you are allowed to use.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>How can I prepare?</h2>
|
||||
</div>
|
||||
|
||||
<p>You don't need any background experience to play EasyCTF! That being said, if you still want to know what kind of problems are going to show up in the contest, try reading some of <a href="http://writeups.easyctf.com/" target="_blank">these writeups from last year</a>, or practicing with challenges from other past CTFs.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
66
server/easyctf/templates/base/easter.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Easter Eggs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section" style="background-color: black;">
|
||||
<div class="container" style="color: #0f0;">
|
||||
<h1>Easter Eggs</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<p>Here's how the Easter Egg game works:</p>
|
||||
<ul>
|
||||
<li>Easter eggs are scattered throughout the site, challenges, and other places that are related to EasyCTF</li>
|
||||
<li>You don't get to know how many easter eggs there are</li>
|
||||
<li>You don't know how many easter eggs other people have</li>
|
||||
<li>Whoever finds the most eggs gets a special bonus ;)</li>
|
||||
</ul>
|
||||
<p>Good luck! Remember, don't spoil the fun of finding this page by telling other people!</p>
|
||||
|
||||
{% if current_user.admin %}
|
||||
<h2>Admin Panel</h2>
|
||||
<p>Add easter eggs here. Go to the database to remove eggs.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="form-horizontal" method="POST">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" autocomplete="off" placeholder="egg{ }" name="egg">
|
||||
<span class="input-group-btn">
|
||||
<input class="btn btn-primary" type="submit" name="submit" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
<ul>
|
||||
{% for egg in eggs %}
|
||||
<li><code>{{ egg.flag }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h2>Submit Easter Egg</h2>
|
||||
<p>For getting this far, here's an easter egg: <code>egg{backdoor_from_the_90's}</code>.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="form-horizontal" method="POST">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" autocomplete="off" placeholder="egg{ }" name="egg">
|
||||
<span class="input-group-btn">
|
||||
<input class="btn btn-primary" type="submit" name="submit" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p></p>
|
||||
<ul>
|
||||
{% for solve in eggs %}
|
||||
<li><code>{{ solve.egg.flag }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
69
server/easyctf/templates/base/index.html
Executable file
|
@ -0,0 +1,69 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header page-header-top gradient" id="masthead">
|
||||
<center style="margin-top:120px; margin-bottom:60px;">
|
||||
<div id="title">
|
||||
<h1>EasyCTF <div class="hidden-xs" style="display: inline;">IV</div></h1>
|
||||
<h2>High School Hacking Competition</h2>
|
||||
<a href="https://www.timeanddate.com/countdown/to?iso=20180210T19&p0=64&msg=EasyCTF+IV&font=sanserif&csz=1" target="_blank"><i class="fa fa-calendar"></i> February 10 to February 20, 2018</a>
|
||||
</div>
|
||||
</center>
|
||||
<center>
|
||||
<p>
|
||||
<a href="{{ url_for("base.about") }}" class="btn btn-lg btn-primary">Learn More</a>
|
||||
<a href="{{ url_for("users.register") }}" class="btn btn-lg btn-success">Register »</a>
|
||||
</p>
|
||||
</center>
|
||||
</div>
|
||||
<div class="section" style="background-color: #f8f8f8;">
|
||||
<div class="container">
|
||||
<div class="page-header" style="text-align: center;">
|
||||
<h1>Ever wanted to be a hacker?</h1>
|
||||
<h3 style="color: #999;">EasyCTF is one of the largest student-run high school cybersecurity events.</h3>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-xs-6" style="text-align: center;">
|
||||
<h3><i class="fa fa-fw fa-2x fa-desktop"></i></h3>
|
||||
<h3>Play</h3>
|
||||
<p>Whether you're a seasoned CTFer, or you've never written a line of code, you'll be able to get involved with EasyCTF!</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6" style="text-align: center;">
|
||||
<h3><i class="fa fa-fw fa-2x fa-graduation-cap"></i></h3>
|
||||
<h3>Learn</h3>
|
||||
<p>Our new and improved classrooms feature makes it easier for teachers to integrate EasyCTF into classroom instruction!</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6" style="text-align: center;">
|
||||
<h3><i class="fa fa-fw fa-2x fa-bar-chart"></i></h3>
|
||||
<h3>Compete</h3>
|
||||
<p>Solve challenges faster than other teams to earn your spot on the leaderboard, whether you're eligible or not.</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6" style="text-align: center;">
|
||||
<h3><i class="fa fa-fw fa-2x fa-trophy"></i></h3>
|
||||
<h3>Win</h3>
|
||||
<p>Teams that consist of high school students from US schools are eligible to win prizes! Click <a href="{{ url_for('base.about') }}">here</a> to learn more.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="page-header">
|
||||
<h1>What is EasyCTF?</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h3>Intense Cybersecurity Contest</h3>
|
||||
<p>No, we're not running around in a gym, tagging other players. Capture the flag contests, or CTFs for short, are intense cybersecurity contests where participants try to capture pieces of information, called flags.</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h3>Fun, Competitive Style</h3>
|
||||
<p>The race to the top is on! Submit flags and watch your team go up on the <span title="not necessarily in real time">leaderboard</span>. You'll get a full week to solve as many challenges as possible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
20
server/easyctf/templates/base/prizes.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Prizes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>Prizes</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<h2>Confirmed Prizes</h2>
|
||||
<ul>
|
||||
<li>Top 10 eligible teams will receive <b>EasyCTF stickers</b>.</li>
|
||||
</ul>
|
||||
<h3>More prizes incoming!</h3>
|
||||
<p>Join us on <a href="{{ url_for('base.updates') }}" target="_blank"><i class="fa fa-comment"></i> Discord</a> to get notified when this information is available.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
46
server/easyctf/templates/base/rules.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Rules{% endblock %}
|
||||
{% block content %}
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>Rules</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3>Please follow these rules to give everyone a fair chance at winning.</h3>
|
||||
<ol>
|
||||
<li><b>Use common sense.</b> If you feel like what you're doing is disrupting the competition in any way, <b>STOP</b> doing it. Don't wait for us to yell at you.</li>
|
||||
<li><b>Don't cheat.</b> We're encouraging you to do your own work. While it's acceptable to use resources on the internet such as documentation or forums, it's not acceptable to ask someone other than the 1-5 members of your team to directly help you on the problem. Don't ask questions on public forums involving specific details of the problems. Don't share flags or methods with other teams.</li>
|
||||
<li><b>Don't disrupt others.</b> Don't attack other teams, and don't attack the contest infrastructure in order to prevent others from solving problems or submitting flags.</li>
|
||||
<li><b>Don't make many accounts.</b> Making multiple accounts for any purpose ("multi-accounting") is not allowed. If you are a teacher and want to set up a class, please ask your students to create accounts for themselves and then you can add them to your classroom. Any hints of multi-accounting is basis for disqualification, no questions asked.</li>
|
||||
<li><b>Be nice.</b> Since this is an educational environment, don't use profanity in your username or team names. Inappropriate names are subject to account deletion or immediate disqualification at the discretion of the organizers. Also, don't spam or harass people in chat.</li>
|
||||
</ol>
|
||||
<p>If you break any of these rules, your team may be converted into an observer team (see below), disqualifying you from winning prizes. In severe cases, we may decide to remove you from the competition altogether. <b>Decisions made by competition organizers are final.</b></p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Who can play?</h2>
|
||||
</div>
|
||||
|
||||
<p>Anyone is welcome to play in EasyCTF! However, since EasyCTF is targeted at students enrolled in middle schools and high schools in the US, only those students will be eligible for prizes. People who don't fall in the category of students enrolled in a middle or high school in the US are <i>observers</i>. While observers are allowed to place on the scoreboard, they will not be eligible for winning prizes. Teams with at least one observer member will be considered an observer team.</p>
|
||||
<p>After the contest is over, we will contact the winning teams to verify that their team meets the conditions before distributing prizes. If a winning team does not meet these conditions, their team will be considered an observer team, and the prizes will be given to the next highest team.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Flag Format</h2>
|
||||
</div>
|
||||
|
||||
<p>The flags that you find will usually follow the format <code>easyctf{flag}</code>. This way, you will know you have the flag when you find it. Problems that don't follow this format will indicate that they don't follow the format next to their problem statement. If you believe you've found a flag but the scoring server is not accepting it, please contact the competition organizers.</p>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Scoring</h2>
|
||||
</div>
|
||||
|
||||
<p>In EasyCTF, every problem has an assigned point value, which is usually representative of the difficulty of the problem. Your team's total score is the sum of the points you obtain from every problem you solve. Problems may have speed bonuses, allowing you to earn extra points for solving it faster. The speed bonus varies per problem and will be indicated in the problem listing.</p>
|
||||
<p>Teams with a higher score will outrank teams with a lower score. The latest listing of every team's score will appear on their team's profile, and an estimate of the latest ranking will appear on the scoreboard page. Ties between teams that have an equal amount of points will be settled by their performance on higher-valued problems. For example, if two teams tied at 100 points by solving a 20-point problem and an 80-point problem, the team that solved the 80-point problem first will outrank the other.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
73
server/easyctf/templates/base/scoreboard.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Scoreboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* .unrank { display: none; } */
|
||||
</style>
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>Scoreboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<a id="toggle-observer" href="javascript:toggleObserver();" class="btn btn-info" data-toggle="tooltip" data-placement="right" title="Observer teams will show up in blue.">Show Observer Teams</a>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Team Name</th>
|
||||
<th>School</th>
|
||||
<th>Points</th>
|
||||
<th>Last Solve</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set rank = 1 %}
|
||||
{% set unrank = 1 %}
|
||||
{% for team, tid, score, date in scoreboard %}
|
||||
<tr{% if team.observer %} class="info" style="display: none;"{% endif %}>
|
||||
<td>
|
||||
<span class="unrank" style="display: none;">{{ unrank }}</span>
|
||||
<span class="rank">{{ rank }}</span>
|
||||
</td>
|
||||
<td><a href="{{ url_for("teams.profile", tid=tid) }}">{{ team.teamname }}</a></td>
|
||||
<td>{{ team.school }}</td>
|
||||
<td>{{ score }}</td>
|
||||
<td><span data-livestamp="{{ date | to_timestamp }}"></span></td>
|
||||
|
||||
{% set unrank = unrank + 1 %}
|
||||
{% if not team.observer %}
|
||||
{% set rank = rank + 1 %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var showObserver = false;
|
||||
var toggleObserver;
|
||||
$(function () {
|
||||
$("[data-toggle='tooltip']").tooltip();
|
||||
toggleObserver = function() {
|
||||
if (showObserver) {
|
||||
$("span.unrank").hide();
|
||||
$("span.rank").show();
|
||||
$("tr.info").hide();
|
||||
$("#toggle-observer").html("Show Observer Teams");
|
||||
showObserver = false;
|
||||
} else {
|
||||
$("span.unrank").show();
|
||||
$("span.rank").hide();
|
||||
$("tr.info").show();
|
||||
$("#toggle-observer").html("Hide Observer Teams");
|
||||
showObserver = true;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
29
server/easyctf/templates/base/sponsors.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}Sponsors{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="section gradient">
|
||||
<div class="container">
|
||||
<h1>Sponsors</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="container">
|
||||
<p>Shout out to these awesome people for helping make EasyCTF happen!</p>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<table class="logo-table">
|
||||
<tr>
|
||||
<td><img src="/assets/images/digitalocean.png" class="logo" /></td>
|
||||
<td>
|
||||
<h2><a href="https://www.digitalocean.com" target="_blank">DigitalOcean</a></h2>
|
||||
<p>Providing developers and businesses a reliable, easy-to-use cloud computing platform of virtual servers (Droplets), object storage (Spaces), and more.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p>If you're interested in sponsoring, shoot us an email at <a href="mailto:team@easyctf.com">team@easyctf.com</a>!</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|