├── .codecov.yml ├── .coveragerc ├── .editorconfig ├── .gcloudignore ├── .gitignore ├── .hooks └── pre-commit.sh ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.yaml ├── config.example.py ├── doc ├── developing │ ├── README.md │ └── requirements.txt ├── docker │ ├── supervisord.conf │ └── uwsgi.ini ├── nginx.conf └── uwsgi.ini ├── main.py ├── requirements.txt ├── scoreboard ├── __init__.py ├── attachments │ ├── __init__.py │ ├── file.py │ ├── gcs.py │ └── testing.py ├── auth │ ├── __init__.py │ └── local.py ├── cache.py ├── config_defaults.py ├── context.py ├── controllers.py ├── csrfutil.py ├── errors.py ├── logger.py ├── mail.py ├── main.py ├── models.py ├── rest.py ├── tests │ ├── __init__.py │ ├── base.py │ ├── cache_test.py │ ├── controllers_test.py │ ├── csrfutil_test.py │ ├── data.py │ ├── models_test.py │ ├── rest_test.py │ ├── utils_test.py │ └── validators_test.py ├── utils.py ├── validators │ ├── __init__.py │ ├── base.py │ ├── nonce.py │ ├── per_team.py │ ├── regex.py │ └── static_pbkdf2.py ├── views.py └── wsgi.py ├── static ├── css │ └── .keep ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── js │ ├── Chart.Step.js │ ├── app.js │ ├── controllers │ │ ├── admin │ │ │ ├── challenges.js │ │ │ ├── news.js │ │ │ ├── page.js │ │ │ ├── teams.js │ │ │ └── tools.js │ │ ├── challenges.js │ │ ├── global.js │ │ ├── page.js │ │ ├── registration.js │ │ ├── scoreboard.js │ │ └── teams.js │ ├── directives.js │ ├── filters.js │ └── services │ │ ├── admin.js │ │ ├── challenges.js │ │ ├── global.js │ │ ├── page.js │ │ ├── session.js │ │ ├── teams.js │ │ ├── upload.js │ │ └── users.js ├── partials │ ├── admin │ │ ├── attachments.html │ │ ├── challenge.html │ │ ├── challenges.html │ │ ├── news.html │ │ ├── page.html │ │ ├── pages.html │ │ ├── restore.html │ │ ├── tags.html │ │ ├── teams.html │ │ ├── tools.html │ │ └── users.html │ ├── challenge_grid.html │ ├── components │ │ ├── challenge.html │ │ └── countdown.html │ ├── login.html │ ├── page.html │ ├── profile.html │ ├── pwreset.html │ ├── register.html │ ├── scoreboard.html │ └── team.html ├── scss │ ├── scoreboard-colors.scss │ ├── scoreboard-mobile.scss │ └── scoreboard.scss └── third_party │ ├── angular │ ├── LICENSE │ ├── angular-csp.css │ ├── angular-resource.js │ ├── angular-resource.min.js │ ├── angular-route.js │ ├── angular-route.min.js │ ├── angular-sanitize.js │ ├── angular-sanitize.min.js │ ├── angular.js │ └── angular.min.js │ ├── bootstrap-theme │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ └── bootstrap-theme.min.css │ ├── bootstrap │ ├── LICENSE │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.js │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ └── bootstrap.min.js │ ├── chart │ ├── Chart.Scatter.js │ ├── Chart.Scatter.min.js │ ├── Chart.js │ ├── Chart.min.js │ ├── LICENSE.md │ └── Scatter.LICENSE.md │ ├── jquery │ ├── LICENSE.txt │ ├── jquery.js │ └── jquery.min.js │ ├── moment │ ├── LICENSE │ ├── moment.js │ └── moment.min.js │ └── pagedown │ ├── LICENSE.txt │ ├── Markdown.Converter.js │ ├── Markdown.Editor.js │ ├── Markdown.Sanitizer.js │ └── wmd-buttons.png ├── templates ├── base.html ├── error.html ├── index.html └── pwreset.eml └── tests.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: true 4 | comment: 5 | behavior: default 6 | layout: header, diff 7 | require_changes: false 8 | coverage: 9 | precision: 2 10 | range: 11 | - 50.0 12 | - 90.0 13 | round: down 14 | status: 15 | changes: false 16 | patch: true 17 | project: true 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: true 22 | loop: true 23 | macro: false 24 | method: false 25 | javascript: 26 | enable_partials: false 27 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = scoreboard 3 | omit = 4 | scoreboard/tests/* 5 | main.py 6 | setup.py 7 | branch = True 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.py] 13 | indent_style = space 14 | indent_size = 4 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | .gcloudignore 2 | .git 3 | scoreboard/tests 4 | *.md 5 | *.pyc 6 | htmlcov/ 7 | doc/ 8 | .hooks/ 9 | LICENSE 10 | AUTHORS 11 | Makefile 12 | Dockerfile 13 | config.example.py 14 | .coverage 15 | .coveragerc 16 | .travis.yml 17 | .editorconfig 18 | .gitignore 19 | .codecov.yml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled, swap files 2 | *.pyc 3 | *.swp 4 | .virtualenv 5 | __pycache__ 6 | 7 | # Runtime data 8 | /config.py 9 | *.bak 10 | *.db 11 | /attachments 12 | /attachments/** 13 | 14 | # Generated files 15 | /static/js/app.min.js 16 | /static/css/*.css 17 | 18 | # Python coverage tool2 19 | .coverage 20 | htmlcov 21 | -------------------------------------------------------------------------------- /.hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${SKIP_TESTS}" != "" ] ; then 4 | exit 0 5 | fi 6 | 7 | # stash code not to be committed 8 | git stash -q --keep-index >/dev/null 2>&1 9 | 10 | RESULT=0 11 | 12 | if git status --porcelain | awk '{print $2}' | grep -q '^scoreboard/' ; then 13 | # Run tests and flake8 if any files in scoreboard/... changed. 14 | python3 tests.py && flake8 scoreboard main.py 15 | RESULT=$? 16 | fi 17 | 18 | # restore stash 19 | # git has a bad bug with 2.24 and --quiet where it deletes files 20 | if git --version | grep -q '^git version 2.24' ; then 21 | git stash pop >/dev/null 2>&1 22 | else 23 | git stash pop -q >/dev/null 2>&1 24 | fi 25 | 26 | exit $RESULT 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.6" 6 | - "3.7" 7 | 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install -r doc/developing/requirements.txt 11 | - pip install codecov 12 | 13 | script: 14 | - coverage run tests.py 15 | - flake8 . 16 | 17 | after_success: 18 | - codecov 19 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Current maintainer: David Tomaschik 2 | 3 | Core Team: 4 | 5 | Andrew Griffiths 6 | David Tomaschik 7 | Niru Ragupathy 8 | Zachary Wade 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement] 6 | (https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | Before you start working on a larger contribution, you should get in touch with 15 | us first through the issue tracker with your idea so that we can help out and 16 | possibly guide you. Coordinating up front makes it much easier to avoid 17 | frustration later on. 18 | 19 | ### Code reviews 20 | All submissions, including submissions by project members, require review. We 21 | use Github pull requests for this purpose. 22 | 23 | ### The small print 24 | Contributions made by corporations are covered by a different agreement than 25 | the one above, the 26 | [Software Grant and Corporate Contributor License Agreement] 27 | (https://cla.developers.google.com/about/google-corporate). 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | nginx \ 5 | python3 \ 6 | python3-dev \ 7 | python3-pip \ 8 | supervisor \ 9 | uwsgi \ 10 | uwsgi-plugin-python3 \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | COPY requirements.txt requirements.txt 14 | RUN pip3 install -r requirements.txt 15 | 16 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 17 | COPY doc/nginx.conf /etc/nginx/sites-enabled/default 18 | COPY doc/docker/supervisord.conf /etc/supervisor/conf.d/ 19 | 20 | COPY . /opt/scoreboard 21 | # Suggest you mount a config at /opt/scoreboard/config.py instead 22 | COPY config.example.py /opt/scoreboard/config.py 23 | WORKDIR /opt/scoreboard 24 | 25 | RUN make 26 | 27 | # TODO: migrate this to run at runtime 28 | RUN python3 main.py createdb 29 | RUN chmod 666 /tmp/scoreboard* 30 | 31 | CMD ["/usr/bin/supervisord"] 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile to minimize JS using UglifyJS (https://github.com/mishoo/UglifyJS2) 2 | # or 'cat' to just assemble into one file. 3 | 4 | MIN_JS:=static/js/app.min.js 5 | JS_SRC:=$(shell find static/js \! -name '*.min.*' -name '*.js') 6 | MINIFY:=$(shell which uglifyjs >/dev/null && echo `which uglifyjs` || echo cat) 7 | 8 | # Declarations for SCSS 9 | SCSS_SRC:=$(shell find static/scss -name '*.scss') 10 | PYSCSS:=$(shell which pyscss >/dev/null && echo `which pyscss` ) 11 | 12 | all: $(MIN_JS) scss 13 | 14 | $(MIN_JS): $(JS_SRC) 15 | $(MINIFY) $^ > $@ 16 | 17 | dev: scss 18 | python main.py 19 | 20 | scss: 21 | @if [ "$(PYSCSS)" = "" ]; then\ 22 | echo "pyscss not found, exiting";\ 23 | exit -1;\ 24 | fi;\ 25 | for i in $$(ls static/scss/); do\ 26 | echo "Making $${i%.scss}.css";\ 27 | $(PYSCSS) -o static/css/$${i%.scss}.css static/scss/$$i;\ 28 | done 29 | 30 | tests: 31 | python tests.py 32 | 33 | coverage: 34 | coverage run main.py runtests 35 | coverage html 36 | xdg-open htmlcov/index.html 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CTF Scoreboard ## 2 | 3 | This is a basic CTF Scoreboard, with support for teams or individual 4 | competitors, and a handful of other features. 5 | 6 | Copyright 2020 Google LLC. 7 | This is not an official Google product. 8 | 9 | Author: Please see the AUTHORS file. 10 | 11 | This is a version 2.x branch. We've eliminated categories, in favor of tagging 12 | challenges. This simplifies the codebase significantly, and is a better fit 13 | since so many challenges border on more than one category. However, this branch 14 | is not compatible with databases from 1.x. If you need that, check out the 1.x 15 | branch, which will only be getting security & bug fixes. 16 | 17 | ### Installation ### 18 | 19 | 1. Install Python with PIP and setuptools. If you'd like to use a virtualenv, 20 | set one up and activate it now. Please note that only Python 3.6+ is 21 | officially supported at the present time, but it should still work on Python 2.7. 22 | 23 | 2. Install the dependencies: 24 | pip install -r requirements.txt 25 | 26 | 3. Install a database library. For MySQL, consider PyMySQL. For Postgres, 27 | use psycopg2. (Others may work; untested.) 28 | 29 | 4. Write a config.py for your relevant installation. An example is provided in 30 | config.example.py. 31 | 32 | SQLALCHEMY_DATABASE_URI = 'mysql://username:password@server/db' 33 | #SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://username:password@server/db' 34 | SECRET_KEY = 'Some Random Value For Session Keys' 35 | TEAM_SECRET_KEY = 'Another Random Value For Team Invite Codes' 36 | TITLE = 'FakeCTF' 37 | TEAMS = True 38 | ATTACHMENT_DIR = 'attachments' 39 | LOGIN_METHOD = 'local' # or appengine 40 | 41 | If you are using plaintext HTTP to run your scoreboard, you will need to add the 42 | following to your config.py, so that cookies will work: 43 | 44 | SESSION_COOKIE_SECURE = False 45 | 46 | If you are developing the scoreboard, the following settings may be useful for 47 | debugging purposes. Not useful for production usage, however. 48 | 49 | COUNT_QUERIES = True 50 | SQLALCHEMY_ECHO = True 51 | 52 | 5. Create the database: 53 | 54 | python main.py createdb 55 | 56 | 6. Set up your favorite python application server, optionally behind a 57 | webserver. You'll want to use main.app as your WSGI handler. 58 | Tested with uwsgi + nginx. Not tested with anything else, 59 | let me know if you have success. Sample configs are in doc/. 60 | 61 | 7. Register a user. The first user registed is automatically made an admin. 62 | You probably want to register your user before your players get access. 63 | 64 | 8. Have fun! Maybe set up some challenges. Players might like that more. 65 | 66 | 67 | ### Installation using Docker ### 68 | 69 | 1. Navigate to the folder where the Dockerfile is located. 70 | 71 | 2. Run the command below to build a docker image for the scoreboard and tag it as "scoreboard". 72 | 73 | docker build -t "scoreboard" . 74 | 75 | 3. Run the command below to create the docker container. 76 | 77 | docker create -p 80:80 scoreboard 78 | 79 | 4. Find the name of the container you created for the scoreboard. 80 | 81 | docker container ls -a 82 | 83 | 5. Run the command below to start the docker container for the scoreboard. 84 | 85 | docker start "container_name" 86 | 87 | ### Options ### 88 | 89 | **SCORING**: Set to 'progressive' to enable a scoring system where the total 90 | points for each challenge are divided amongst all the teams that solve that 91 | challenge. This rewards teams that solve infrequently solved (hard or obscure) 92 | challenges. 93 | 94 | **TITLE**: Scoreboard page titles. 95 | 96 | **TEAMS**: True if teams should be used, False for each player on their own 97 | team. 98 | 99 | **SQLALCHEMY_DATABASE_URI**: A SQLAlchemy database URI string. 100 | 101 | **LOGIN_METHOD**: Supports 'local' 102 | 103 | ### Development ### 104 | 105 | [![Build Status](https://travis-ci.org/google/ctfscoreboard.svg?branch=master)](https://travis-ci.org/google/ctfscoreboard) 106 | [![codecov](https://codecov.io/gh/google/ctfscoreboard/branch/master/graph/badge.svg)](https://codecov.io/gh/google/ctfscoreboard) 107 | 108 | **Use hooks** 109 | 110 | ln -s ../../.hooks/pre-commit.sh .git/hooks/pre-commit 111 | 112 | **Test Cases** 113 | 114 | - Setup database 115 | - Create user, verify admin 116 | - Create challenge 117 | - With, without attachment 118 | - Edit challenges 119 | - Add attachment 120 | - Delete attachment 121 | - Download backup 122 | - Restore backup 123 | - Create 2nd user, verify not admin 124 | - Solve challenge 125 | - Download attachment 126 | 127 | 128 | ### Thanks ### 129 | 130 | This project stands on the shoulders of giants. 131 | A big thanks to the following projects used to build this: 132 | 133 | - [Flask](http://flask.pocoo.org/) 134 | - [Flask-SQLAlchemy](https://pythonhosted.org/Flask-SQLAlchemy/) 135 | - [Flask-RESTful](https://flask-restful.readthedocs.io/en/latest/) 136 | - [SQLAlchemy](http://www.sqlalchemy.org/) 137 | - [AngularJS](https://angularjs.org/) 138 | - [jQuery](https://jquery.com/) 139 | - [PageDown](https://jquery.com/) 140 | - [Bootstrap](http://getbootstrap.com/) 141 | 142 | And many more indirect dependencies. 143 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: python37 2 | instance_class: F4 3 | automatic_scaling: 4 | max_instances: 50 5 | max_idle_instances: 10 6 | 7 | handlers: 8 | - url: /css 9 | static_dir: static/css 10 | secure: always 11 | 12 | - url: /js 13 | static_dir: static/js 14 | secure: always 15 | 16 | - url: /partials 17 | static_dir: static/partials 18 | secure: always 19 | 20 | - url: /fonts 21 | static_dir: static/fonts 22 | secure: always 23 | 24 | - url: /third_party 25 | static_dir: static/third_party 26 | secure: always 27 | 28 | - url: /createdb 29 | secure: always 30 | script: auto 31 | 32 | - url: /.* 33 | secure: always 34 | script: auto 35 | -------------------------------------------------------------------------------- /config.example.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # Demo config.py, please configure your own 17 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/scoreboard.db' 18 | SQLALCHEMY_TRACK_MODIFICATIONS = True 19 | SECRET_KEY = 'CHANGEME CHANGEME CHANGEME' 20 | # Set TEAM_SECRET_KEY to a unique value so that you can rotate session 21 | # secrets (SECRET_KEY) without affecting team invite codes. 22 | TEAM_SECRET_KEY = SECRET_KEY 23 | TITLE = 'CTF Scoreboard Dev' 24 | TEAMS = True 25 | ATTACHMENT_BACKEND = 'file:///tmp/attachments' 26 | LOGIN_METHOD = 'local' 27 | SESSION_COOKIE_SECURE = False 28 | PROOF_OF_WORK_BITS = 12 29 | -------------------------------------------------------------------------------- /doc/developing/README.md: -------------------------------------------------------------------------------- 1 | # Development Setup 2 | 3 | You'll want to have `python3` and `pip` installed. I also recommend 4 | `virtualenv` and `virtualenvwrapper` (if you don't have these, skip the 5 | virtualenv steps). 6 | 7 | On Debian or Ubuntu Linux, run: 8 | 9 | `apt install python3 python3-pip virtualenvwrapper` 10 | 11 | 1. `mkvirtualenv -p $(which python3) scoreboard` to create the virtualenv. 12 | 2. `git clone https://github.com/google/ctfscoreboard && cd ctfscoreboard` to clone the source. 13 | 3. `pip install -r requirements.txt` to install runtime dependencies. 14 | 4. `pip install -r doc/developing/requirements.txt` to install development 15 | dependencies. 16 | 5. `ln -s .hooks/pre-commit.sh .git/hooks/pre-commit` to install the development 17 | pre-commit hook. 18 | 19 | # Configuration & Initial Setup 20 | 21 | Copy the file `config.example.py` to `config.py` to make a configuration. This 22 | is suitable for basic development work, and will use a sqlite3 database in 23 | `/tmp/scoreboard.db` for storage. 24 | 25 | You'll want to run `python main.py createdb` to create the initial database. 26 | 27 | Optionally, you can run `python main.py createdata` to create some test data. 28 | These are just dummy challenges, teams, and users used for testing. 29 | 30 | # Running/Iterating 31 | 32 | You can either run `make dev` or `python main.py` to run the development server. 33 | By default, it runs on port 9999, but you can change this in your `config.py`. 34 | 35 | **Note** that if you make changes to models, affecting the database schema, you 36 | must either manually update your database, or delete it and recreate from 37 | scratch. Because this is a CTF scoreboard, used for short-lived events, there's 38 | no migration code. 39 | 40 | Run `make scss` to compile SCSS to CSS. You'll want to do this at least once, 41 | and any time you change the SCSS. Note that the CSS is not tracked in the git 42 | repository, so all style changes *must* be to SCSS. 43 | 44 | # Making Changes 45 | 46 | Please do all development work on a feature branch. Run the tests before you 47 | commit (if you have the git hook, it should run the the tests before 48 | committing). We try to mostly follow PEP-8, and `flake8` helps catch those 49 | mistakes. 50 | -------------------------------------------------------------------------------- /doc/developing/requirements.txt: -------------------------------------------------------------------------------- 1 | # Additional requirements for development 2 | coverage 3 | flake8 4 | flask-testing 5 | mock 6 | -------------------------------------------------------------------------------- /doc/docker/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:uwsgi] 5 | command=/usr/bin/uwsgi --ini /opt/scoreboard/doc/docker/uwsgi.ini 6 | stdout_logfile=/dev/stdout 7 | stdout_logfile_maxbytes=0 8 | stderr_logfile=/dev/stderr 9 | stderr_logfile_maxbytes=0 10 | 11 | [program:nginx] 12 | command=/usr/sbin/nginx 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 -------------------------------------------------------------------------------- /doc/docker/uwsgi.ini: -------------------------------------------------------------------------------- 1 | # Sample uWSGI config file 2 | [uwsgi] 3 | chdir = /opt/scoreboard 4 | socket = 127.0.0.1:9000 5 | processes = 4 6 | threads = 2 7 | master = true 8 | module = scoreboard.wsgi 9 | callable = app 10 | uid = nobody 11 | gid = nogroup 12 | daemonize = /var/log/uwsgi/app/uwsgi.log 13 | plugins = python3 14 | -------------------------------------------------------------------------------- /doc/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | root /opt/scoreboard/static; # Make sure code is not in document root! 4 | 5 | location @backend { 6 | include uwsgi_params; 7 | uwsgi_pass 127.0.0.1:9000; 8 | } 9 | 10 | location / { 11 | try_files $uri @backend; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /doc/uwsgi.ini: -------------------------------------------------------------------------------- 1 | # Sample uWSGI config file 2 | [uwsgi] 3 | chdir = /opt/scoreboard 4 | socket = 127.0.0.1:9000 5 | processes = 4 6 | threads = 2 7 | master = true 8 | module = scoreboard.wsgi 9 | callable = app 10 | virtualenv = /opt/virtualenv 11 | uid = nobody 12 | gid = nogroup 13 | daemonize = /var/log/uwsgi/app/uwsgi.log 14 | plugins = python 15 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | from scoreboard import wsgi 17 | from scoreboard import models 18 | # For use in gunicorn 19 | from scoreboard.wsgi import app # noqa: F401 20 | 21 | 22 | def main(argv): 23 | if 'createdb' in argv: 24 | models.db.create_all() 25 | elif 'createdata' in argv: 26 | from scoreboard.tests import data 27 | models.db.create_all() 28 | data.create_all() 29 | elif 'shell' in argv: 30 | try: 31 | import IPython 32 | run_shell = IPython.embed 33 | except ImportError: 34 | import readline # noqa: F401 35 | import code 36 | run_shell = code.InteractiveConsole().interact 37 | run_shell() 38 | else: 39 | wsgi.app.run( 40 | host='0.0.0.0', debug=True, 41 | port=wsgi.app.config.get('PORT', 9999)) 42 | 43 | 44 | if __name__ == '__main__': 45 | main(sys.argv) 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-RESTful 3 | Flask-SQLAlchemy 4 | Flask-Scss 5 | Jinja2 6 | MarkupSafe 7 | PyMySQL 8 | SQLAlchemy<1.4.0 9 | Werkzeug<1.0.0 10 | aniso8601 11 | argparse 12 | itsdangerous 13 | pbkdf2 14 | pylibmc 15 | python-dateutil 16 | pytz 17 | six 18 | google-cloud-logging 19 | google-cloud-storage 20 | mailjet_rest 21 | requests 22 | -------------------------------------------------------------------------------- /scoreboard/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /scoreboard/attachments/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | Handle attachments via the appropriate backend. 18 | 19 | Required API: 20 | send(models.Attachment): returns a flask response to download attachment 21 | upload(werkzeug.datastructures.FileStorage): returns attachment ID and path 22 | delete(models.Attachment): deletes the attachment specified 23 | """ 24 | 25 | 26 | try: 27 | import urlparse 28 | except ImportError: 29 | from urllib import parse as urlparse 30 | 31 | from scoreboard import main 32 | 33 | app = main.get_app() 34 | 35 | backend = None 36 | 37 | 38 | def get_backend_path(): 39 | """Get backend path for attachments.""" 40 | return app.config.get('ATTACHMENT_BACKEND') 41 | 42 | 43 | def get_backend_type(): 44 | """Determine type of backend.""" 45 | backend = get_backend_path() 46 | return urlparse.urlparse(backend).scheme 47 | 48 | 49 | def get_backend(_backend_type): 50 | backend = None 51 | if _backend_type == "file": 52 | from . import file as backend 53 | elif _backend_type == "gcs": 54 | from . import gcs as backend 55 | elif _backend_type == "test": 56 | from . import testing as backend 57 | else: 58 | raise ImportError('Unhandled attachment backend %s' % _backend_type) 59 | return backend 60 | 61 | 62 | def patch(_backend_type): 63 | globals()['backend'] = get_backend(_backend_type) 64 | 65 | 66 | backend = get_backend(get_backend_type()) 67 | -------------------------------------------------------------------------------- /scoreboard/attachments/file.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | Local filesystem backend for attachments. 18 | """ 19 | 20 | 21 | import hashlib 22 | import os 23 | import os.path 24 | 25 | try: 26 | import urlparse 27 | except ImportError: 28 | from urllib import parse as urlparse 29 | 30 | import flask 31 | 32 | from scoreboard import main 33 | 34 | app = main.get_app() 35 | 36 | 37 | def attachment_dir(create=False): 38 | """Return path and optionally create attachment directory.""" 39 | components = urlparse.urlparse(app.config.get('ATTACHMENT_BACKEND')) 40 | app.config_dir = components.path or components.netloc 41 | if app.config.get('CWD'): 42 | target_dir = os.path.normpath(os.path.join(app.config.get('CWD'), 43 | app.config_dir)) 44 | else: 45 | target_dir = os.path.abspath(app.config_dir) 46 | if not os.path.isdir(target_dir): 47 | if create: 48 | os.mkdir(target_dir) 49 | else: 50 | app.logger.error('Missing or invalid ATTACHMENT_DIR: %s', 51 | target_dir) 52 | flask.abort(500) 53 | return target_dir 54 | 55 | 56 | def send(attachment): 57 | """Send the attachment to the client.""" 58 | return flask.send_from_directory( 59 | attachment_dir(), attachment.aid, 60 | mimetype=attachment.content_type, 61 | attachment_filename=attachment.filename, 62 | as_attachment=True) 63 | 64 | 65 | def delete(attachment): 66 | """Delete the attachment from disk.""" 67 | path = os.path.join(attachment_dir(), attachment.aid) 68 | os.unlink(path) 69 | 70 | 71 | def upload(fp): 72 | """Upload the file attachment to the storage medium.""" 73 | md = hashlib.sha256() 74 | while True: 75 | blk = fp.read(2**16) 76 | if not blk: 77 | break 78 | md.update(blk) 79 | aid = md.hexdigest() 80 | fp.seek(0, os.SEEK_SET) 81 | dest_name = os.path.join(attachment_dir(create=True), aid) 82 | fp.save(dest_name, buffer_size=2**16) 83 | # TODO: add file:// prefix 84 | return aid, dest_name 85 | -------------------------------------------------------------------------------- /scoreboard/attachments/gcs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | Attachments on Google Cloud Storage. 18 | """ 19 | 20 | 21 | import hashlib 22 | import os 23 | try: 24 | import urlparse 25 | except ImportError: 26 | import urllib.parse as urlparse 27 | try: 28 | from io import BytesIO 29 | except ImportError: 30 | try: 31 | import cStringIO.StringIO as BytesIO 32 | except ImportError: 33 | import StringIO.StringIO as BytesIO 34 | 35 | 36 | import flask 37 | 38 | from google.cloud import storage 39 | from google.cloud import exceptions 40 | 41 | from scoreboard import main 42 | 43 | app = main.get_app() 44 | 45 | 46 | def get_bucket(path=None): 47 | path = path or app.config.get('ATTACHMENT_BACKEND') 48 | url = urlparse.urlparse(path) 49 | return url.netloc 50 | 51 | 52 | def send(attachment): 53 | """Send to download URI.""" 54 | try: 55 | client = storage.Client() 56 | bucket = client.bucket(get_bucket()) 57 | buf = BytesIO() 58 | blob = bucket.get_blob(attachment.aid) 59 | if not blob: 60 | return flask.abort(404) 61 | blob.download_to_file(buf) 62 | buf.seek(0) 63 | return flask.send_file( 64 | buf, 65 | mimetype=attachment.content_type, 66 | attachment_filename=attachment.filename, 67 | add_etags=False, as_attachment=True) 68 | except exceptions.NotFound: 69 | return flask.abort(404) 70 | 71 | 72 | def delete(attachment): 73 | """Delete from GCS Bucket.""" 74 | try: 75 | client = storage.Client() 76 | bucket = client.bucket(get_bucket()) 77 | bucket.delete_blob(attachment.aid) 78 | except exceptions.NotFound: 79 | return flask.abort(404) 80 | 81 | 82 | def upload(fp): 83 | """Upload the attachment.""" 84 | md = hashlib.sha256() 85 | while True: 86 | blk = fp.read(2**16) 87 | if not blk: 88 | break 89 | md.update(blk) 90 | fp.seek(0, os.SEEK_SET) 91 | aid = md.hexdigest() 92 | client = storage.Client() 93 | bucket = client.bucket(get_bucket()) 94 | blob = bucket.blob(aid) 95 | blob.upload_from_file(fp) 96 | path = 'gcs://{}/{}'.format(get_bucket(), aid) 97 | return aid, path 98 | -------------------------------------------------------------------------------- /scoreboard/attachments/testing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | Volatile filesystem backend for attachments. 18 | """ 19 | 20 | 21 | import hashlib 22 | import io 23 | 24 | import flask 25 | 26 | 27 | from scoreboard import main 28 | 29 | app = main.get_app() 30 | 31 | files = {} 32 | 33 | 34 | def send(attachment): 35 | """Send the attachment to the client.""" 36 | return flask.send_file(files[attachment.aid], 37 | attachment_filename="testing.txt", 38 | as_attachment=True) 39 | 40 | 41 | def delete(attachment): 42 | """Delete the attachment from disk.""" 43 | del files[attachment.aid] 44 | 45 | 46 | def upload(fp): 47 | """Upload the file attachment to the storage medium.""" 48 | md = hashlib.sha256() 49 | ret = io.BytesIO() 50 | while True: 51 | blk = fp.read(2**16) 52 | if not blk: 53 | break 54 | md.update(blk) 55 | ret.write(blk) 56 | aid = md.hexdigest() 57 | ret.seek(0) 58 | files[aid] = ret 59 | return aid, ('test://%s' % aid) 60 | -------------------------------------------------------------------------------- /scoreboard/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | This supports multiple auth systems as configured by the LOGIN_METHOD setting. 18 | 19 | The required API includes: 20 | login_user(flask_request): returns User or None 21 | get_login_uri(): returns URI for login 22 | get_register_uri(): returns URI for registration 23 | logout(): returns None 24 | register(flask_request): register a new user 25 | """ 26 | 27 | 28 | from scoreboard import main 29 | 30 | 31 | _login_method = main.get_app().config.get('LOGIN_METHOD') 32 | if _login_method == 'local': 33 | from scoreboard.auth.local import * # noqa: F401,F403 34 | else: 35 | raise ImportError('Unhandled LOGIN_METHOD %s' % _login_method) 36 | -------------------------------------------------------------------------------- /scoreboard/auth/local.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Local login support.""" 17 | 18 | from scoreboard import controllers 19 | from scoreboard import errors 20 | from scoreboard import models 21 | 22 | 23 | def login_user(flask_request): 24 | """Get the user for this request.""" 25 | data = flask_request.get_json() 26 | email = data['email'].lower() 27 | user = models.User.login_user(email, data['password']) 28 | if not user: 29 | raise errors.LoginError('Invalid username/password.') 30 | return user 31 | 32 | 33 | def get_login_uri(): 34 | return '/login' 35 | 36 | 37 | def get_register_uri(): 38 | return '/register' 39 | 40 | 41 | def logout(): 42 | pass 43 | 44 | 45 | def register(flask_request): 46 | data = flask_request.get_json() 47 | user = controllers.register_user( 48 | data['email'].lower(), data['nick'], 49 | data['password'], data.get('team_id'), data.get('team_name'), 50 | data.get('team_code')) 51 | return user 52 | -------------------------------------------------------------------------------- /scoreboard/cache.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import functools 17 | import json 18 | import flask 19 | 20 | from werkzeug.contrib import cache 21 | 22 | from scoreboard import main 23 | 24 | app = main.get_app() 25 | 26 | 27 | class CacheWrapper(object): 28 | 29 | def __init__(self, app): 30 | cache_type = app.config.get('CACHE_TYPE') 31 | if cache_type == 'memcached': 32 | host = app.config.get('MEMCACHE_HOST') 33 | self._cache = cache.MemcachedCache([host]) 34 | elif cache_type == 'local': 35 | self._cache = cache.SimpleCache() 36 | else: 37 | self._cache = cache.NullCache() 38 | 39 | def __getattr__(self, name): 40 | return getattr(self._cache, name) 41 | 42 | 43 | global_cache = CacheWrapper(app) 44 | 45 | 46 | def rest_cache(f_or_key): 47 | """Mark a function for global caching.""" 48 | override_cache_key = None 49 | 50 | def wrap_func(f): 51 | @functools.wraps(f) 52 | def wrapped(*args, **kwargs): 53 | if override_cache_key: 54 | cache_key = override_cache_key 55 | else: 56 | try: 57 | cache_key = '%s/%s' % ( 58 | f.im_class.__name__, f.__name__) 59 | except AttributeError: 60 | cache_key = f.__name__ 61 | return _rest_cache_caller(f, cache_key, *args, **kwargs) 62 | return wrapped 63 | if isinstance(f_or_key, str): 64 | override_cache_key = f_or_key 65 | return wrap_func 66 | return wrap_func(f_or_key) 67 | 68 | 69 | def rest_cache_path(f): 70 | """Cache a result based on the path received.""" 71 | 72 | @functools.wraps(f) 73 | def wrapped(*args, **kwargs): 74 | cache_key = flask.request.path.encode('utf-8') 75 | return _rest_cache_caller(f, cache_key, *args, **kwargs) 76 | return wrapped 77 | 78 | 79 | def rest_team_cache(f_or_key): 80 | """Mark a function for per-team caching.""" 81 | override_cache_key = None 82 | 83 | def wrap_func(f): 84 | @functools.wraps(f) 85 | def wrapped(*args, **kwargs): 86 | if flask.g.tid: 87 | if override_cache_key: 88 | cache_key = override_cache_key % (flask.g.tid) 89 | else: 90 | try: 91 | cache_key = '%s/%s/%s' % ( 92 | f.im_class.__name__, f.__name__, flask.g.tid) 93 | except AttributeError: 94 | cache_key = '%s/%s' % ( 95 | f.__name__, flask.g.tid) 96 | return _rest_cache_caller(f, cache_key, *args, **kwargs) 97 | return f(*args, **kwargs) 98 | return wrapped 99 | if isinstance(f_or_key, str): 100 | override_cache_key = f_or_key 101 | if '%d' not in override_cache_key: 102 | raise ValueError('No way to override the key per team!') 103 | return wrap_func 104 | return wrap_func(f_or_key) 105 | 106 | 107 | def delete(key): 108 | """Delete cache entry.""" 109 | global_cache.delete(key) 110 | 111 | 112 | def clear(): 113 | """Flush global cache.""" 114 | global_cache.clear() 115 | 116 | 117 | def delete_team(base_key): 118 | """Delete team-based cache entry.""" 119 | if not flask.g.tid: 120 | return 121 | global_cache.delete(base_key % flask.g.tid) 122 | 123 | 124 | def _rest_cache_caller(f, cache_key, *args, **kwargs): 125 | value = global_cache.get(cache_key) 126 | if value: 127 | try: 128 | return _rest_add_cache_header(json.loads(value), True) 129 | except ValueError: 130 | pass 131 | value = f(*args, **kwargs) 132 | try: 133 | # TODO: only cache on success 134 | global_cache.set(cache_key, json.dumps(value)) 135 | except TypeError: 136 | pass 137 | return _rest_add_cache_header(value) 138 | 139 | 140 | def _rest_add_cache_header(rv, hit=False): 141 | # TODO: check status codes? 142 | headers = {'X-Cache-Hit': str(hit)} 143 | if isinstance(rv, str): 144 | return (rv, 200, headers) 145 | if isinstance(rv, tuple): 146 | if len(rv) == 1: 147 | return (rv[0], 200, headers) 148 | if len(rv) == 2: 149 | return (rv[0], rv[1], headers) 150 | if len(rv) == 3: 151 | if rv[2] is None: 152 | return (rv[0], rv[1], headers) 153 | if isinstance(rv[2], dict): 154 | rv[2].update(headers) 155 | return rv 156 | if isinstance(rv, (list, dict)): 157 | return rv, 200, headers 158 | # TODO: might need to support Response objects 159 | return rv 160 | -------------------------------------------------------------------------------- /scoreboard/config_defaults.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | 18 | class Defaults(object): 19 | ATTACHMENT_BACKEND = 'file://attachments' 20 | COUNT_QUERIES = False 21 | CSP_POLICY = None 22 | CWD = os.path.dirname(os.path.realpath(__file__)) 23 | DEBUG = False 24 | EXTEND_CSP_POLICY = None 25 | ERROR_404_HELP = False 26 | FIRST_BLOOD = 0 27 | FIRST_BLOOD_MIN = 0 28 | GAME_TIME = (None, None) 29 | INVITE_KEY = None 30 | LOGIN_METHOD = 'local' 31 | MAIL_FROM = None 32 | MAIL_FROM_NAME = None 33 | MAIL_HOST = 'localhost' 34 | NEWS_POLL_INTERVAL = 60000 35 | PROOF_OF_WORK_BITS = 0 36 | RULES = '/rules' 37 | SCOREBOARD_ZEROS = True 38 | SCORING = 'plain' 39 | SECRET_KEY = None 40 | TEAM_SECRET_KEY = None 41 | SESSION_COOKIE_HTTPONLY = True 42 | SESSION_COOKIE_SECURE = True 43 | SQLALCHEMY_TRACK_MODIFICATIONS = True 44 | SESSION_EXPIRATION_SECONDS = 60 * 60 45 | SYSTEM_NAME = 'root' 46 | TEAMS = True 47 | TEASE_HIDDEN = True 48 | TITLE = 'Scoreboard' 49 | SUBMIT_AFTER_END = True 50 | -------------------------------------------------------------------------------- /scoreboard/context.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | import time 17 | 18 | import flask 19 | from sqlalchemy import event 20 | 21 | from scoreboard import main 22 | from scoreboard import models 23 | from scoreboard import utils 24 | 25 | app = main.get_app() 26 | 27 | 28 | DEFAULT_CSP_POLICY = { 29 | 'default-src': ["'self'"], 30 | 'script-src': [ 31 | "'self'", 32 | "'unsafe-eval'", # Needed for Charts.js 33 | ], 34 | 'img-src': [ 35 | "'self'", 36 | 'data:', 37 | ], 38 | 'object-src': ["'none'"], 39 | 'font-src': [ 40 | "'self'", 41 | 'fonts.gstatic.com', 42 | ], 43 | 'style-src': [ 44 | "'self'", 45 | 'fonts.googleapis.com', 46 | "'unsafe-inline'", # Needed for Charts.js 47 | ], 48 | } 49 | 50 | _CSP_POLICY_STRING = None 51 | 52 | 53 | def get_csp_policy(): 54 | global _CSP_POLICY_STRING 55 | if _CSP_POLICY_STRING is not None: 56 | return _CSP_POLICY_STRING 57 | if app.config.get('CSP_POLICY'): 58 | policy = app.config.get('CSP_POLICY') 59 | elif app.config.get('EXTEND_CSP_POLICY'): 60 | policy = collections.defaultdict(list) 61 | for k, v in DEFAULT_CSP_POLICY.items(): 62 | policy[k] = v 63 | for k, v in app.config.get('EXTEND_CSP_POLICY').items(): 64 | policy[k].extend(v) 65 | else: 66 | policy = DEFAULT_CSP_POLICY 67 | components = [] 68 | for k, v in policy.items(): 69 | sources = ' '.join(v) 70 | components.append(k + ' ' + sources) 71 | _CSP_POLICY_STRING = '; '.join(components) 72 | return _CSP_POLICY_STRING 73 | 74 | 75 | # Setup flask.g 76 | @app.before_request 77 | def load_globals(): 78 | """Prepopulate flask.g.* with properties.""" 79 | try: 80 | del flask.g.user 81 | except AttributeError: 82 | pass 83 | try: 84 | del flask.g.team 85 | except AttributeError: 86 | pass 87 | if load_apikey(): 88 | return 89 | if (app.config.get('SESSION_EXPIRATION_SECONDS') and 90 | flask.session.get('expires') and 91 | flask.session.get('expires') < time.time()): 92 | flask.session.clear() 93 | flask.g.uid = flask.session.get('user') 94 | flask.g.tid = flask.session.get('team') 95 | flask.g.admin = flask.session.get('admin') or False 96 | 97 | 98 | def load_apikey(): 99 | """Load flask.g.user, flask.g.uid from an API key.""" 100 | try: 101 | key = flask.request.headers.get('X-SCOREBOARD-API-KEY') 102 | if not key or len(key) != 32: 103 | return 104 | user = models.User.get_by_api_key(key) 105 | if not user: 106 | return 107 | flask.g.user = user 108 | flask.g.uid = user.uid 109 | flask.g.admin = user.admin 110 | flask.g.tid = None 111 | return True 112 | except Exception: 113 | # Don't want any API key problems to block requests 114 | pass 115 | 116 | 117 | # Add headers to responses 118 | @app.after_request 119 | def add_headers(response): 120 | """Add security-related headers to all outgoing responses.""" 121 | h = response.headers 122 | h.setdefault('Content-Security-Policy', get_csp_policy()) 123 | h.setdefault('X-Frame-Options', 'DENY') 124 | h.add('X-XSS-Protection', '1', mode='block') 125 | return response 126 | 127 | 128 | @app.context_processor 129 | def util_contexts(): 130 | return dict(gametime=utils.GameTime) 131 | 132 | 133 | _query_count = 0 134 | 135 | 136 | if app.config.get('COUNT_QUERIES'): 137 | @event.listens_for(models.db.engine, 'before_cursor_execute') 138 | def receive_before_cursor_execute( 139 | conn, cursor, statement, parameters, context, executemany): 140 | global _query_count 141 | _query_count += 1 142 | 143 | @app.after_request 144 | def count_queries(response): 145 | global _query_count 146 | if _query_count > 0: 147 | app.logger.info('Request issued %d queries.', _query_count) 148 | _query_count = 0 149 | return response 150 | 151 | 152 | def ensure_setup(): 153 | if not app: 154 | raise RuntimeError('Invalid app setup.') 155 | -------------------------------------------------------------------------------- /scoreboard/csrfutil.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | import binascii 17 | import flask 18 | import functools 19 | import hashlib 20 | import hmac 21 | import jinja2 22 | import struct 23 | import time 24 | 25 | from scoreboard import main 26 | from scoreboard import utils 27 | 28 | app = main.get_app() 29 | 30 | b64_vals = utils.to_bytes('_-') 31 | 32 | 33 | def _get_csrf_token(user=None, expires=None): 34 | user = user or flask.session.get('user', flask.request.remote_addr) 35 | expires = expires or int(time.time()) + 60 * 60 * 24 36 | expires_bytes = struct.pack('') 79 | return field % token 80 | 81 | 82 | @app.before_request 83 | def csrf_protection_request(): 84 | """Add CSRF Protection to all non-GET/non-HEAD requests.""" 85 | if flask.request.method in ('GET', 'HEAD'): 86 | return 87 | if app.config.get('TESTING'): 88 | return 89 | header = flask.request.headers.get('X-XSRF-TOKEN') 90 | token = header or flask.request.values.get('csrftoken') 91 | if not token or not verify_csrf_token(token): 92 | app.logger.warning('CSRF Validation Failed') 93 | flask.abort(403) 94 | 95 | 96 | @app.after_request 97 | def add_csrf_protection(resp): 98 | """Add the XSRF-TOKEN cookie to all outgoing requests.""" 99 | resp.set_cookie('XSRF-TOKEN', get_csrf_token()) 100 | return resp 101 | 102 | 103 | @app.context_processor 104 | def csrf_context_processor(): 105 | """Add CSRF token and field to all rendering contexts.""" 106 | return { 107 | 'csrftoken': get_csrf_token, 108 | 'csrffield': get_csrf_field, 109 | } 110 | -------------------------------------------------------------------------------- /scoreboard/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Custom error classes plus access to SQLAlchemy exceptions 16 | from werkzeug import exceptions 17 | 18 | from sqlalchemy.exc import * # noqa: F401,F403 19 | from sqlalchemy.orm.exc import * # noqa: F401,F403 20 | 21 | 22 | class _MessageException(exceptions.HTTPException): 23 | """Message with JSON exception.""" 24 | 25 | default_message = 'Error' 26 | 27 | def __init__(self, msg=None): 28 | msg = msg or self.default_message 29 | super(_MessageException, self).__init__() 30 | self.data = {'message': msg} 31 | 32 | 33 | class AccessDeniedError(_MessageException): 34 | """No access to the resource.""" 35 | code = 403 36 | 37 | 38 | class ValidationError(_MessageException): 39 | """Error during input validation.""" 40 | code = 400 41 | 42 | 43 | class InvalidAnswerError(AccessDeniedError): 44 | """Submitting the wrong answer.""" 45 | default_message = 'Ha ha ha... No.' 46 | 47 | 48 | class LoginError(AccessDeniedError): 49 | """Failing to login.""" 50 | default_message = 'Invalid username/password.' 51 | 52 | 53 | class ServerError(_MessageException): 54 | code = 500 55 | -------------------------------------------------------------------------------- /scoreboard/logger.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | import flask 18 | 19 | 20 | class Formatter(logging.Formatter): 21 | """Custom formatter to handle application logging. 22 | 23 | This formatter adds a "client" attribute that will log the user and client 24 | information. 25 | """ 26 | 27 | def format(self, record): 28 | if flask.request: 29 | user = (('UID<%d>' % flask.g.uid) 30 | if 'uid' in flask.g and flask.g.uid else '-') 31 | record.client = "[{}/{}]".format(flask.request.remote_addr, user) 32 | else: 33 | record.client = "" 34 | return super(Formatter, self).format(record) 35 | -------------------------------------------------------------------------------- /scoreboard/mail.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from email.mime import text 16 | import email.utils 17 | import smtplib 18 | import socket 19 | import mailjet_rest 20 | 21 | from scoreboard import main 22 | 23 | app = main.get_app() 24 | 25 | 26 | class MailFailure(Exception): 27 | """Inability to send mail.""" 28 | pass 29 | 30 | 31 | def send(message, subject, to, to_name=None, sender=None, sender_name=None): 32 | """Send an email.""" 33 | sender = sender or app.config.get('MAIL_FROM') 34 | sender_name = sender_name or app.config.get('MAIL_FROM_NAME') or '' 35 | mail_provider = app.config.get('MAIL_PROVIDER') 36 | if mail_provider is None: 37 | app.logger.error('No MAIL_PROVIDER configured!') 38 | raise MailFailure('No MAIL_PROVIDER configured!') 39 | elif mail_provider == 'smtp': 40 | _send_smtp(message, subject, to, to_name, sender, sender_name) 41 | elif mail_provider == 'mailjet': 42 | _send_mailjet(message, subject, to, to_name, sender, sender_name) 43 | else: 44 | app.logger.error('Invalid MAIL_PROVIDER configured!') 45 | raise MailFailure('Invalid MAIL_PROVIDER configured!') 46 | 47 | 48 | def _send_smtp(message, subject, to, to_name, sender, sender_name): 49 | """SMTP implementation of sending email.""" 50 | host = app.config.get('MAIL_HOST') 51 | 52 | if not host: 53 | raise MailFailure('SMTP Server Not Configured') 54 | 55 | try: 56 | server = smtplib.SMTP(host) 57 | except (smtplib.SMTPConnectError, socket.error) as ex: 58 | app.logger.error('Unable to send mail: %s', str(ex)) 59 | raise MailFailure('Error connecting to SMTP server.') 60 | 61 | msg = text.MIMEText(message) 62 | msg['Subject'] = subject 63 | msg['To'] = email.utils.formataddr((to_name, to)) 64 | msg['From'] = email.utils.formataddr((sender_name, sender)) 65 | 66 | try: 67 | if app.debug: 68 | server.set_debuglevel(True) 69 | server.sendmail(sender, [to], msg.as_string()) 70 | except (smtplib.SMTPException, socket.error) as ex: 71 | app.logger.error('Unable to send mail: %s', str(ex)) 72 | raise MailFailure('Error sending mail to SMTP server.') 73 | finally: 74 | try: 75 | server.quit() 76 | except smtplib.SMTPException: 77 | pass 78 | 79 | 80 | def _send_mailjet(message, subject, to, to_name, sender, sender_name): 81 | """Mailjet implementation of sending email.""" 82 | api_key = app.config.get('MJ_APIKEY_PUBLIC') 83 | api_secret = app.config.get('MJ_APIKEY_PRIVATE') 84 | if not api_key or not api_secret: 85 | app.logger.error('Missing MJ_APIKEY_PUBLIC/MJ_APIKEY_PRIVATE!') 86 | return 87 | # Note the data structures we use are api v3.1 88 | client = mailjet_rest.Client( 89 | auth=(api_key, api_secret), 90 | api_url='https://api.mailjet.com/', 91 | version='v3.1') 92 | from_obj = { 93 | "Email": sender, 94 | } 95 | if sender_name: 96 | from_obj["Name"] = sender_name 97 | to_obj = [{ 98 | "Email": to, 99 | }] 100 | if to_name: 101 | to_obj[0]["Name"] = to_name 102 | message = { 103 | "From": from_obj, 104 | "To": to_obj, 105 | "Subject": subject, 106 | "TextPart": message, 107 | } 108 | result = client.send.create(data={'Messages': [message]}) 109 | if result.status_code != 200: 110 | app.logger.error( 111 | 'Error sending via mailjet: (%d) %r', 112 | result.status_code, result.text) 113 | raise MailFailure('Error sending via mailjet!') 114 | try: 115 | j = result.json() 116 | except Exception: 117 | app.logger.error('Error sending via mailjet: %r', result.text) 118 | raise MailFailure('Error sending via mailjet!') 119 | if j['Messages'][0]['Status'] != 'success': 120 | app.logger.error('Error sending via mailjet: %r', j) 121 | raise MailFailure('Error sending via mailjet!') 122 | -------------------------------------------------------------------------------- /scoreboard/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import flask 16 | from flask import logging as flask_logging 17 | import logging 18 | import os 19 | from werkzeug import exceptions 20 | from werkzeug import utils as werkzeug_utils 21 | import flask_scss 22 | 23 | from scoreboard import logger 24 | 25 | # Singleton app instance 26 | _app_singleton = None 27 | 28 | 29 | def on_appengine(): 30 | """Returns true if we're running on AppEngine.""" 31 | runtime = os.environ.get('SERVER_SOFTWARE', '') 32 | gae_env = os.environ.get('GAE_ENV', '') 33 | return ((gae_env != '') or 34 | runtime.startswith('Development/') or 35 | runtime.startswith('Google App Engine/')) 36 | 37 | 38 | def create_app(config=None): 39 | app = flask.Flask( 40 | 'scoreboard', 41 | static_folder='../static', 42 | template_folder='../templates', 43 | ) 44 | app.config.from_object('scoreboard.config_defaults.Defaults') 45 | if config is not None: 46 | app.config.update(**config) 47 | 48 | if not on_appengine(): 49 | # Configure Scss to watch the files 50 | scss_compiler = flask_scss.Scss( 51 | app, static_dir='static/css', asset_dir='static/scss') 52 | scss_compiler.update_scss() 53 | 54 | for c in exceptions.default_exceptions.keys(): 55 | app.register_error_handler(c, api_error_handler) 56 | 57 | setup_logging(app) 58 | return app 59 | 60 | 61 | def load_config_file(app=None): 62 | app = app or get_app() 63 | try: 64 | app.config.from_object('config') 65 | except werkzeug_utils.ImportStringError: 66 | pass 67 | app.config.from_envvar('SCOREBOARD_CONFIG', silent=True) 68 | setup_logging(app) # reset logs 69 | 70 | 71 | def setup_logging(app): 72 | log_formatter = logger.Formatter( 73 | '%(asctime)s %(levelname)8s [%(filename)s:%(lineno)d] ' 74 | '%(client)s %(message)s') 75 | # log to files unless on AppEngine 76 | if not on_appengine(): 77 | # Main logger 78 | if not (app.debug or app.config.get('TESTING')): 79 | handler = logging.FileHandler( 80 | app.config.get('LOGFILE', '/tmp/scoreboard.wsgi.log')) 81 | handler.setLevel(logging.INFO) 82 | handler.setFormatter(log_formatter) 83 | app.logger.addHandler(handler) 84 | else: 85 | app.logger.handlers[0].setFormatter(log_formatter) 86 | 87 | # Challenge logger 88 | handler = logging.FileHandler( 89 | app.config.get('CHALLENGELOG', '/tmp/scoreboard.challenge.log')) 90 | handler.setLevel(logging.INFO) 91 | handler.setFormatter(logger.Formatter( 92 | '%(asctime)s %(client)s %(message)s')) 93 | local_logger = logging.getLogger('scoreboard') 94 | local_logger.addHandler(handler) 95 | app.challenge_log = local_logger 96 | else: 97 | app.challenge_log = app.logger 98 | try: 99 | import google.cloud.logging 100 | from google.cloud.logging import handlers 101 | client = google.cloud.logging.Client() 102 | client.setup_logging() 103 | handler = handlers.CloudLoggingHandler(client) 104 | app.logger.addHandler(handler) 105 | handler.setLevel(logging.INFO) 106 | return app 107 | except ImportError as ex: 108 | logging.error('Failed setting up logging: %s', ex) 109 | if not app.logger.handlers: 110 | app.logger.addHandler(flask_logging.default_handler) 111 | app.logger.handlers[0].setFormatter(log_formatter) 112 | logging.getLogger().handlers[0].setFormatter(log_formatter) 113 | 114 | return app 115 | 116 | 117 | def api_error_handler(ex): 118 | """Handle errors as appropriate depending on path.""" 119 | error_titles = { 120 | 401: 'Unauthorized', 121 | 403: 'Forbidden', 122 | 500: 'Internal Error', 123 | } 124 | try: 125 | status_code = ex.code 126 | except AttributeError: 127 | status_code = 500 128 | if flask.request.path.startswith('/api/'): 129 | app = get_app() 130 | app.logger.error(str(ex)) 131 | if app.config.get('DEBUG', False): 132 | resp = flask.jsonify(message=str(ex)) 133 | else: 134 | resp = flask.jsonify(message='Internal Server Error') 135 | resp.status_code = status_code 136 | return resp 137 | return flask.make_response( 138 | flask.render_template( 139 | 'error.html', exc=ex, 140 | title=error_titles.get(status_code, 'Error')), 141 | status_code) 142 | 143 | 144 | def get_app(): 145 | global _app_singleton 146 | if _app_singleton is None: 147 | _app_singleton = create_app() 148 | return _app_singleton 149 | -------------------------------------------------------------------------------- /scoreboard/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /scoreboard/tests/controllers_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from scoreboard.tests import base 16 | 17 | from scoreboard import controllers 18 | from scoreboard import errors 19 | 20 | 21 | class RegisterTest(base.BaseTestCase): 22 | """Test register_user controller.""" 23 | 24 | def testRegister_Normal(self): 25 | rv = controllers.register_user('foo@bar.com', 'foo', 'pass') 26 | self.assertIsNotNone(rv) 27 | 28 | def testRegister_BadEmail(self): 29 | """Test variations on bad emails.""" 30 | for email in ('', 'frob', '//', ''): 31 | with self.assertRaises(errors.ValidationError): 32 | controllers.register_user(email, 'foo', 'pass') 33 | 34 | def testRegister_DupeNick(self): 35 | self.app.config['TEAMS'] = False 36 | controllers.register_user('foo@bar.com', 'foo', 'pass') 37 | with self.assertRaises(errors.ValidationError): 38 | controllers.register_user('bar@bar.com', 'foo', 'pass') 39 | 40 | def testRegister_DupeTeam(self): 41 | self.app.config['TEAMS'] = True 42 | controllers.register_user( 43 | 'foo@bar.com', 'foo', 'pass', team_id='new', 44 | team_name='faketeam') 45 | with self.assertRaises(errors.ValidationError): 46 | controllers.register_user( 47 | 'bar@bar.com', 'foo', 'pass', team_id='new', 48 | team_name='faketeam') 49 | 50 | def testRegister_DupeEmail(self): 51 | self.app.config['TEAMS'] = False 52 | controllers.register_user('foo@bar.com', 'foo', 'pass') 53 | with self.assertRaises(errors.ValidationError): 54 | controllers.register_user('foo@bar.com', 'sam', 'pass') 55 | -------------------------------------------------------------------------------- /scoreboard/tests/data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import datetime 16 | import json 17 | import random 18 | 19 | from scoreboard import models 20 | 21 | 22 | def make_admin(): 23 | u = models.User.create('admin@example.com', 'admin', 'admin') 24 | u.promote() 25 | return u 26 | 27 | 28 | def make_teams(): 29 | teams = [] 30 | for name in ('QQQ', 'Light Cats', 'Siberian Nopsled', 'PPP', 'Raelly', 31 | 'Toast', 'csh', 'ByTeh', 'See Sure', 'Skinniest', '213374U'): 32 | teams.append(models.Team.create(name)) 33 | return teams 34 | 35 | 36 | def make_players(teams): 37 | players = [] 38 | for name in ('Ritam', 'Dr34dc0d3', 'alpha', 'beta', 'gamma', 'delta', 39 | 'Dade', 'Kate', 'zwad3', 'strikerkid', 'redpichu', 'n0pe', 40 | '0xcdb'): 41 | team = random.choice(teams) 42 | players.append(models.User.create( 43 | name.lower() + '@example.com', name, 'password', team=team)) 44 | return players 45 | 46 | 47 | def make_tags(): 48 | tags = [] 49 | for name in ('x86', 'x64', 'MIPS', 'RISC', 'Fun'): 50 | tags.append(models.Tag.create(name, 'Problems involving '+name)) 51 | return tags 52 | 53 | 54 | def make_challenges(tags): 55 | challs = [] 56 | chall_words = ( 57 | 'Magic', 'Grand', 'Fast', 'Hash', 'Table', 'Password', 58 | 'Crypto', 'Alpha', 'Beta', 'Win', 'Socket', 'Ball', 59 | 'Stego', 'Word', 'Gamma', 'Native', 'Mine', 'Dump', 60 | 'Tangled', 'Hackers', 'Book', 'Delta', 'Shadow', 61 | 'Lose', 'Draw', 'Long', 'Pointer', 'Free', 'Not', 62 | 'Only', 'Live', 'Secret', 'Agent', 'Hax0r', 'Whiskey', 63 | 'Tango', 'Foxtrot') 64 | for _ in range(25): 65 | title = random.sample(chall_words, 3) 66 | random.shuffle(title) 67 | title = ' '.join(title) 68 | flag = '_'.join(random.sample(chall_words, 4)).lower() 69 | # Choose a random subset of tags 70 | numtags = random.randint(0, len(tags)-1) 71 | local_tags = random.sample(tags, numtags) 72 | points = random.randint(1, 20) * 100 73 | desc = 'Flag: ' + flag 74 | ch = models.Challenge.create( 75 | title, desc, points, flag, 76 | unlocked=True) 77 | ch.add_tags(local_tags) 78 | if len(challs) % 8 == 7: 79 | ch.prerequisite = json.dumps( 80 | {'type': 'solved', 'challenge': challs[-1].cid}) 81 | # TODO: attachments 82 | challs.append(ch) 83 | models.commit() 84 | return challs 85 | 86 | 87 | def make_answers(teams, challs): 88 | for team in teams: 89 | times = sorted( 90 | [random.randint(0, 24*60) for _ in range(16)], 91 | reverse=True) 92 | for ch in random.sample(challs, random.randint(4, 16)): 93 | a = models.Answer.create(ch, team, '') 94 | ago = datetime.timedelta(minutes=times.pop(0)) 95 | a.timestamp = datetime.datetime.utcnow() - ago 96 | team.score += ch.points 97 | h = models.ScoreHistory() 98 | h.team = team 99 | h.score = team.score 100 | h.when = a.timestamp 101 | models.db.session.add(h) 102 | 103 | 104 | def create_all(): 105 | make_admin() 106 | 107 | # Teams and players 108 | teams = make_teams() 109 | make_players(teams) 110 | 111 | # Challenges 112 | tags = make_tags() 113 | models.commit() # Need IDs allocated 114 | challs = make_challenges(tags) 115 | 116 | # Submitted answers 117 | make_answers(teams, challs) 118 | models.commit() 119 | -------------------------------------------------------------------------------- /scoreboard/tests/utils_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from scoreboard.tests import base 16 | 17 | from scoreboard import utils 18 | 19 | 20 | class NormalizeInputTest(base.BaseTestCase): 21 | 22 | def testNormalizeInput(self): 23 | ni = utils.normalize_input # Shorthand 24 | self.assertEqual(ni("hello"), "hello") 25 | self.assertEqual(ni("Hello World"), "Hello World") 26 | self.assertEqual(ni(" foo "), "foo") 27 | 28 | 29 | class ProofOfWorkTest(base.BaseTestCase): 30 | 31 | def testValidateProofOfWork_Succeeds(self): 32 | val = "foo" 33 | key = "N77manQK9CvjvPRXB8U7ftJxys1d36xVfcBkGvM-jqM" 34 | nbits = 12 35 | self.assertTrue(utils.validate_proof_of_work(val, key, nbits)) 36 | 37 | def testValidateProofOfWork_SucceedsUnicode(self): 38 | val = u"foo" 39 | key = u"N77manQK9CvjvPRXB8U7ftJxys1d36xVfcBkGvM-jqM" 40 | nbits = 12 41 | self.assertTrue(utils.validate_proof_of_work(val, key, nbits)) 42 | 43 | def testValidateProofOfWork_FailsWrongVal(self): 44 | val = "bar" 45 | key = "N77manQK9CvjvPRXB8U7ftJxys1d36xVfcBkGvM-jqM" 46 | nbits = 12 47 | self.assertFalse(utils.validate_proof_of_work(val, key, nbits)) 48 | 49 | def testValidateProofOfWork_FailsWrongKey(self): 50 | val = "foo" 51 | key = "N77manQK9CvjvPRXB8U7ftJxys1d36xVfcBkGvM-jq" 52 | nbits = 12 53 | self.assertFalse(utils.validate_proof_of_work(val, key, nbits)) 54 | 55 | def testValidateProofOfWork_FailsMoreBits(self): 56 | val = "foo" 57 | key = "N77manQK9CvjvPRXB8U7ftJxys1d36xVfcBkGvM-jq" 58 | nbits = 16 59 | self.assertFalse(utils.validate_proof_of_work(val, key, nbits)) 60 | 61 | def testValidateProofOfWork_FailsInvalidBase64(self): 62 | val = "foo" 63 | key = "!!" 64 | nbits = 12 65 | self.assertFalse(utils.validate_proof_of_work(val, key, nbits)) 66 | -------------------------------------------------------------------------------- /scoreboard/tests/validators_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from scoreboard.tests import base 16 | 17 | from scoreboard import errors 18 | from scoreboard import models 19 | from scoreboard import validators 20 | 21 | 22 | class ChallengeStub(object): 23 | 24 | def __init__(self, answer, validator='static_pbkdf2'): 25 | self.answer_hash = answer 26 | self.validator = validator 27 | 28 | 29 | class StaticValidatorTest(base.BaseTestCase): 30 | 31 | def testStaticValidator(self): 32 | chall = ChallengeStub(None) 33 | validator = validators.GetValidatorForChallenge(chall) 34 | self.assertFalse(validator.validate_answer('fooabc', None)) 35 | validator.change_answer('fooabc') 36 | self.assertTrue(validator.validate_answer('fooabc', None)) 37 | self.assertFalse(validator.validate_answer('abcfoo', None)) 38 | 39 | 40 | class CaseStaticValidatorTest(base.BaseTestCase): 41 | 42 | def testCaseStaticValidator(self): 43 | chall = ChallengeStub(None, validator='static_pbkdf2_ci') 44 | validator = validators.GetValidatorForChallenge(chall) 45 | self.assertFalse(validator.validate_answer('foo', None)) 46 | validator.change_answer('FooBar') 47 | for test in ('FooBar', 'foobar', 'FOOBAR', 'fooBAR'): 48 | self.assertTrue( 49 | validator.validate_answer(test, None), 50 | msg='Case failed: {}'.format(test)) 51 | for test in ('barfoo', 'bar', 'foo', None): 52 | self.assertFalse( 53 | validator.validate_answer(test, None), 54 | msg='Case failed: {}'.format(test)) 55 | 56 | 57 | class RegexValidatorTest(base.BaseTestCase): 58 | 59 | def makeValidator(self, regex): 60 | """Construct a validator.""" 61 | chall = ChallengeStub(regex, validator='regex') 62 | return validators.GetValidatorForChallenge(chall) 63 | 64 | def testRegexWorks(self): 65 | v = self.makeValidator('[abc]+') 66 | self.assertTrue(v.validate_answer('aaa', None)) 67 | self.assertTrue(v.validate_answer('abc', None)) 68 | self.assertFalse(v.validate_answer('ddd', None)) 69 | self.assertFalse(v.validate_answer('aaad', None)) 70 | self.assertFalse(v.validate_answer('AAA', None)) 71 | 72 | def testRegexChangeWorks(self): 73 | v = self.makeValidator('[abc]+') 74 | self.assertTrue(v.validate_answer('a', None)) 75 | self.assertFalse(v.validate_answer('foo', None)) 76 | v.change_answer('fo+') 77 | self.assertTrue(v.validate_answer('foo', None)) 78 | self.assertFalse(v.validate_answer('a', None)) 79 | 80 | 81 | class RegexCaseValidatorTest(base.BaseTestCase): 82 | 83 | def makeValidator(self, regex): 84 | """Construct a validator.""" 85 | chall = ChallengeStub(regex, validator='regex_ci') 86 | return validators.GetValidatorForChallenge(chall) 87 | 88 | def testRegexWorks(self): 89 | v = self.makeValidator('[abc]+') 90 | self.assertTrue(v.validate_answer('aaa', None)) 91 | self.assertTrue(v.validate_answer('abc', None)) 92 | self.assertFalse(v.validate_answer('ddd', None)) 93 | self.assertFalse(v.validate_answer('aaad', None)) 94 | self.assertTrue(v.validate_answer('AAA', None)) 95 | 96 | def testRegexChangeWorks(self): 97 | v = self.makeValidator('[abc]+') 98 | self.assertTrue(v.validate_answer('a', None)) 99 | self.assertFalse(v.validate_answer('foo', None)) 100 | v.change_answer('fo+') 101 | self.assertTrue(v.validate_answer('Foo', None)) 102 | self.assertFalse(v.validate_answer('a', None)) 103 | 104 | 105 | class NonceValidatorTest(base.BaseTestCase): 106 | 107 | def setUp(self): 108 | super(NonceValidatorTest, self).setUp() 109 | self.chall = models.Challenge.create( 110 | 'foo', 'bar', 100, '', unlocked=True, 111 | validator='nonce_166432') 112 | self.validator = validators.GetValidatorForChallenge(self.chall) 113 | self.validator.change_answer('secret123') 114 | self.team = models.Team.create('footeam') 115 | models.commit() 116 | 117 | def testNonceValidator_Basic(self): 118 | answer = self.validator.make_answer(1) 119 | self.assertTrue(self.validator.validate_answer(answer, self.team)) 120 | 121 | def testNonceValidator_Dupe(self): 122 | answer = self.validator.make_answer(5) 123 | self.assertTrue(self.validator.validate_answer(answer, self.team)) 124 | models.commit() 125 | self.assertTrue(self.validator.validate_answer(answer, self.team)) 126 | self.assertRaises(errors.IntegrityError, models.commit) 127 | -------------------------------------------------------------------------------- /scoreboard/validators/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from . import static_pbkdf2 17 | from . import per_team 18 | from . import nonce 19 | from . import regex 20 | 21 | _Validators = { 22 | 'static_pbkdf2': static_pbkdf2.StaticPBKDF2Validator, 23 | 'static_pbkdf2_ci': static_pbkdf2.CaseStaticPBKDF2Validator, 24 | 'per_team': per_team.PerTeamValidator, 25 | 'nonce_166432': nonce.Nonce_16_64_Base32_Validator, 26 | 'nonce_245632': nonce.Nonce_24_56_Base32_Validator, 27 | 'nonce_328832': nonce.Nonce_32_88_Base32_Validator, 28 | 'regex': regex.RegexValidator, 29 | 'regex_ci': regex.RegexCaseValidator, 30 | } 31 | 32 | 33 | def GetDefaultValidator(): 34 | return 'static_pbkdf2' 35 | 36 | 37 | def GetValidatorForChallenge(challenge): 38 | cls = _Validators[challenge.validator] 39 | return cls(challenge) 40 | 41 | 42 | def ValidatorNames(): 43 | return {k: getattr(v, 'name', k) for k, v in _Validators.items()} 44 | 45 | 46 | def ValidatorMeta(): 47 | meta = {} 48 | for k, v in _Validators.items(): 49 | meta[k] = { 50 | 'name': v.name, 51 | 'per_team': v.per_team, 52 | 'flag_gen': v.flag_gen, 53 | } 54 | return meta 55 | 56 | 57 | def IsValidator(name): 58 | return name in _Validators 59 | 60 | 61 | __all__ = [GetValidatorForChallenge, ValidatorNames] 62 | -------------------------------------------------------------------------------- /scoreboard/validators/base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | class BaseValidator(object): 17 | 18 | # Can we generate these flags? 19 | flag_gen = False 20 | # Is this flag per team? 21 | per_team = False 22 | 23 | def __init__(self, challenge): 24 | self.challenge = challenge 25 | 26 | def validate_answer(self, answer, team): 27 | """Validate the answer for the team.""" 28 | raise NotImplementedError( 29 | '%s does not implement validate_answer.' % 30 | type(self).__name__) 31 | 32 | def change_answer(self, answer): 33 | """Change the answer for the challenge.""" 34 | self.challenge.answer_hash = answer 35 | -------------------------------------------------------------------------------- /scoreboard/validators/nonce.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import division 16 | 17 | import base64 18 | import hashlib 19 | import hmac 20 | import struct 21 | 22 | from scoreboard import main 23 | from scoreboard import utils 24 | from scoreboard import models 25 | 26 | from . import base 27 | 28 | app = main.get_app() 29 | 30 | 31 | class BaseNonceValidator(base.BaseValidator): 32 | 33 | # Bits to use for each of the nonce and authenticator 34 | NONCE_BITS = 0 35 | AUTHENTICATOR_BITS = 0 36 | HASH = hashlib.sha256 37 | 38 | def __init__(self, *args, **kwargs): 39 | super(BaseNonceValidator, self).__init__(*args, **kwargs) 40 | if not self.NONCE_BITS or self.NONCE_BITS % 8: 41 | raise ValueError('NONCE_BITS must be non-0 and a multiple of 8.') 42 | if not self.AUTHENTICATOR_BITS or self.AUTHENTICATOR_BITS % 8: 43 | raise ValueError( 44 | 'AUTHENTICATOR_BITS must be non-0 and a multiple of 8.') 45 | 46 | @staticmethod 47 | def _decode(buf): 48 | raise NotImplementedError('Must implement decode.') 49 | 50 | @staticmethod 51 | def _encode(buf): 52 | raise NotImplementedError('Must implement encode.') 53 | 54 | def validate_answer(self, answer, team): 55 | """Validate the nonce-based flag.""" 56 | try: 57 | decoded_answer = self._decode(answer) 58 | except TypeError: 59 | app.logger.error('Invalid padding for answer.') 60 | return False 61 | if len(decoded_answer) != ( 62 | self.NONCE_BITS + self.AUTHENTICATOR_BITS) // 8: 63 | app.logger.error('Invalid length of decoded answer in %s', 64 | type(self).__name__) 65 | return False 66 | nonce = decoded_answer[:self.NONCE_BITS//8] 67 | authenticator = decoded_answer[self.NONCE_BITS//8:] 68 | if not utils.compare_digest(authenticator, 69 | self.compute_authenticator(nonce)): 70 | app.logger.error('Invalid nonce flag: %s', answer) 71 | return False 72 | # At this point, it's a valid flag, but need to check for reuse. 73 | # We do this by inserting and primary key checks will fail in the 74 | # commit phase. 75 | if team: 76 | models.NonceFlagUsed.create( 77 | self.challenge, self.unpack_nonce(nonce), 78 | team) 79 | return True 80 | 81 | def compute_authenticator(self, nonce): 82 | """Compute the authenticator part for a nonce.""" 83 | mac = hmac.new( 84 | self.challenge.answer_hash.encode('utf-8'), 85 | nonce, 86 | digestmod=self.HASH).digest() 87 | return mac[:self.AUTHENTICATOR_BITS//8] 88 | 89 | def make_answer(self, nonce): 90 | """Compute the whole answer for a nonce.""" 91 | if isinstance(nonce, int): 92 | nonce = struct.pack('>Q', nonce) 93 | nonce = nonce[8 - (self.NONCE_BITS // 8):] 94 | if len(nonce) != self.NONCE_BITS // 8: 95 | raise ValueError('nonce is wrong length!') 96 | return self._encode(nonce + self.compute_authenticator(nonce)) 97 | 98 | @classmethod 99 | def unpack_nonce(cls, nonce): 100 | pad = b'\x00' * (8 - cls.NONCE_BITS // 8) 101 | return struct.unpack('>Q', pad + nonce)[0] 102 | 103 | 104 | class Base32Validator(BaseNonceValidator): 105 | 106 | def __init__(self, *args, **kwargs): 107 | if (self.NONCE_BITS + self.AUTHENTICATOR_BITS) % 5 != 0: 108 | raise ValueError('Length must be a mulitple of 5 bits.') 109 | super(Base32Validator, self).__init__(*args, **kwargs) 110 | 111 | @staticmethod 112 | def _encode(buf): 113 | return base64.b32encode(buf) 114 | 115 | @staticmethod 116 | def _decode(buf): 117 | buf = utils.to_bytes(buf) 118 | return base64.b32decode(buf, casefold=True, map01='I') 119 | 120 | 121 | class Nonce_16_64_Base32_Validator(Base32Validator): 122 | 123 | name = 'Nonce: 16 bits, 64 bit validator, Base32 encoded' 124 | NONCE_BITS = 16 125 | AUTHENTICATOR_BITS = 64 126 | 127 | 128 | class Nonce_24_56_Base32_Validator(Base32Validator): 129 | 130 | name = 'Nonce: 24 bits, 56 bit validator, Base32 encoded' 131 | NONCE_BITS = 24 132 | AUTHENTICATOR_BITS = 56 133 | 134 | 135 | class Nonce_32_88_Base32_Validator(Base32Validator): 136 | 137 | name = 'Nonce: 32 bits, 88 bit validator, Base32 encoded' 138 | NONCE_BITS = 32 139 | AUTHENTICATOR_BITS = 88 140 | -------------------------------------------------------------------------------- /scoreboard/validators/per_team.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import hashlib 16 | import hmac 17 | 18 | from scoreboard import utils 19 | from scoreboard.validators import base 20 | 21 | 22 | class PerTeamValidator(base.BaseValidator): 23 | """Creates a flag that's per-team.""" 24 | 25 | name = 'Per-Team' 26 | flag_gen = True 27 | per_team = True 28 | 29 | def validate_answer(self, answer, team): 30 | if not team: 31 | return False 32 | return utils.compare_digest( 33 | self.construct_mac(team), 34 | answer) 35 | 36 | def construct_mac(self, team): 37 | if not isinstance(team, str): 38 | if not isinstance(team, int): 39 | team = team.tid 40 | team = str(team) 41 | mac = hmac.new( 42 | self.challenge.answer_hash, 43 | team, 44 | digestmod=hashlib.sha1) 45 | return mac.hexdigest() 46 | -------------------------------------------------------------------------------- /scoreboard/validators/regex.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | 17 | from scoreboard.validators import base 18 | 19 | 20 | class RegexValidator(base.BaseValidator): 21 | """Regex-based validator. 22 | 23 | Note that validation based on a regex is inherently subject to timing 24 | attacks. If this is important to you, you should use a validator like 25 | static_pbkdf2. 26 | """ 27 | 28 | name = 'Regular Expression' 29 | re_flags = 0 30 | 31 | def validate_answer(self, answer, unused_team): 32 | m = re.match(self.challenge.answer_hash, answer, flags=self.re_flags) 33 | if m: 34 | return m.group(0) == answer 35 | return False 36 | 37 | 38 | class RegexCaseValidator(RegexValidator): 39 | """Case-insensitive regex match.""" 40 | 41 | name = 'Regular Expression (Case Insensitive)' 42 | re_flags = re.IGNORECASE 43 | -------------------------------------------------------------------------------- /scoreboard/validators/static_pbkdf2.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pbkdf2 16 | 17 | from scoreboard import utils 18 | from scoreboard.validators import base 19 | 20 | 21 | class StaticPBKDF2Validator(base.BaseValidator): 22 | """PBKDF2-based secrets, everyone gets the same flag.""" 23 | 24 | name = 'Static' 25 | 26 | def validate_answer(self, answer, unused_team): 27 | if not self.challenge.answer_hash: 28 | return False 29 | return utils.compare_digest( 30 | pbkdf2.crypt(answer, self.challenge.answer_hash), 31 | self.challenge.answer_hash) 32 | 33 | def change_answer(self, answer): 34 | self.challenge.answer_hash = pbkdf2.crypt(answer) 35 | 36 | 37 | class CaseStaticPBKDF2Validator(StaticPBKDF2Validator): 38 | """PBKDF2-based secrets, case insensitive.""" 39 | 40 | name = 'Static (Case Insensitive)' 41 | 42 | def validate_answer(self, answer, team): 43 | if not isinstance(answer, str): 44 | return False 45 | return super(CaseStaticPBKDF2Validator, self).validate_answer( 46 | answer.lower(), team) 47 | 48 | def change_answer(self, answer): 49 | return super(CaseStaticPBKDF2Validator, self).change_answer( 50 | answer.lower()) 51 | -------------------------------------------------------------------------------- /scoreboard/views.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import flask 16 | import os 17 | from werkzeug import exceptions 18 | 19 | from scoreboard import attachments 20 | from scoreboard import main 21 | from scoreboard import models 22 | 23 | app = main.get_app() 24 | 25 | _VIEW_CACHE = {} 26 | 27 | 28 | @app.errorhandler(404) 29 | def handle_404(ex): 30 | """Handle 404s, sending index.html for unhandled paths.""" 31 | path = flask.request.path[1:] 32 | try: 33 | return app.send_static_file(path) 34 | except (exceptions.NotFound, UnicodeEncodeError): 35 | if '.' not in path and not path.startswith('api/'): 36 | app.logger.info('%s -> index.html', path) 37 | return render_index() 38 | return '404 Not Found', 404 39 | 40 | 41 | # Needed because emails with a "." in them prevent 404 handler from working 42 | @app.route('/pwreset/') 43 | def render_pwreset(unused): 44 | return render_index() 45 | 46 | 47 | @app.route('/') 48 | @app.route('/index.html') 49 | def render_index(): 50 | """Render index. 51 | 52 | Do not include any user-controlled content to avoid XSS! 53 | """ 54 | try: 55 | tmpl = _VIEW_CACHE['index'] 56 | except KeyError: 57 | minify = not app.debug and os.path.exists( 58 | os.path.join(app.static_folder, 'js/app.min.js')) 59 | tmpl = flask.render_template('index.html', minify=minify) 60 | _VIEW_CACHE['index'] = tmpl 61 | resp = flask.make_response(tmpl, 200) 62 | if flask.request.path.startswith('/scoreboard'): 63 | resp.headers.add('X-FRAME-OPTIONS', 'ALLOW') 64 | return resp 65 | 66 | 67 | @app.route('/attachment/') 68 | def download(filename): 69 | """Download an attachment.""" 70 | 71 | attachment = models.Attachment.query.get_or_404(filename) 72 | cuser = models.User.current() 73 | valid = cuser and cuser.admin 74 | for ch in attachment.challenges: 75 | if ch.unlocked: 76 | valid = True 77 | break 78 | if not valid: 79 | flask.abort(404) 80 | app.logger.info('Download of %s by %r.', attachment, cuser or "Anonymous") 81 | 82 | return attachments.backend.send(attachment) 83 | 84 | 85 | @app.route('/createdb') 86 | def createdb(): 87 | """Create database schema without CLI access. 88 | 89 | Useful for AppEngine and other container environments. 90 | Should be safe to be exposed, as operation is idempotent and does not 91 | clear any data. 92 | """ 93 | try: 94 | models.db.create_all() 95 | return 'Tables created.' 96 | except Exception as ex: 97 | app.logger.exception('Failed creating tables: %s', str(ex)) 98 | return 'Failed creating tables: see log.' 99 | -------------------------------------------------------------------------------- /scoreboard/wsgi.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Google LLC. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from scoreboard import main 16 | 17 | app = main.get_app() 18 | main.load_config_file(app) 19 | 20 | # These must be after config loading 21 | from scoreboard import rest # noqa: E402 22 | from scoreboard import views # noqa: E402 23 | 24 | # Used here to catch accidental removal 25 | _modules_for_views = (rest, views) 26 | -------------------------------------------------------------------------------- /static/css/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/ctfscoreboard/28a8f6c30e401e07031741d5bafea3003e2d100e/static/css/.keep -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/ctfscoreboard/28a8f6c30e401e07031741d5bafea3003e2d100e/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/ctfscoreboard/28a8f6c30e401e07031741d5bafea3003e2d100e/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/ctfscoreboard/28a8f6c30e401e07031741d5bafea3003e2d100e/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var scoreboardApp = angular.module('scoreboardApp', [ 18 | 'ngRoute', 19 | 'ngSanitize', 20 | 'adminChallengeCtrls', 21 | 'adminNewsCtrls', 22 | 'adminPageCtrls', 23 | 'adminTeamCtrls', 24 | 'adminToolCtrls', 25 | 'challengeCtrls', 26 | 'globalCtrls', 27 | 'pageCtrls', 28 | 'regCtrls', 29 | 'scoreboardCtrls', 30 | 'teamCtrls', 31 | 'sbDirectives', 32 | 'sbFilters' 33 | ]); 34 | 35 | scoreboardApp.config([ 36 | '$routeProvider', 37 | '$locationProvider', 38 | function($routeProvider, $locationProvider) { 39 | $locationProvider.html5Mode(true); 40 | $routeProvider. 41 | when('/', { 42 | templateUrl: '/partials/page.html', 43 | controller: 'StaticPageCtrl' 44 | }). 45 | when('/login', { 46 | templateUrl: '/partials/login.html', 47 | controller: 'LoginCtrl' 48 | }). 49 | when('/logout', { 50 | templateUrl: '/partials/login.html', 51 | controller: 'LoginCtrl' 52 | }). 53 | when('/register', { 54 | templateUrl: '/partials/register.html', 55 | controller: 'RegistrationCtrl' 56 | }). 57 | when('/profile', { 58 | templateUrl: '/partials/profile.html', 59 | controller: 'ProfileCtrl' 60 | }). 61 | when('/challenges/', { 62 | templateUrl: '/partials/challenge_grid.html', 63 | controller: 'ChallengeGridCtrl' 64 | }). 65 | when('/scoreboard', { 66 | templateUrl: '/partials/scoreboard.html', 67 | controller: 'ScoreboardCtrl' 68 | }). 69 | when('/teams/:tid', { 70 | templateUrl: '/partials/team.html', 71 | controller: 'TeamPageCtrl' 72 | }). 73 | when('/pwreset/:email/:token', { 74 | templateUrl: '/partials/pwreset.html', 75 | controller: 'PasswordResetCtrl' 76 | }). 77 | when('/admin/tags', { 78 | templateUrl: '/partials/admin/tags.html', 79 | controller: 'AdminTagCtrl' 80 | }). 81 | when('/admin/attachments', { 82 | templateUrl: '/partials/admin/attachments.html', 83 | controller: 'AdminAttachmentCtrl' 84 | }). 85 | when('/admin/challenges/:cid?', { 86 | templateUrl: '/partials/admin/challenges.html', 87 | controller: 'AdminChallengesCtrl' 88 | }). 89 | when('/admin/challenge/:cid?', { 90 | templateUrl: '/partials/admin/challenge.html', 91 | controller: 'AdminChallengeCtrl' 92 | }). 93 | when('/admin/backups', { 94 | templateUrl: '/partials/admin/restore.html', 95 | controller: 'AdminRestoreCtrl' 96 | }). 97 | when('/admin/teams/:tid?', { 98 | templateUrl: '/partials/admin/teams.html', 99 | controller: 'AdminTeamsCtrl' 100 | }). 101 | when('/admin/users/:uid?', { 102 | templateUrl: '/partials/admin/users.html', 103 | controller: 'AdminUsersCtrl' 104 | }). 105 | when('/admin/news', { 106 | templateUrl: '/partials/admin/news.html', 107 | controller: 'AdminNewsCtrl' 108 | }). 109 | when('/admin/page/:path', { 110 | templateUrl: '/partials/admin/page.html', 111 | controller: 'AdminPageCtrl' 112 | }). 113 | when('/admin/pages', { 114 | templateUrl: '/partials/admin/pages.html', 115 | controller: 'AdminPagesCtrl' 116 | }). 117 | when('/admin/tools', { 118 | templateUrl: '/partials/admin/tools.html', 119 | controller: 'AdminToolCtrl' 120 | }). 121 | otherwise({ 122 | templateUrl: '/partials/page.html', 123 | controller: 'StaticPageCtrl' 124 | }); 125 | }]); 126 | 127 | 128 | scoreboardApp.run([ 129 | '$rootScope', 130 | 'loadingService', 131 | function($rootScope, loadingService) { 132 | $rootScope.$on('$locationChangeStart', function() { 133 | loadingService.start(); 134 | }); 135 | }]); 136 | 137 | var getInjector = function() { 138 | return angular.element('*[ng-app]').injector(); 139 | }; 140 | -------------------------------------------------------------------------------- /static/js/controllers/admin/news.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var adminNewsCtrls = angular.module('adminNewsCtrls', [ 18 | 'globalServices', 19 | 'sessionServices', 20 | 'teamServices', 21 | ]); 22 | 23 | adminNewsCtrls.controller('AdminNewsCtrl', [ 24 | '$scope', 25 | 'errorService', 26 | 'newsService', 27 | 'sessionService', 28 | 'teamService', 29 | 'loadingService', 30 | function($scope, errorService, newsService, sessionService, teamService, 31 | loadingService) { 32 | if (!sessionService.requireAdmin()) return; 33 | 34 | var makeNewsItem = function() { 35 | return { 36 | 'news_type': 'Broadcast' 37 | } 38 | }; 39 | $scope.newsItem = makeNewsItem(); 40 | $scope.teams = teamService.get(); 41 | $scope.submitNews = function() { 42 | errorService.clearErrors(); 43 | newsService.save($scope.newsItem, 44 | function() { 45 | $scope.newsItem = makeNewsItem(); 46 | newsService.poll(); 47 | errorService.success('News item saved.'); 48 | }, 49 | function(msg) { 50 | errorService.error(msg); 51 | }); 52 | }; 53 | loadingService.stop(); 54 | }]); 55 | -------------------------------------------------------------------------------- /static/js/controllers/admin/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var adminPageCtrls = angular.module('adminPageCtrls', [ 18 | 'globalServices', 19 | 'sessionServices', 20 | 'pageServices', 21 | 'ngRoute', 22 | ]); 23 | 24 | adminPageCtrls.controller('AdminPageCtrl', [ 25 | '$scope', 26 | '$routeParams', 27 | 'errorService', 28 | 'pageService', 29 | 'sessionService', 30 | 'loadingService', 31 | function($scope, $routeParams, errorService, pageService, sessionService, 32 | loadingService) { 33 | if (!sessionService.requireAdmin()) return; 34 | 35 | var path = $routeParams.path; 36 | 37 | $scope.action = 'New Page: ' + path; 38 | 39 | var goEdit = function() { 40 | $scope.action = 'Edit Page: ' + path; 41 | }; 42 | 43 | $scope.save = function() { 44 | errorService.clearErrors(); 45 | pageService.save({path: path}, $scope.page, 46 | function(data) { 47 | $scope.page = data; 48 | errorService.success('Saved.'); 49 | goEdit(); 50 | }, 51 | function(data) { 52 | errorService.error(data); 53 | }); 54 | }; 55 | 56 | $scope.page = {path: path}; 57 | 58 | pageService.get({path: path}, 59 | function(data) { 60 | goEdit(); 61 | $scope.page = data; 62 | loadingService.stop(); 63 | }, 64 | function(data) { 65 | loadingService.stop(); 66 | if (data.status == 404) 67 | // Don't care, creating a new page? 68 | return; 69 | errorService.error(data); 70 | }); 71 | }]); 72 | -------------------------------------------------------------------------------- /static/js/controllers/admin/teams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var adminTeamCtrls = angular.module('adminTeamCtrls', [ 18 | 'ngResource', 19 | 'ngRoute', 20 | 'challengeServices', 21 | 'globalServices', 22 | 'sessionServices', 23 | 'teamServices', 24 | 'userServices' 25 | ]); 26 | 27 | adminTeamCtrls.controller('AdminTeamsCtrl', [ 28 | '$scope', 29 | '$routeParams', 30 | 'answerService', 31 | 'challengeService', 32 | 'errorService', 33 | 'sessionService', 34 | 'teamService', 35 | 'loadingService', 36 | function($scope, $routeParams, answerService, challengeService, errorService, 37 | sessionService, teamService, loadingService) { 38 | if (!sessionService.requireAdmin()) return; 39 | 40 | $scope.teams = []; 41 | $scope.team = null; 42 | $scope.unsolved = []; 43 | $scope.grantee = null; 44 | 45 | $scope.updateTeamModal = function() { 46 | $("#team-rename").modal("show"); 47 | }; 48 | 49 | $scope.updateTeam = function() { 50 | errorService.clearErrors(); 51 | $scope.team.$save({tid: $scope.team.tid}, 52 | function(data) { 53 | $scope.team = data; 54 | errorService.error('Saved.', 'success'); 55 | }, 56 | function(data) { 57 | errorService.error(data); 58 | }); 59 | }; 60 | 61 | $scope.grantFlag = function(chall) { 62 | $scope.grantee = chall; 63 | $("#team-grant").modal("show"); 64 | }; 65 | 66 | $scope.grantFlagConfirm = function() { 67 | answerService.create( 68 | { cid: $scope.grantee.cid, 69 | tid: $scope.team.tid }, 70 | function() { 71 | errorService.error('Flag granted.', 'success'); 72 | refreshTeam($scope.team.tid); 73 | }, 74 | errorService.error); 75 | }; 76 | 77 | var refreshTeam = function(tid) { 78 | $scope.team = teamService.get({tid: tid}, 79 | teamLoaded, 80 | function(data) { 81 | errorService.error(data); 82 | loadingService.stop(); 83 | }); 84 | }; 85 | 86 | var teamLoaded = function() { 87 | var tagData = {}; 88 | var solved = []; 89 | angular.forEach($scope.team.solved_challenges, function(chall) { 90 | solved.push(chall.cid); 91 | angular.forEach(chall.tags, function(tag) { 92 | if (!(tag.tagslug in tagData)) 93 | tagData[tag.tagslug] = chall.points; 94 | else 95 | tagData[tag.tagslug] += chall.points; 96 | }); 97 | }); 98 | $scope.tagData = tagData; 99 | $scope.scoreHistory = {}; 100 | $scope.scoreHistory[$scope.team.name] = $scope.team.score_history; 101 | $scope.unsolved = []; 102 | 103 | challengeService.get(function(challs) { 104 | angular.forEach(challs.challenges, function(ch) { 105 | if (solved.indexOf(ch.cid) < 0) { 106 | $scope.unsolved.push(ch); 107 | } 108 | }); 109 | loadingService.stop(); 110 | }); 111 | }; 112 | 113 | sessionService.requireLogin(function() { 114 | var tid = $routeParams.tid; 115 | if (tid) { 116 | refreshTeam(tid); 117 | } else { 118 | teamService.get(function(data) { 119 | $scope.teams = data.teams; 120 | loadingService.stop(); 121 | }); 122 | } 123 | }); 124 | }]); 125 | 126 | adminTeamCtrls.controller('AdminUsersCtrl', [ 127 | '$scope', 128 | '$routeParams', 129 | 'configService', 130 | 'errorService', 131 | 'sessionService', 132 | 'teamService', 133 | 'userService', 134 | 'loadingService', 135 | function($scope, $routeParams, configService, errorService, sessionService, 136 | teamService, userService, loadingService) { 137 | if (!sessionService.requireAdmin()) return; 138 | 139 | $scope.users = []; 140 | $scope.teams = []; 141 | $scope.user = null; 142 | $scope.config = configService.get(); 143 | 144 | $scope.updateUser = function() { 145 | errorService.clearErrors(); 146 | $scope.user.$save({uid: $scope.user.uid}, 147 | function(data) { 148 | $scope.user = data; 149 | errorService.error('Saved.', 'success'); 150 | }, 151 | function(data) { 152 | errorService.error(data); 153 | }); 154 | }; 155 | 156 | var getTeam = function(tid) { 157 | var team = null; 158 | angular.forEach($scope.teams, function(t) { 159 | if (t.tid == tid) { 160 | team = t; 161 | } 162 | }); 163 | return team; 164 | }; 165 | 166 | sessionService.requireLogin(function() { 167 | teamService.get(function(data) { 168 | $scope.teams = data.teams; 169 | var uid = $routeParams.uid; 170 | if (uid) { 171 | $scope.user = userService.get({uid: uid}, 172 | function() { 173 | loadingService.stop(); 174 | $scope.user.team = getTeam($scope.user.team_tid); 175 | }); 176 | } else { 177 | userService.get(function(data) { 178 | $scope.users = data.users; 179 | angular.forEach($scope.users, function(u) { 180 | u.team = getTeam(u.team_tid); 181 | }); 182 | loadingService.stop(); 183 | }); 184 | } 185 | }); 186 | }); // end requireLogin block 187 | }]); 188 | -------------------------------------------------------------------------------- /static/js/controllers/admin/tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var adminToolCtrls = angular.module('adminToolCtrls', [ 18 | 'adminServices', 19 | 'sessionServices', 20 | 'userServices', 21 | ]); 22 | 23 | adminToolCtrls.controller('AdminToolCtrl', [ 24 | '$scope', 25 | 'adminToolsService', 26 | 'errorService', 27 | 'sessionService', 28 | 'loadingService', 29 | 'apiKeyService', 30 | function($scope, adminToolsService, errorService, sessionService, loadingService, apiKeyService) { 31 | if (!sessionService.requireAdmin()) return; 32 | 33 | $scope.recalculateScores = adminToolsService.recalculateScores; 34 | $scope.resetScores = function() { 35 | adminToolsService.resetScores( 36 | errorService.success, errorService.error); 37 | }; 38 | $scope.resetPlayers = function() { 39 | adminToolsService.resetPlayers( 40 | errorService.success, errorService.error); 41 | }; 42 | $scope.clearApiKeys = function() { 43 | apiKeyService.deleteAll(function() { 44 | errorService.success("Cleared"); 45 | }, function() { 46 | errorService.error("Failed clearing API keys."); 47 | }); 48 | }; 49 | 50 | loadingService.stop(); 51 | }]); 52 | -------------------------------------------------------------------------------- /static/js/controllers/challenges.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var challengeCtrls = angular.module('challengeCtrls', [ 18 | 'ngResource', 19 | 'ngRoute', 20 | 'challengeServices', 21 | 'globalServices', 22 | ]); 23 | 24 | challengeCtrls.controller('ChallengeGridCtrl', [ 25 | '$rootScope', 26 | '$scope', 27 | '$location', 28 | 'challengeService', 29 | 'configService', 30 | 'loadingService', 31 | 'scoreService', 32 | 'tagService', 33 | function($rootScope, $scope, $location, challengeService, configService, 34 | loadingService, scoreService, tagService) { 35 | $scope.currChall = null; 36 | $scope.shownTags = {}; 37 | $scope.config = configService.get(); 38 | $scope.challenges = []; 39 | 40 | var compareChallenges = function(a, b) { 41 | return (a.weight - b.weight); 42 | }; 43 | 44 | var refresh = function(cb) { 45 | console.log('Refresh grid.'); 46 | challengeService.get(function(data) { 47 | data.challenges.sort(compareChallenges); 48 | $scope.challenges = data.challenges; 49 | if (cb !== undefined && cb !== null) { 50 | cb(); 51 | } 52 | }); 53 | }; 54 | 55 | tagService.getList(function(tags) { 56 | $scope.allTags = tags.tags; 57 | for (var i = 0; i < $scope.allTags.length; i++) { 58 | $scope.shownTags[$scope.allTags[i].tagslug] = 1; 59 | } 60 | }) 61 | 62 | $scope.goChallenge = function(chall) { 63 | $scope.currChall = chall; 64 | $('#challenge-modal').modal('show'); 65 | }; 66 | 67 | $scope.flipSide = function(chall) { 68 | if (chall.answered) 69 | return "Solved! (" + scoreService.getCurrentPoints(chall) + " points)"; 70 | else 71 | return scoreService.getCurrentPoints(chall) + " points"; 72 | }; 73 | 74 | $scope.tagsAllowed = function(chall) { 75 | var containsTag = function(chall, tagslug) { 76 | for (var i = 0; i < chall.tags.length; i++) { 77 | if (chall.tags[i].tagslug == tagslug) return true; 78 | } 79 | return false; 80 | } 81 | 82 | //Check for prohibition 83 | for (var i = 0; i < chall.tags.length; i++) { 84 | var type = $scope.shownTags[chall.tags[i].tagslug]; 85 | if (type == 0) { 86 | return false; 87 | } 88 | } 89 | 90 | //Check for inclusion 91 | for (var i in $scope.shownTags) { 92 | if ($scope.shownTags[i] == 2 && !containsTag(chall, i)) { 93 | return false; 94 | } 95 | } 96 | return true; 97 | } 98 | 99 | $scope.toggleTag = function(t, click) { 100 | var tindex = $scope.shownTags[t]; 101 | //Return next permutation 102 | if (click == 0) { 103 | tindex += 1; 104 | } else { 105 | tindex += 3-1; 106 | } 107 | $scope.shownTags[t] = tindex % 3; 108 | } 109 | 110 | $scope.getSentiment = function(tag) { 111 | var sentiments = [ 112 | 'sentiment_dissatisfied', 113 | 'sentiment_neutral', 114 | 'sentiment_satisfied']; 115 | return sentiments[$scope.shownTags[tag.tagslug]]; 116 | } 117 | 118 | refresh(loadingService.stop); 119 | 120 | $rootScope.$on('correctAnswer', (e) => refresh()); 121 | }]); 122 | -------------------------------------------------------------------------------- /static/js/controllers/global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var globalCtrls = angular.module('globalCtrls', [ 18 | 'globalServices', 19 | 'sessionServices', 20 | ]); 21 | 22 | globalCtrls.controller('GlobalCtrl', [ 23 | '$scope', 24 | 'configService', 25 | function($scope, configService) { 26 | $scope.config = configService.get(); 27 | }]); 28 | 29 | globalCtrls.controller('LoggedInCtrl', [ 30 | '$scope', 31 | 'sessionService', 32 | function($scope, sessionService) { 33 | $scope.session = sessionService.session; 34 | $scope.loggedIn = function(){ 35 | return !!sessionService.session.user; 36 | }; 37 | $scope.isAdmin = function(){ 38 | return (!!sessionService.session.user && 39 | sessionService.session.user.admin); 40 | }; 41 | }]); 42 | 43 | globalCtrls.controller('ErrorCtrl', [ 44 | '$scope', 45 | 'errorService', 46 | function($scope, errorService) { 47 | $scope.errors = errorService.errors; 48 | 49 | $scope.$on('$locationChangeStart', function(ev) { 50 | errorService.clearErrors(); 51 | }); 52 | }]); 53 | 54 | globalCtrls.controller('NewsCtrl', [ 55 | '$scope', 56 | 'newsService', 57 | function($scope, newsService) { 58 | $scope.latest = 0; 59 | var updateNews = function(newsItems) { 60 | var latest = 0; 61 | angular.forEach(newsItems, function(item) { 62 | var d = Date.parse(item.timestamp); 63 | if (d > latest) 64 | latest = d; 65 | }); 66 | if (latest > $scope.latest) { 67 | // TODO: call attention to new news 68 | $scope.latest = latest; 69 | $scope.newsItems = newsItems; 70 | } 71 | }; 72 | 73 | newsService.registerClient(updateNews); 74 | newsService.start(); 75 | }]); 76 | -------------------------------------------------------------------------------- /static/js/controllers/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var pageCtrls = angular.module('pageCtrls', [ 18 | 'globalServices', 19 | 'pageServices', 20 | ]); 21 | 22 | pageCtrls.controller('StaticPageCtrl', [ 23 | '$scope', 24 | 'pageService', 25 | 'errorService', 26 | 'loadingService', 27 | function($scope, pageService, errorService, loadingService) { 28 | $scope.path = pageService.pagePath(); 29 | if ($scope.path == "") { 30 | $scope.path = "home"; 31 | } 32 | pageService.get({path: $scope.path}, 33 | function(data) { 34 | $scope.page = data; 35 | loadingService.stop(); 36 | }, 37 | function(data) { 38 | // TODO: better handling here 39 | errorService.error(data); 40 | loadingService.stop(); 41 | }); 42 | }]); 43 | -------------------------------------------------------------------------------- /static/js/controllers/scoreboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var scoreboardCtrls = angular.module('scoreboardCtrls', [ 18 | 'ngResource', 19 | 'globalServices' 20 | ]); 21 | 22 | scoreboardCtrls.controller('ScoreboardCtrl', [ 23 | '$scope', 24 | '$resource', 25 | '$interval', 26 | 'configService', 27 | 'errorService', 28 | 'loadingService', 29 | function($scope, $resource, $interval, configService, errorService, 30 | loadingService) { 31 | $scope.config = configService.get(); 32 | 33 | var topTeams = function(scoreboard, numTeams) { 34 | // Scoreboard data is sorted by backend 35 | var numTeams = numTeams || 10; 36 | return scoreboard.slice(0, numTeams); 37 | }; 38 | 39 | var getHistory = function(scoreboard) { 40 | var histories = {}; 41 | angular.forEach(topTeams(scoreboard), function(entry) { 42 | histories[entry.name] = entry.history; 43 | }); 44 | return histories; 45 | }; 46 | 47 | var refresh = function() { 48 | errorService.clearErrors(); 49 | $resource('/api/scoreboard').get( 50 | function(data) { 51 | $scope.scoreboard = data.scoreboard; 52 | $scope.scoreHistory = getHistory(data.scoreboard); 53 | loadingService.stop(); 54 | }, 55 | function(data) { 56 | errorService.error(data); 57 | loadingService.stop(); 58 | }); 59 | }; 60 | 61 | refresh(); 62 | var iprom = $interval(refresh, 60000); 63 | 64 | $scope.$on('$destroy', function() { 65 | $interval.cancel(iprom); 66 | }); 67 | }]); 68 | -------------------------------------------------------------------------------- /static/js/controllers/teams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var teamCtrls = angular.module('teamCtrls', [ 18 | 'teamServices', 19 | 'globalServices', 20 | ]); 21 | 22 | teamCtrls.controller('TeamPageCtrl', [ 23 | '$scope', 24 | '$routeParams', 25 | 'teamService', 26 | 'errorService', 27 | 'loadingService', 28 | function($scope, $routeParams, teamService, errorService, loadingService) { 29 | var tid = $routeParams.tid; 30 | teamService.get({tid: tid}, 31 | function(team) { 32 | $scope.team = team; 33 | var tagData = {}; 34 | angular.forEach(team.solved_challenges, function(chall) { 35 | angular.forEach(chall.tags, function(tag) { 36 | if (!(tag.tagslug in tagData)) 37 | tagData[tag.tagslug] = chall.points; 38 | else 39 | tagData[tag.tagslug] += chall.points; 40 | }); 41 | }); 42 | $scope.tagData = tagData; 43 | $scope.scoreHistory = {}; 44 | $scope.scoreHistory[team.name] = team.score_history; 45 | $scope.scoreHistory[team.name].sort(function(a, b) { 46 | return (Date.parse(a.solved) - Date.parse(b.solved)); 47 | }); 48 | loadingService.stop(); 49 | }, 50 | function(err) { 51 | errorService.error('Unable to load team info.'); 52 | loadingService.stop(); 53 | }); 54 | }]); 55 | -------------------------------------------------------------------------------- /static/js/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var sbFilters = angular.module('sbFilters', []); 18 | 19 | sbFilters.filter('markdown', [ 20 | '$sce', 21 | function($sce) { 22 | return function(input) { 23 | if (typeof input != "string") 24 | return ""; 25 | if (typeof Markdown == "undefined" || 26 | typeof Markdown.getSanitizingConverter == "undefined") { 27 | console.log('Markdown not available!'); 28 | return input; 29 | } 30 | var converter = Markdown.getSanitizingConverter(); 31 | return $sce.trustAsHtml(converter.makeHtml(input)); 32 | }; 33 | }]); 34 | 35 | sbFilters.filter('padint', 36 | function() { 37 | return function(n, len) { 38 | if (!len) 39 | len = 2; 40 | else 41 | len = parseInt(len); 42 | n = '' + n; 43 | while(n.length < len) 44 | n = '0' + n; 45 | return n; 46 | }; 47 | }); 48 | 49 | sbFilters.filter('escapeHtml', [ 50 | function() { 51 | return function(input) { 52 | return $("
").text(input).html(); 53 | }; 54 | }]); 55 | -------------------------------------------------------------------------------- /static/js/services/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* Admin-only services */ 18 | var adminServices = angular.module('adminServices', ['ngResource']); 19 | 20 | adminServices.service('adminToolsService', [ 21 | '$resource', 22 | function($resource) { 23 | this.recalculateScores = $resource('/api/tools/recalculate').save; 24 | this.resetScores = function(cb, err) { 25 | return $resource('/api/tools/reset').save( 26 | {op: "scores", ack: "ack"}, cb, err); 27 | }; 28 | this.resetPlayers = function(cb, err) { 29 | return $resource('/api/tools/reset').save( 30 | {op: "players", ack: "ack"}, cb, err); 31 | }; 32 | }]); 33 | -------------------------------------------------------------------------------- /static/js/services/challenges.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var challengeServices = angular.module('challengeServices', [ 18 | 'ngResource', 19 | 'globalServices']); 20 | 21 | challengeServices.service('challengeService', [ 22 | '$resource', 23 | '$rootScope', 24 | '$cacheFactory', 25 | '$timeout', 26 | function($resource, $rootScope, $cacheFactory, $timeout) { 27 | var cache = $cacheFactory('challengeCache'); 28 | var cacheTimeout = 30000; 29 | var res = $resource('/api/challenges/:cid', {}, { 30 | 'get': {method: 'GET', cache: cache}, 31 | 'save': {method: 'PUT'}, 32 | 'create': {method: 'POST'}, 33 | 'delete': {method: 'DELETE'}, 34 | }); 35 | this.get = res.get; 36 | this.delete = res.delete; 37 | this.save = function() { 38 | cache.removeAll(); 39 | return res.save.apply(res, arguments); 40 | }; 41 | this.create = function() { 42 | cache.removeAll(); 43 | return res.create.apply(res, arguments); 44 | }; 45 | this.flush = cache.removeAll; 46 | $rootScope.$on('correctAnswer', cache.removeAll); 47 | return this; 48 | }]); 49 | 50 | challengeServices.service('tagService', [ 51 | '$resource', 52 | '$rootScope', 53 | '$cacheFactory', 54 | '$timeout', 55 | function($resource, $rootScope, $cacheFactory, $timeout) { 56 | var tagCache = $cacheFactory('tagCache'); 57 | 58 | this.res = $resource('/api/tags/:tagslug', {}, { 59 | 'get': {method: 'GET', tagCache}, 60 | 'save': {method: 'PUT'}, 61 | 'create': {method: 'POST'}, 62 | }) 63 | 64 | this.get = this.res.get; 65 | this.save = this.res.save; 66 | this.create = this.res.create; 67 | this.delete = this.res.delete; 68 | 69 | this.getList = function(callback) { 70 | if (this.taglist) { 71 | callback(this.taglist); 72 | return; 73 | } 74 | this.res.get(angular.bind(this, function(data) { 75 | this.taglist = data; 76 | $timeout( 77 | angular.bind(this, function() { 78 | this.taglist = null; 79 | tagCache.removeAll(); 80 | }), 81 | 30000, false); 82 | callback(data); 83 | })) 84 | $rootScope.$on('$locationChangeSuccess', function() { 85 | this.taglist = null; 86 | tagCache.removeAll(); 87 | }); 88 | } 89 | 90 | } 91 | ]) 92 | 93 | challengeServices.service('attachService', [ 94 | '$resource', 95 | '$rootScope', 96 | '$cacheFactory', 97 | '$timeout', 98 | function($resource, $rootScope, $cacheFactory, $timeout) { 99 | var attachCache = $cacheFactory('attachCache'); 100 | 101 | this.res = $resource('/api/attachments/:aid', {}, { 102 | 'get': {method: 'GET', attachCache}, 103 | 'save': {method: 'PUT'}, 104 | }) 105 | 106 | this.get = this.res.get; 107 | this.create = this.res.create; 108 | this.save = this.res.save; 109 | this.delete = this.res.delete; 110 | 111 | this.getList = function(callback) { 112 | if (this.attachlist) { 113 | callback(this.attachlist); 114 | return; 115 | } 116 | this.res.get(angular.bind(this, function(data) { 117 | this.attachlist = data; 118 | $timeout( 119 | angular.bind(this, function() { 120 | this.attachlist = null; 121 | attachCache.removeAll(); 122 | }), 123 | 30000, false); 124 | callback(data); 125 | })) 126 | $rootScope.$on('$locationChangeSuccess', function() { 127 | this.attachlist = null; 128 | attachCache.removeAll(); 129 | }); 130 | } 131 | 132 | } 133 | ]) 134 | 135 | challengeServices.service('answerService', [ 136 | '$resource', 137 | '$rootScope', 138 | function($resource, $rootScope) { 139 | this.res = $resource('/api/answers/:aid', {}, { 140 | 'create': {method: 'POST'} 141 | }); 142 | this.create = function(what, success, failure) { 143 | this.res.create(what, 144 | function(resp) { 145 | success(resp); 146 | $rootScope.$broadcast('correctAnswer'); 147 | }, 148 | failure); 149 | }; 150 | }]); 151 | 152 | challengeServices.service('validatorService', [ 153 | '$resource', 154 | '$rootScope', 155 | function($resource, $rootScope) { 156 | this.res = $resource('/api/validator', {}, { 157 | 'create': {method: 'POST'} 158 | }); 159 | this.create = function(what, success, failure) { 160 | this.res.create(what, 161 | function(resp) { 162 | success(resp); 163 | }, 164 | failure); 165 | }; 166 | }]); 167 | 168 | challengeServices.service('scoreService', [ 169 | 'configService', 170 | function(configService) { 171 | this.scoring = 'plain'; 172 | configService.get(angular.bind(this, function(cfg) { 173 | this.scoring = cfg.scoring; 174 | })); 175 | this.getCurrentPoints = function(challenge) { 176 | if (!challenge) 177 | return 0; 178 | return challenge.current_points; 179 | }; 180 | }]) 181 | -------------------------------------------------------------------------------- /static/js/services/page.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* page services */ 18 | var pageServices = angular.module('pageServices', ['ngResource']); 19 | 20 | pageServices.service('pageService', [ 21 | '$resource', 22 | '$location', 23 | function($resource, $location) { 24 | this.pagelist = []; 25 | 26 | this.resource = $resource('/api/page/:path'); 27 | this.get = this.resource.get; 28 | this.save = this.resource.save; 29 | this.delete = this.resource.delete; 30 | 31 | /** Return path to page with prefix stripped. */ 32 | this.pagePath = function(prefix) { 33 | prefix = prefix || '/'; 34 | var path = $location.path(); 35 | if (path.substr(0, prefix.length) == prefix) { 36 | path = path.substr(prefix.length); 37 | } 38 | return path; 39 | }; 40 | 41 | this.getList = function(callback) { 42 | if (this.pagelist) { 43 | callback(this.pagelist); 44 | return; 45 | } 46 | this.res.get(angular.bind(this, function(data) { 47 | this.pagelist = data; 48 | $timeout( 49 | angular.bind(this, function() { 50 | this.pagelist = null; 51 | }), 52 | 60000, false); 53 | callback(data); 54 | })) 55 | $rootScope.$on('$locationChangeSuccess', function() { 56 | this.pagelist = null; 57 | }); 58 | } 59 | 60 | }]); 61 | -------------------------------------------------------------------------------- /static/js/services/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var sessionServices = angular.module('sessionServices', [ 18 | 'ngResource', 19 | 'globalServices', 20 | ]); 21 | 22 | sessionServices.service('sessionService', [ 23 | '$resource', '$location', '$rootScope', 'errorService', 24 | function($resource, $location, $rootScope, errorService) { 25 | this.sessionData = $resource('/api/session'); 26 | this.session = { 27 | user: null, 28 | team: null 29 | }; 30 | 31 | this.login = function(email, password, successCallback, errorCallback) { 32 | this.sessionData.save({email: email, password: password}, 33 | angular.bind(this, function(data) { 34 | this.session.user = data.user; 35 | this.session.team = data.team; 36 | if (successCallback) 37 | successCallback(); 38 | $rootScope.$broadcast('sessionLogin'); 39 | }), errorCallback || function() {}); 40 | }; 41 | 42 | this.logout = function(callback) { 43 | this.sessionData.remove(function() { 44 | $rootScope.$broadcast('sessionLogout'); 45 | callback(); 46 | }); 47 | this.session.user = null; 48 | this.session.team = null; 49 | }; 50 | 51 | this.refresh = function(successCallback, errorCallback) { 52 | // Attempt to load 53 | this.sessionData.get(angular.bind(this, function(data) { 54 | var currUser = this.session.user && this.session.user.nick; 55 | this.session.user = data.user; 56 | this.session.team = data.team; 57 | if (currUser && !this.session.user) 58 | $rootScope.$broadcast('sessionLogout'); 59 | if (!currUser && this.session.user) 60 | $rootScope.$broadcast('sessionLogin'); 61 | if (successCallback) 62 | successCallback(); 63 | }), errorCallback || function() {}); 64 | }; 65 | 66 | this.requireLogin = function(callback, no_redirect) { 67 | /* If the user is logged in, execute the callback. Otherwise, redirect 68 | * to the login. */ 69 | if (this.session.user !== null) { 70 | return callback(); 71 | } 72 | return this.refresh(callback, 73 | function() { 74 | if (no_redirect) 75 | return; 76 | errorService.clearAndInhibit(); 77 | errorService.error('You must be logged in.', 'info'); 78 | $location.path('/login'); 79 | }); 80 | }; 81 | 82 | this.requireAdmin = function(opt_callback) { 83 | var cb = angular.bind(this, function() { 84 | if (this.session.user && this.session.user.admin) { 85 | if (opt_callback) 86 | opt_callback(); 87 | return true; 88 | } 89 | errorService.clearAndInhibit(); 90 | errorService.error('You are not an admin!'); 91 | $location.path('/'); 92 | return false; 93 | }); 94 | if (this.session.user != null) { 95 | return cb(); 96 | } 97 | this.requireLogin(cb); 98 | return true; 99 | }; 100 | 101 | this.refresh(); 102 | }]); 103 | 104 | function getss(){ 105 | return angular.element(document).injector().get('sessionService'); 106 | } 107 | -------------------------------------------------------------------------------- /static/js/services/teams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var teamServices = angular.module('teamServices', ['ngResource']); 18 | 19 | teamServices.service('teamService', ['$resource', 20 | function($resource) { 21 | var resource = $resource('/api/teams/:tid', {}, { 22 | save: {method: 'PUT'}, 23 | create: {method: 'POST'} 24 | }); 25 | resource.change = function(data, cb, error) { 26 | return resource.save({tid: "change"}, data, cb, error); 27 | } 28 | return resource; 29 | }]); 30 | -------------------------------------------------------------------------------- /static/js/services/upload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* upload services */ 18 | var uploadServices = angular.module('uploadServices', ['ngResource']); 19 | 20 | uploadServices.service('uploadService', ['$http', '$q', 21 | function($http, $q) { 22 | var basename = function(path) { 23 | return path.split('/').reverse()[0]; 24 | }; 25 | 26 | this.request = function() { 27 | return $q(function(resolve) { 28 | var form = $('#new-attachment'); 29 | form.click(); 30 | form.off('change'); 31 | form.on('change', function() { 32 | resolve(form.get(0).files[0]); 33 | }) 34 | }) 35 | } 36 | this.upload = function(file) { 37 | // Returns a promise with the file hash 38 | var filename = basename(file.name); 39 | // Construct the promise 40 | var promise = $q.defer(); 41 | // HTTP Config 42 | var config = { 43 | transformRequest: angular.identity, 44 | 'headers': { 45 | 'Content-type': undefined 46 | } 47 | }; 48 | 49 | // Setup form data 50 | var fd = new FormData(); 51 | fd.append('file', file); 52 | // Request 53 | $http.post('/api/attachments', fd, config). 54 | success(function(data) { 55 | data.filename = filename; 56 | promise.resolve(data); 57 | }). 58 | error(function(data, status) { 59 | if (data) 60 | promise.reject(data); 61 | else 62 | promise.reject('Unknown upload error.'); 63 | }); 64 | return promise.promise; 65 | }; 66 | }]); 67 | -------------------------------------------------------------------------------- /static/js/services/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var userServices = angular.module('userServices', ['ngResource']); 18 | 19 | userServices.service('userService', [ 20 | '$resource', 21 | function($resource) { 22 | return $resource('/api/users/:uid', {}, { 23 | 'save': {method: 'PUT'}, 24 | 'create': {method: 'POST'} 25 | }); 26 | }]); 27 | 28 | userServices.service('passwordResetService', [ 29 | '$resource', 30 | function($resource) { 31 | return $resource('/api/pwreset/:email'); 32 | }]); 33 | 34 | userServices.service('apiKeyService', [ 35 | '$resource', 36 | function($resource) { 37 | return $resource('/api/apikey/:keyid', {}, { 38 | 'create': {method: 'POST'}, 39 | 'deleteAll': {method: 'DELETE', params:{}} 40 | }); 41 | }]); 42 | -------------------------------------------------------------------------------- /static/partials/admin/attachments.html: -------------------------------------------------------------------------------- 1 |

Attachments

2 |
3 |
4 |
5 | 6 | 8 |
9 |
10 | 11 | 14 |
15 | 16 |
17 |
18 | {{ch.name}} 19 |
20 |
21 | 22 |
23 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 34 |
35 |
36 | 37 | 43 |
44 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /static/partials/admin/challenge.html: -------------------------------------------------------------------------------- 1 |

Challenge

2 |
3 |
4 |
5 |
6 |
7 | 8 | 11 |
12 |
13 |
14 |
15 | 16 | 19 |
20 |
21 |
22 |
23 | 24 | 26 |
27 |
28 |
29 |
30 | 31 | 34 |
35 |
36 |
37 |
38 | 39 | 43 |
44 |
45 |
46 |
47 | 48 | 51 |
52 |
53 |
54 |
55 | 56 | 58 |
59 |
60 |
61 |

Tags

62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 |
Tag NameTag Description
{{tag.name}}{{tag.description}} 74 | 75 | 76 |
80 |
81 |
82 |

Attachments

83 | 84 | 85 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | 96 | 105 | 106 | 107 |
FileFilehash
{{attachment.filename}}{{attachment.aid}}
97 | 98 | 102 | 103 | 104 |
108 |
109 |
110 |

Prerequisite

111 |
112 | 113 | 118 |
119 |
121 | 122 | 128 |
129 |
130 | 132 |
133 | 134 | 135 | -------------------------------------------------------------------------------- /static/partials/admin/challenges.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 28 | 29 | 30 | 34 | 35 |
NamePointsSolvesActions
{{challenge.name}}{{challenge.points}}{{challenge.solves}} 12 | 13 | Locked 16 | Unlocked 19 | Edit 21 | Delete 23 | 25 | 27 |
NewSave Order
36 | -------------------------------------------------------------------------------- /static/partials/admin/news.html: -------------------------------------------------------------------------------- 1 |

Submit News

2 |
3 |
4 | 5 | 10 |
11 |
13 | 14 | 19 |
20 |
21 | 22 | 24 |
25 | 27 |
28 | -------------------------------------------------------------------------------- /static/partials/admin/page.html: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 | 5 | 7 |
8 |
9 | 10 | 12 |
13 | 15 |
16 | -------------------------------------------------------------------------------- /static/partials/admin/pages.html: -------------------------------------------------------------------------------- 1 |

Pages

2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 25 | 28 | 31 | 32 | 33 | 34 | 37 | 40 | 41 | 42 | 43 | 44 |
TitleURL
15 | {{p.title}} 16 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | search 27 | 29 | edit 30 |
35 | 36 | 38 | launch 39 |
45 |
46 | 47 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /static/partials/admin/restore.html: -------------------------------------------------------------------------------- 1 |

Backup/Restore Challenges

2 |
Backup 3 | Backup Challenges 4 |
5 |
Restore 6 |
7 | 9 |
10 | 11 |
12 |
{{fileName}}
13 |
16 |
17 |
18 |
19 | 21 |
22 |
23 |
    24 |
  • {{chall.name}}
  • 25 |
26 |
27 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /static/partials/admin/tags.html: -------------------------------------------------------------------------------- 1 |

Tags

2 |
3 |
4 |
5 | 6 | 8 |
9 |
10 | 11 | 13 |
14 | 15 |
16 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 27 |
28 |
29 | 30 | 32 |
33 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /static/partials/admin/teams.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
TeamSolvesScore
{{team.name}}{{team.solves}}{{team.score}}
17 | 18 | 19 |
20 |

21 |
22 |
23 |
24 |
25 |
26 |
27 |

Solved Challenges ({{team.score}} points)

28 |

This team has not solved any challenges.

29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
NamePointsSolved
38 |
39 |
40 |
41 |
43 |

Team Members

44 | 45 | 46 | 47 | 48 | 49 | 50 |
HandleEmail
51 |
52 |
53 |
54 |
56 |

Unsolved Challenges

57 | 59 | 60 | 61 | 62 | 63 | 65 | 66 |
NamePoints
67 |
68 |
69 |
70 | 71 | 97 | 98 | 122 | -------------------------------------------------------------------------------- /static/partials/admin/tools.html: -------------------------------------------------------------------------------- 1 |

Admin Tools

2 |

4 |

6 |

8 |

10 | -------------------------------------------------------------------------------- /static/partials/admin/users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 17 | 18 | 19 |
NickE-MailTeam 5 | Solves
{{user.nick}} 11 | (Admin){{user.email}}{{user.team.name}} 16 | {{user.team.solves}}
20 |
21 |
22 |
23 |
24 | 25 | 27 |
28 |
29 | 30 | 32 |
33 |
34 |
35 | 36 | 38 |
39 |
40 |
41 | 45 |
46 | 47 |
48 | 49 |
50 |
51 | -------------------------------------------------------------------------------- /static/partials/challenge_grid.html: -------------------------------------------------------------------------------- 1 |

Challenges

2 |
3 |
4 | left or right click to filter tags 5 |
6 |
7 |
10 | 11 | {{getSentiment(tag)}} 12 | {{tag.name}} 13 | 14 |
15 |
16 |
17 | 18 |
19 |
24 |
25 |
Solved
27 |
28 | 29 |
30 |
31 |
32 | 33 | 34 |
35 |
36 |

37 |
38 |
39 |
40 | 41 |