This commit is contained in:
Michael Zhang 2018-02-20 22:37:10 -06:00
commit 4225cc4dde
No known key found for this signature in database
GPG key ID: A1B65B603268116B
157 changed files with 18826 additions and 0 deletions

10
.gitignore vendored Normal file
View 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
View 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
View file

@ -0,0 +1,7 @@
{
"resource_prefix": "staging_",
"region": "nyc3",
"load_balancer": {
"name": "load_balancer"
}
}

0
db/Vagrantfile vendored Normal file
View file

15
db/conf.d/easyctf.cnf Executable file
View 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
View file

@ -0,0 +1 @@
config.yml

View file

@ -0,0 +1 @@
private_key: "~/.ssh/id_rsa"

54
deploy/deploy.py Normal file
View 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
View file

@ -0,0 +1 @@
.env

53
filestore/app.py Executable file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
UPLOAD_FOLDER=/usr/share/nginx/html
FILESTORE_PORT=8001

2
filestore/requirements.txt Executable file
View file

@ -0,0 +1,2 @@
flask
gunicorn

View 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
View 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
View 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
View 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
View 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

Binary file not shown.

300
judge/executor.py Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

0
judge/output Normal file
View file

View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,4 @@
__pycache__
*.pyc
easyctf.db
secret.sh

3
server/app.py Executable file
View file

@ -0,0 +1,3 @@
from easyctf import create_app
app = create_app()

65
server/cloud-provision.sh Executable file
View 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
View 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

File diff suppressed because one or more lines are too long

2337
server/easyctf/assets/css/font-awesome.css vendored Executable file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View 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;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

7
server/easyctf/assets/js/bootstrap.min.js vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
server/easyctf/assets/js/livestamp.min.js vendored Executable file
View 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
View 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});

File diff suppressed because one or more lines are too long

View 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});

View 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
View 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
View 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
View 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)

View file

@ -0,0 +1 @@
#

94
server/easyctf/forms/admin.py Executable file
View 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
View 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)")

View 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
View 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
View 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
View 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.")

View 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
View 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
View 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()

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 &raquo;</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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

Some files were not shown because too many files have changed in this diff Show more