Showing preview only (3,104K chars total). Download the full file or copy to clipboard to get everything.
Repository: google/ctfscoreboard
Branch: main
Commit: 28a8f6c30e40
Files: 135
Total size: 2.9 MB
Directory structure:
gitextract_m6halfw6/
├── .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
│ ├── 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-route.js
│ │ ├── angular-sanitize.js
│ │ └── angular.js
│ ├── bootstrap/
│ │ ├── LICENSE
│ │ ├── bootstrap.css
│ │ └── bootstrap.js
│ ├── bootstrap-theme/
│ │ └── bootstrap-theme.css
│ ├── chart/
│ │ ├── Chart.Scatter.js
│ │ ├── Chart.js
│ │ ├── LICENSE.md
│ │ └── Scatter.LICENSE.md
│ ├── jquery/
│ │ ├── LICENSE.txt
│ │ └── jquery.js
│ ├── moment/
│ │ ├── LICENSE
│ │ └── moment.js
│ └── pagedown/
│ ├── LICENSE.txt
│ ├── Markdown.Converter.js
│ ├── Markdown.Editor.js
│ └── Markdown.Sanitizer.js
├── templates/
│ ├── base.html
│ ├── error.html
│ ├── index.html
│ └── pwreset.eml
└── tests.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .codecov.yml
================================================
codecov:
notify:
require_ci_to_pass: true
comment:
behavior: default
layout: header, diff
require_changes: false
coverage:
precision: 2
range:
- 50.0
- 90.0
round: down
status:
changes: false
patch: true
project: true
parsers:
gcov:
branch_detection:
conditional: true
loop: true
macro: false
method: false
javascript:
enable_partials: false
================================================
FILE: .coveragerc
================================================
[run]
source = scoreboard
omit =
scoreboard/tests/*
main.py
setup.py
branch = True
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
indent_style = space
indent_size = 4
charset = utf-8
[*.py]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
================================================
FILE: .gcloudignore
================================================
.gcloudignore
.git
scoreboard/tests
*.md
*.pyc
htmlcov/
doc/
.hooks/
LICENSE
AUTHORS
Makefile
Dockerfile
config.example.py
.coverage
.coveragerc
.travis.yml
.editorconfig
.gitignore
.codecov.yml
================================================
FILE: .gitignore
================================================
# Compiled, swap files
*.pyc
*.swp
.virtualenv
__pycache__
# Runtime data
/config.py
*.bak
*.db
/attachments
/attachments/**
# Generated files
/static/js/app.min.js
/static/css/*.css
# Python coverage tool2
.coverage
htmlcov
================================================
FILE: .hooks/pre-commit.sh
================================================
#!/bin/bash
if [ "${SKIP_TESTS}" != "" ] ; then
exit 0
fi
# stash code not to be committed
git stash -q --keep-index >/dev/null 2>&1
RESULT=0
if git status --porcelain | awk '{print $2}' | grep -q '^scoreboard/' ; then
# Run tests and flake8 if any files in scoreboard/... changed.
python3 tests.py && flake8 scoreboard main.py
RESULT=$?
fi
# restore stash
# git has a bad bug with 2.24 and --quiet where it deletes files
if git --version | grep -q '^git version 2.24' ; then
git stash pop >/dev/null 2>&1
else
git stash pop -q >/dev/null 2>&1
fi
exit $RESULT
================================================
FILE: .travis.yml
================================================
language: python
sudo: false
python:
- "2.7"
- "3.6"
- "3.7"
install:
- pip install -r requirements.txt
- pip install -r doc/developing/requirements.txt
- pip install codecov
script:
- coverage run tests.py
- flake8 .
after_success:
- codecov
================================================
FILE: AUTHORS
================================================
Current maintainer: David Tomaschik <dwt@google.com>
Core Team:
Andrew Griffiths <agriffiths@google.com>
David Tomaschik <dwt@google.com>
Niru Ragupathy <niruragu@google.com>
Zachary Wade <zwade@google.com>
================================================
FILE: CONTRIBUTING.md
================================================
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement]
(https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement]
(https://cla.developers.google.com/about/google-corporate).
================================================
FILE: Dockerfile
================================================
FROM debian:buster
RUN apt-get update && apt-get install -y \
nginx \
python3 \
python3-dev \
python3-pip \
supervisor \
uwsgi \
uwsgi-plugin-python3 \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
COPY doc/nginx.conf /etc/nginx/sites-enabled/default
COPY doc/docker/supervisord.conf /etc/supervisor/conf.d/
COPY . /opt/scoreboard
# Suggest you mount a config at /opt/scoreboard/config.py instead
COPY config.example.py /opt/scoreboard/config.py
WORKDIR /opt/scoreboard
RUN make
# TODO: migrate this to run at runtime
RUN python3 main.py createdb
RUN chmod 666 /tmp/scoreboard*
CMD ["/usr/bin/supervisord"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
# Makefile to minimize JS using UglifyJS (https://github.com/mishoo/UglifyJS2)
# or 'cat' to just assemble into one file.
MIN_JS:=static/js/app.min.js
JS_SRC:=$(shell find static/js \! -name '*.min.*' -name '*.js')
MINIFY:=$(shell which uglifyjs >/dev/null && echo `which uglifyjs` || echo cat)
# Declarations for SCSS
SCSS_SRC:=$(shell find static/scss -name '*.scss')
PYSCSS:=$(shell which pyscss >/dev/null && echo `which pyscss` )
all: $(MIN_JS) scss
$(MIN_JS): $(JS_SRC)
$(MINIFY) $^ > $@
dev: scss
python main.py
scss:
@if [ "$(PYSCSS)" = "" ]; then\
echo "pyscss not found, exiting";\
exit -1;\
fi;\
for i in $$(ls static/scss/); do\
echo "Making $${i%.scss}.css";\
$(PYSCSS) -o static/css/$${i%.scss}.css static/scss/$$i;\
done
tests:
python tests.py
coverage:
coverage run main.py runtests
coverage html
xdg-open htmlcov/index.html
================================================
FILE: README.md
================================================
## CTF Scoreboard ##
This is a basic CTF Scoreboard, with support for teams or individual
competitors, and a handful of other features.
Copyright 2020 Google LLC.
This is not an official Google product.
Author: Please see the AUTHORS file.
This is a version 2.x branch. We've eliminated categories, in favor of tagging
challenges. This simplifies the codebase significantly, and is a better fit
since so many challenges border on more than one category. However, this branch
is not compatible with databases from 1.x. If you need that, check out the 1.x
branch, which will only be getting security & bug fixes.
### Installation ###
1. Install Python with PIP and setuptools. If you'd like to use a virtualenv,
set one up and activate it now. Please note that only Python 3.6+ is
officially supported at the present time, but it should still work on Python 2.7.
2. Install the dependencies:
pip install -r requirements.txt
3. Install a database library. For MySQL, consider PyMySQL. For Postgres,
use psycopg2. (Others may work; untested.)
4. Write a config.py for your relevant installation. An example is provided in
config.example.py.
SQLALCHEMY_DATABASE_URI = 'mysql://username:password@server/db'
#SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://username:password@server/db'
SECRET_KEY = 'Some Random Value For Session Keys'
TEAM_SECRET_KEY = 'Another Random Value For Team Invite Codes'
TITLE = 'FakeCTF'
TEAMS = True
ATTACHMENT_DIR = 'attachments'
LOGIN_METHOD = 'local' # or appengine
If you are using plaintext HTTP to run your scoreboard, you will need to add the
following to your config.py, so that cookies will work:
SESSION_COOKIE_SECURE = False
If you are developing the scoreboard, the following settings may be useful for
debugging purposes. Not useful for production usage, however.
COUNT_QUERIES = True
SQLALCHEMY_ECHO = True
5. Create the database:
python main.py createdb
6. Set up your favorite python application server, optionally behind a
webserver. You'll want to use main.app as your WSGI handler.
Tested with uwsgi + nginx. Not tested with anything else,
let me know if you have success. Sample configs are in doc/.
7. Register a user. The first user registed is automatically made an admin.
You probably want to register your user before your players get access.
8. Have fun! Maybe set up some challenges. Players might like that more.
### Installation using Docker ###
1. Navigate to the folder where the Dockerfile is located.
2. Run the command below to build a docker image for the scoreboard and tag it as "scoreboard".
docker build -t "scoreboard" .
3. Run the command below to create the docker container.
docker create -p 80:80 scoreboard
4. Find the name of the container you created for the scoreboard.
docker container ls -a
5. Run the command below to start the docker container for the scoreboard.
docker start "container_name"
### Options ###
**SCORING**: Set to 'progressive' to enable a scoring system where the total
points for each challenge are divided amongst all the teams that solve that
challenge. This rewards teams that solve infrequently solved (hard or obscure)
challenges.
**TITLE**: Scoreboard page titles.
**TEAMS**: True if teams should be used, False for each player on their own
team.
**SQLALCHEMY_DATABASE_URI**: A SQLAlchemy database URI string.
**LOGIN_METHOD**: Supports 'local'
### Development ###
[](https://travis-ci.org/google/ctfscoreboard)
[](https://codecov.io/gh/google/ctfscoreboard)
**Use hooks**
ln -s ../../.hooks/pre-commit.sh .git/hooks/pre-commit
**Test Cases**
- Setup database
- Create user, verify admin
- Create challenge
- With, without attachment
- Edit challenges
- Add attachment
- Delete attachment
- Download backup
- Restore backup
- Create 2nd user, verify not admin
- Solve challenge
- Download attachment
### Thanks ###
This project stands on the shoulders of giants.
A big thanks to the following projects used to build this:
- [Flask](http://flask.pocoo.org/)
- [Flask-SQLAlchemy](https://pythonhosted.org/Flask-SQLAlchemy/)
- [Flask-RESTful](https://flask-restful.readthedocs.io/en/latest/)
- [SQLAlchemy](http://www.sqlalchemy.org/)
- [AngularJS](https://angularjs.org/)
- [jQuery](https://jquery.com/)
- [PageDown](https://jquery.com/)
- [Bootstrap](http://getbootstrap.com/)
And many more indirect dependencies.
================================================
FILE: app.yaml
================================================
runtime: python37
instance_class: F4
automatic_scaling:
max_instances: 50
max_idle_instances: 10
handlers:
- url: /css
static_dir: static/css
secure: always
- url: /js
static_dir: static/js
secure: always
- url: /partials
static_dir: static/partials
secure: always
- url: /fonts
static_dir: static/fonts
secure: always
- url: /third_party
static_dir: static/third_party
secure: always
- url: /createdb
secure: always
script: auto
- url: /.*
secure: always
script: auto
================================================
FILE: config.example.py
================================================
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Demo config.py, please configure your own
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/scoreboard.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = 'CHANGEME CHANGEME CHANGEME'
# Set TEAM_SECRET_KEY to a unique value so that you can rotate session
# secrets (SECRET_KEY) without affecting team invite codes.
TEAM_SECRET_KEY = SECRET_KEY
TITLE = 'CTF Scoreboard Dev'
TEAMS = True
ATTACHMENT_BACKEND = 'file:///tmp/attachments'
LOGIN_METHOD = 'local'
SESSION_COOKIE_SECURE = False
PROOF_OF_WORK_BITS = 12
================================================
FILE: doc/developing/README.md
================================================
# Development Setup
You'll want to have `python3` and `pip` installed. I also recommend
`virtualenv` and `virtualenvwrapper` (if you don't have these, skip the
virtualenv steps).
On Debian or Ubuntu Linux, run:
`apt install python3 python3-pip virtualenvwrapper`
1. `mkvirtualenv -p $(which python3) scoreboard` to create the virtualenv.
2. `git clone https://github.com/google/ctfscoreboard && cd ctfscoreboard` to clone the source.
3. `pip install -r requirements.txt` to install runtime dependencies.
4. `pip install -r doc/developing/requirements.txt` to install development
dependencies.
5. `ln -s .hooks/pre-commit.sh .git/hooks/pre-commit` to install the development
pre-commit hook.
# Configuration & Initial Setup
Copy the file `config.example.py` to `config.py` to make a configuration. This
is suitable for basic development work, and will use a sqlite3 database in
`/tmp/scoreboard.db` for storage.
You'll want to run `python main.py createdb` to create the initial database.
Optionally, you can run `python main.py createdata` to create some test data.
These are just dummy challenges, teams, and users used for testing.
# Running/Iterating
You can either run `make dev` or `python main.py` to run the development server.
By default, it runs on port 9999, but you can change this in your `config.py`.
**Note** that if you make changes to models, affecting the database schema, you
must either manually update your database, or delete it and recreate from
scratch. Because this is a CTF scoreboard, used for short-lived events, there's
no migration code.
Run `make scss` to compile SCSS to CSS. You'll want to do this at least once,
and any time you change the SCSS. Note that the CSS is not tracked in the git
repository, so all style changes *must* be to SCSS.
# Making Changes
Please do all development work on a feature branch. Run the tests before you
commit (if you have the git hook, it should run the the tests before
committing). We try to mostly follow PEP-8, and `flake8` helps catch those
mistakes.
================================================
FILE: doc/developing/requirements.txt
================================================
# Additional requirements for development
coverage
flake8
flask-testing
mock
================================================
FILE: doc/docker/supervisord.conf
================================================
[supervisord]
nodaemon=true
[program:uwsgi]
command=/usr/bin/uwsgi --ini /opt/scoreboard/doc/docker/uwsgi.ini
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
================================================
FILE: doc/docker/uwsgi.ini
================================================
# Sample uWSGI config file
[uwsgi]
chdir = /opt/scoreboard
socket = 127.0.0.1:9000
processes = 4
threads = 2
master = true
module = scoreboard.wsgi
callable = app
uid = nobody
gid = nogroup
daemonize = /var/log/uwsgi/app/uwsgi.log
plugins = python3
================================================
FILE: doc/nginx.conf
================================================
server {
listen 80 default_server;
root /opt/scoreboard/static; # Make sure code is not in document root!
location @backend {
include uwsgi_params;
uwsgi_pass 127.0.0.1:9000;
}
location / {
try_files $uri @backend;
}
}
================================================
FILE: doc/uwsgi.ini
================================================
# Sample uWSGI config file
[uwsgi]
chdir = /opt/scoreboard
socket = 127.0.0.1:9000
processes = 4
threads = 2
master = true
module = scoreboard.wsgi
callable = app
virtualenv = /opt/virtualenv
uid = nobody
gid = nogroup
daemonize = /var/log/uwsgi/app/uwsgi.log
plugins = python
================================================
FILE: main.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
from scoreboard import wsgi
from scoreboard import models
# For use in gunicorn
from scoreboard.wsgi import app # noqa: F401
def main(argv):
if 'createdb' in argv:
models.db.create_all()
elif 'createdata' in argv:
from scoreboard.tests import data
models.db.create_all()
data.create_all()
elif 'shell' in argv:
try:
import IPython
run_shell = IPython.embed
except ImportError:
import readline # noqa: F401
import code
run_shell = code.InteractiveConsole().interact
run_shell()
else:
wsgi.app.run(
host='0.0.0.0', debug=True,
port=wsgi.app.config.get('PORT', 9999))
if __name__ == '__main__':
main(sys.argv)
================================================
FILE: requirements.txt
================================================
Flask
Flask-RESTful
Flask-SQLAlchemy
Flask-Scss
Jinja2
MarkupSafe
PyMySQL
SQLAlchemy<1.4.0
Werkzeug<1.0.0
aniso8601
argparse
itsdangerous
pbkdf2
pylibmc
python-dateutil
pytz
six
google-cloud-logging
google-cloud-storage
mailjet_rest
requests
================================================
FILE: scoreboard/__init__.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
================================================
FILE: scoreboard/attachments/__init__.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Handle attachments via the appropriate backend.
Required API:
send(models.Attachment): returns a flask response to download attachment
upload(werkzeug.datastructures.FileStorage): returns attachment ID and path
delete(models.Attachment): deletes the attachment specified
"""
try:
import urlparse
except ImportError:
from urllib import parse as urlparse
from scoreboard import main
app = main.get_app()
backend = None
def get_backend_path():
"""Get backend path for attachments."""
return app.config.get('ATTACHMENT_BACKEND')
def get_backend_type():
"""Determine type of backend."""
backend = get_backend_path()
return urlparse.urlparse(backend).scheme
def get_backend(_backend_type):
backend = None
if _backend_type == "file":
from . import file as backend
elif _backend_type == "gcs":
from . import gcs as backend
elif _backend_type == "test":
from . import testing as backend
else:
raise ImportError('Unhandled attachment backend %s' % _backend_type)
return backend
def patch(_backend_type):
globals()['backend'] = get_backend(_backend_type)
backend = get_backend(get_backend_type())
================================================
FILE: scoreboard/attachments/file.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Local filesystem backend for attachments.
"""
import hashlib
import os
import os.path
try:
import urlparse
except ImportError:
from urllib import parse as urlparse
import flask
from scoreboard import main
app = main.get_app()
def attachment_dir(create=False):
"""Return path and optionally create attachment directory."""
components = urlparse.urlparse(app.config.get('ATTACHMENT_BACKEND'))
app.config_dir = components.path or components.netloc
if app.config.get('CWD'):
target_dir = os.path.normpath(os.path.join(app.config.get('CWD'),
app.config_dir))
else:
target_dir = os.path.abspath(app.config_dir)
if not os.path.isdir(target_dir):
if create:
os.mkdir(target_dir)
else:
app.logger.error('Missing or invalid ATTACHMENT_DIR: %s',
target_dir)
flask.abort(500)
return target_dir
def send(attachment):
"""Send the attachment to the client."""
return flask.send_from_directory(
attachment_dir(), attachment.aid,
mimetype=attachment.content_type,
attachment_filename=attachment.filename,
as_attachment=True)
def delete(attachment):
"""Delete the attachment from disk."""
path = os.path.join(attachment_dir(), attachment.aid)
os.unlink(path)
def upload(fp):
"""Upload the file attachment to the storage medium."""
md = hashlib.sha256()
while True:
blk = fp.read(2**16)
if not blk:
break
md.update(blk)
aid = md.hexdigest()
fp.seek(0, os.SEEK_SET)
dest_name = os.path.join(attachment_dir(create=True), aid)
fp.save(dest_name, buffer_size=2**16)
# TODO: add file:// prefix
return aid, dest_name
================================================
FILE: scoreboard/attachments/gcs.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Attachments on Google Cloud Storage.
"""
import hashlib
import os
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
try:
from io import BytesIO
except ImportError:
try:
import cStringIO.StringIO as BytesIO
except ImportError:
import StringIO.StringIO as BytesIO
import flask
from google.cloud import storage
from google.cloud import exceptions
from scoreboard import main
app = main.get_app()
def get_bucket(path=None):
path = path or app.config.get('ATTACHMENT_BACKEND')
url = urlparse.urlparse(path)
return url.netloc
def send(attachment):
"""Send to download URI."""
try:
client = storage.Client()
bucket = client.bucket(get_bucket())
buf = BytesIO()
blob = bucket.get_blob(attachment.aid)
if not blob:
return flask.abort(404)
blob.download_to_file(buf)
buf.seek(0)
return flask.send_file(
buf,
mimetype=attachment.content_type,
attachment_filename=attachment.filename,
add_etags=False, as_attachment=True)
except exceptions.NotFound:
return flask.abort(404)
def delete(attachment):
"""Delete from GCS Bucket."""
try:
client = storage.Client()
bucket = client.bucket(get_bucket())
bucket.delete_blob(attachment.aid)
except exceptions.NotFound:
return flask.abort(404)
def upload(fp):
"""Upload the attachment."""
md = hashlib.sha256()
while True:
blk = fp.read(2**16)
if not blk:
break
md.update(blk)
fp.seek(0, os.SEEK_SET)
aid = md.hexdigest()
client = storage.Client()
bucket = client.bucket(get_bucket())
blob = bucket.blob(aid)
blob.upload_from_file(fp)
path = 'gcs://{}/{}'.format(get_bucket(), aid)
return aid, path
================================================
FILE: scoreboard/attachments/testing.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Volatile filesystem backend for attachments.
"""
import hashlib
import io
import flask
from scoreboard import main
app = main.get_app()
files = {}
def send(attachment):
"""Send the attachment to the client."""
return flask.send_file(files[attachment.aid],
attachment_filename="testing.txt",
as_attachment=True)
def delete(attachment):
"""Delete the attachment from disk."""
del files[attachment.aid]
def upload(fp):
"""Upload the file attachment to the storage medium."""
md = hashlib.sha256()
ret = io.BytesIO()
while True:
blk = fp.read(2**16)
if not blk:
break
md.update(blk)
ret.write(blk)
aid = md.hexdigest()
ret.seek(0)
files[aid] = ret
return aid, ('test://%s' % aid)
================================================
FILE: scoreboard/auth/__init__.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This supports multiple auth systems as configured by the LOGIN_METHOD setting.
The required API includes:
login_user(flask_request): returns User or None
get_login_uri(): returns URI for login
get_register_uri(): returns URI for registration
logout(): returns None
register(flask_request): register a new user
"""
from scoreboard import main
_login_method = main.get_app().config.get('LOGIN_METHOD')
if _login_method == 'local':
from scoreboard.auth.local import * # noqa: F401,F403
else:
raise ImportError('Unhandled LOGIN_METHOD %s' % _login_method)
================================================
FILE: scoreboard/auth/local.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Local login support."""
from scoreboard import controllers
from scoreboard import errors
from scoreboard import models
def login_user(flask_request):
"""Get the user for this request."""
data = flask_request.get_json()
email = data['email'].lower()
user = models.User.login_user(email, data['password'])
if not user:
raise errors.LoginError('Invalid username/password.')
return user
def get_login_uri():
return '/login'
def get_register_uri():
return '/register'
def logout():
pass
def register(flask_request):
data = flask_request.get_json()
user = controllers.register_user(
data['email'].lower(), data['nick'],
data['password'], data.get('team_id'), data.get('team_name'),
data.get('team_code'))
return user
================================================
FILE: scoreboard/cache.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import json
import flask
from werkzeug.contrib import cache
from scoreboard import main
app = main.get_app()
class CacheWrapper(object):
def __init__(self, app):
cache_type = app.config.get('CACHE_TYPE')
if cache_type == 'memcached':
host = app.config.get('MEMCACHE_HOST')
self._cache = cache.MemcachedCache([host])
elif cache_type == 'local':
self._cache = cache.SimpleCache()
else:
self._cache = cache.NullCache()
def __getattr__(self, name):
return getattr(self._cache, name)
global_cache = CacheWrapper(app)
def rest_cache(f_or_key):
"""Mark a function for global caching."""
override_cache_key = None
def wrap_func(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
if override_cache_key:
cache_key = override_cache_key
else:
try:
cache_key = '%s/%s' % (
f.im_class.__name__, f.__name__)
except AttributeError:
cache_key = f.__name__
return _rest_cache_caller(f, cache_key, *args, **kwargs)
return wrapped
if isinstance(f_or_key, str):
override_cache_key = f_or_key
return wrap_func
return wrap_func(f_or_key)
def rest_cache_path(f):
"""Cache a result based on the path received."""
@functools.wraps(f)
def wrapped(*args, **kwargs):
cache_key = flask.request.path.encode('utf-8')
return _rest_cache_caller(f, cache_key, *args, **kwargs)
return wrapped
def rest_team_cache(f_or_key):
"""Mark a function for per-team caching."""
override_cache_key = None
def wrap_func(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
if flask.g.tid:
if override_cache_key:
cache_key = override_cache_key % (flask.g.tid)
else:
try:
cache_key = '%s/%s/%s' % (
f.im_class.__name__, f.__name__, flask.g.tid)
except AttributeError:
cache_key = '%s/%s' % (
f.__name__, flask.g.tid)
return _rest_cache_caller(f, cache_key, *args, **kwargs)
return f(*args, **kwargs)
return wrapped
if isinstance(f_or_key, str):
override_cache_key = f_or_key
if '%d' not in override_cache_key:
raise ValueError('No way to override the key per team!')
return wrap_func
return wrap_func(f_or_key)
def delete(key):
"""Delete cache entry."""
global_cache.delete(key)
def clear():
"""Flush global cache."""
global_cache.clear()
def delete_team(base_key):
"""Delete team-based cache entry."""
if not flask.g.tid:
return
global_cache.delete(base_key % flask.g.tid)
def _rest_cache_caller(f, cache_key, *args, **kwargs):
value = global_cache.get(cache_key)
if value:
try:
return _rest_add_cache_header(json.loads(value), True)
except ValueError:
pass
value = f(*args, **kwargs)
try:
# TODO: only cache on success
global_cache.set(cache_key, json.dumps(value))
except TypeError:
pass
return _rest_add_cache_header(value)
def _rest_add_cache_header(rv, hit=False):
# TODO: check status codes?
headers = {'X-Cache-Hit': str(hit)}
if isinstance(rv, str):
return (rv, 200, headers)
if isinstance(rv, tuple):
if len(rv) == 1:
return (rv[0], 200, headers)
if len(rv) == 2:
return (rv[0], rv[1], headers)
if len(rv) == 3:
if rv[2] is None:
return (rv[0], rv[1], headers)
if isinstance(rv[2], dict):
rv[2].update(headers)
return rv
if isinstance(rv, (list, dict)):
return rv, 200, headers
# TODO: might need to support Response objects
return rv
================================================
FILE: scoreboard/config_defaults.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
class Defaults(object):
ATTACHMENT_BACKEND = 'file://attachments'
COUNT_QUERIES = False
CSP_POLICY = None
CWD = os.path.dirname(os.path.realpath(__file__))
DEBUG = False
EXTEND_CSP_POLICY = None
ERROR_404_HELP = False
FIRST_BLOOD = 0
FIRST_BLOOD_MIN = 0
GAME_TIME = (None, None)
INVITE_KEY = None
LOGIN_METHOD = 'local'
MAIL_FROM = None
MAIL_FROM_NAME = None
MAIL_HOST = 'localhost'
NEWS_POLL_INTERVAL = 60000
PROOF_OF_WORK_BITS = 0
RULES = '/rules'
SCOREBOARD_ZEROS = True
SCORING = 'plain'
SECRET_KEY = None
TEAM_SECRET_KEY = None
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SQLALCHEMY_TRACK_MODIFICATIONS = True
SESSION_EXPIRATION_SECONDS = 60 * 60
SYSTEM_NAME = 'root'
TEAMS = True
TEASE_HIDDEN = True
TITLE = 'Scoreboard'
SUBMIT_AFTER_END = True
================================================
FILE: scoreboard/context.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import time
import flask
from sqlalchemy import event
from scoreboard import main
from scoreboard import models
from scoreboard import utils
app = main.get_app()
DEFAULT_CSP_POLICY = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-eval'", # Needed for Charts.js
],
'img-src': [
"'self'",
'data:',
],
'object-src': ["'none'"],
'font-src': [
"'self'",
'fonts.gstatic.com',
],
'style-src': [
"'self'",
'fonts.googleapis.com',
"'unsafe-inline'", # Needed for Charts.js
],
}
_CSP_POLICY_STRING = None
def get_csp_policy():
global _CSP_POLICY_STRING
if _CSP_POLICY_STRING is not None:
return _CSP_POLICY_STRING
if app.config.get('CSP_POLICY'):
policy = app.config.get('CSP_POLICY')
elif app.config.get('EXTEND_CSP_POLICY'):
policy = collections.defaultdict(list)
for k, v in DEFAULT_CSP_POLICY.items():
policy[k] = v
for k, v in app.config.get('EXTEND_CSP_POLICY').items():
policy[k].extend(v)
else:
policy = DEFAULT_CSP_POLICY
components = []
for k, v in policy.items():
sources = ' '.join(v)
components.append(k + ' ' + sources)
_CSP_POLICY_STRING = '; '.join(components)
return _CSP_POLICY_STRING
# Setup flask.g
@app.before_request
def load_globals():
"""Prepopulate flask.g.* with properties."""
try:
del flask.g.user
except AttributeError:
pass
try:
del flask.g.team
except AttributeError:
pass
if load_apikey():
return
if (app.config.get('SESSION_EXPIRATION_SECONDS') and
flask.session.get('expires') and
flask.session.get('expires') < time.time()):
flask.session.clear()
flask.g.uid = flask.session.get('user')
flask.g.tid = flask.session.get('team')
flask.g.admin = flask.session.get('admin') or False
def load_apikey():
"""Load flask.g.user, flask.g.uid from an API key."""
try:
key = flask.request.headers.get('X-SCOREBOARD-API-KEY')
if not key or len(key) != 32:
return
user = models.User.get_by_api_key(key)
if not user:
return
flask.g.user = user
flask.g.uid = user.uid
flask.g.admin = user.admin
flask.g.tid = None
return True
except Exception:
# Don't want any API key problems to block requests
pass
# Add headers to responses
@app.after_request
def add_headers(response):
"""Add security-related headers to all outgoing responses."""
h = response.headers
h.setdefault('Content-Security-Policy', get_csp_policy())
h.setdefault('X-Frame-Options', 'DENY')
h.add('X-XSS-Protection', '1', mode='block')
return response
@app.context_processor
def util_contexts():
return dict(gametime=utils.GameTime)
_query_count = 0
if app.config.get('COUNT_QUERIES'):
@event.listens_for(models.db.engine, 'before_cursor_execute')
def receive_before_cursor_execute(
conn, cursor, statement, parameters, context, executemany):
global _query_count
_query_count += 1
@app.after_request
def count_queries(response):
global _query_count
if _query_count > 0:
app.logger.info('Request issued %d queries.', _query_count)
_query_count = 0
return response
def ensure_setup():
if not app:
raise RuntimeError('Invalid app setup.')
================================================
FILE: scoreboard/controllers.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import flask
import re
from sqlalchemy import exc
import urllib
from scoreboard import errors
from scoreboard import mail
from scoreboard import main
from scoreboard import models
from scoreboard import utils
from scoreboard import validators
# Need to handle Python 2 and 3.
try:
urllib_quote = urllib.quote
except AttributeError:
import urllib.parse
urllib_quote = urllib.parse.quote
app = main.get_app()
def register_user(email, nick, password, team_id=None,
team_name=None, team_code=None):
"""Registers a player.
Arguments:
email: User's email
nick: User's nick
password: Player's password
team_id: Id# of team, or None to create new team.
team_name: Name of new team.
team_code: Validation code to join team.
"""
if not re.match(r'[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+\.[a-zA-Z]+$',
email):
raise errors.ValidationError('Invalid email address.')
# TODO: Sanitize other fields
first = models.User.query.count() == 0
if not first and app.config.get('TEAMS'):
if team_id == 'new':
try:
app.logger.info('Creating new team %s for user %s',
team_name, nick)
team = models.Team.create(team_name)
except exc.IntegrityError:
models.db.session.rollback()
raise errors.ValidationError('Team already exists!')
else:
team = models.Team.query.get(int(team_id))
if not team or team_code.lower() != team.code.lower():
raise errors.ValidationError(
'Invalid team selection or team code.')
else:
team = None
try:
if not team and not first:
team = models.Team.create(nick)
user = models.User.create(email, nick, password, team=team)
models.commit()
except exc.IntegrityError:
models.db.session.rollback()
if models.User.get_by_email(email):
raise errors.ValidationError('Duplicate email address.')
if models.User.get_by_nick(nick):
raise errors.ValidationError('Duplicate nick.')
if team_name and models.Team.get_by_name(team_name):
raise errors.ValidationError('Duplicate team name.')
raise errors.ValidationError('Unknown integrity error.')
if not user.admin:
models.ScoreHistory.add_entry(team)
models.commit()
app.logger.info('User %s <%s> registered from IP %s.',
nick, email, flask.request.remote_addr)
return user
@utils.require_not_started
def change_user_team(uid, team_tid, code):
"""Provide an interface for changing a user's team"""
team = models.Team.query.get_or_404(team_tid)
user = models.User.query.get_or_404(uid)
old_team = user.team
if code.lower() != team.code.lower():
raise errors.ValidationError('Invalid team selection or team code')
if team.tid == user.team_tid:
raise errors.ValidationError('Changing to same team')
app.logger.info('User %s switched to team %s from %s' %
(user.nick, team.name, old_team.name))
user.team = team
user.team_tid = team_tid
flask.session['team'] = team_tid
if old_team.players.count() == 0 and len(old_team.answers) == 0:
app.logger.info('Removing team %s due to lack of players' %
old_team.name)
models.db.session.delete(old_team)
models.commit()
@utils.require_submittable
def submit_answer(cid, answer, token):
"""Submits an answer.
Args:
cid: The ID of the challenge.
answer: The answer to check.
token: Provided proof of work token.
Returns:
Number of points awarded for answer.
"""
correct = 'WRONG'
nbits = app.config.get('PROOF_OF_WORK_BITS', 0)
if nbits and not utils.validate_proof_of_work(answer, token, nbits):
raise errors.InvalidAnswerError('Bad proof of work token!')
team = models.Team.current()
if not team:
raise errors.AccessDeniedError('No team!')
try:
challenge = models.Challenge.query.get(cid)
if not challenge.unlocked_for_team(team):
raise errors.AccessDeniedError('Challenge is locked!')
validator = validators.GetValidatorForChallenge(challenge)
if validator.validate_answer(answer, team):
points = save_team_answer(challenge, team, answer)
if utils.GameTime.over():
correct = 'CORRECT (Game Over)'
else:
correct = 'CORRECT'
return points
else:
raise errors.InvalidAnswerError('Really? Haha no....')
except (errors.IntegrityError, errors.FlushError) as exc:
app.logger.exception('Error saving flag: %s', exc)
models.db.session.rollback()
raise
finally:
user = models.User.current()
app.challenge_log.info(
'Player %s <%s>(%d)/Team %s(%d) submitted '
'"%s" for Challenge %s<%d>: %s',
user.nick, user.email, user.uid, team.name, team.tid, answer,
challenge.name, challenge.cid, correct)
@utils.require_submittable
def save_team_answer(challenge, team, answer):
"""Create the answer entry and update the scores."""
ans = models.Answer.create(challenge, team, answer)
models.commit()
team.last_solve = datetime.datetime.utcnow()
challenge.update_answers(exclude_team=team)
if utils.GameTime.over():
return 0
team.score += ans.current_points
models.ScoreHistory.add_entry(team)
models.commit()
return ans.current_points
def test_answer(cid, answer):
"""Tests an answer, returns Truthiness of answer."""
try:
challenge = models.Challenge.query.get(cid)
validator = validators.GetValidatorForChallenge(challenge)
return validator.validate_answer(answer, None)
except errors.IntegrityError:
return False
def offer_password_reset(user):
token = user.get_token().decode('utf-8')
token_url = utils.absolute_url('/pwreset/%s/%s' %
(urllib_quote(user.email), token))
message = flask.render_template(
'pwreset.eml', token_url=token_url,
user=user, ip=flask.request.remote_addr, config=app.config)
subject = '%s Password Reset' % app.config.get('TITLE')
try:
mail.send(message, subject, user.email, to_name=user.nick)
except mail.MailFailure as ex:
raise errors.ServerError('Could not send mail: ' + str(ex))
================================================
FILE: scoreboard/csrfutil.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import binascii
import flask
import functools
import hashlib
import hmac
import jinja2
import struct
import time
from scoreboard import main
from scoreboard import utils
app = main.get_app()
b64_vals = utils.to_bytes('_-')
def _get_csrf_token(user=None, expires=None):
user = user or flask.session.get('user', flask.request.remote_addr)
expires = expires or int(time.time()) + 60 * 60 * 24
expires_bytes = struct.pack('<I', expires)
msg = utils.to_bytes('%s:' % user) + expires_bytes
key = utils.to_bytes(app.config.get('SECRET_KEY'))
sig = hmac.new(key, msg, hashlib.sha256).digest()
return expires_bytes + sig
def get_csrf_token(*args, **kwargs):
"""Returns a URL-safe base64 CSRF token."""
return base64.b64encode(utils.to_bytes(
_get_csrf_token(*args, **kwargs)), b64_vals).decode('utf-8')
def verify_csrf_token(token, user=None):
"""Verify a token for a user."""
try:
token = base64.b64decode(str(token), b64_vals)
except (binascii.Error, TypeError):
return False
expires = struct.unpack('<I', token[:4])[0]
if expires < time.time():
return False
return token == _get_csrf_token(user, expires)
def csrf_protect(f):
"""Decorator to add CSRF protection to a request handler."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
if flask.request.method == 'POST':
token = flask.request.values.get('csrftoken')
if not token or not verify_csrf_token(token):
app.logger.warning('CSRF Validation Failed.')
flask.abort(403)
return f(*args, **kwargs)
return wrapper
def get_csrf_field(*args, **kwargs):
"""Render a CSRF field."""
token = get_csrf_token(*args, **kwargs)
field = jinja2.Markup(
'<input type="hidden" name="csrftoken" value="%s" />')
return field % token
@app.before_request
def csrf_protection_request():
"""Add CSRF Protection to all non-GET/non-HEAD requests."""
if flask.request.method in ('GET', 'HEAD'):
return
if app.config.get('TESTING'):
return
header = flask.request.headers.get('X-XSRF-TOKEN')
token = header or flask.request.values.get('csrftoken')
if not token or not verify_csrf_token(token):
app.logger.warning('CSRF Validation Failed')
flask.abort(403)
@app.after_request
def add_csrf_protection(resp):
"""Add the XSRF-TOKEN cookie to all outgoing requests."""
resp.set_cookie('XSRF-TOKEN', get_csrf_token())
return resp
@app.context_processor
def csrf_context_processor():
"""Add CSRF token and field to all rendering contexts."""
return {
'csrftoken': get_csrf_token,
'csrffield': get_csrf_field,
}
================================================
FILE: scoreboard/errors.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Custom error classes plus access to SQLAlchemy exceptions
from werkzeug import exceptions
from sqlalchemy.exc import * # noqa: F401,F403
from sqlalchemy.orm.exc import * # noqa: F401,F403
class _MessageException(exceptions.HTTPException):
"""Message with JSON exception."""
default_message = 'Error'
def __init__(self, msg=None):
msg = msg or self.default_message
super(_MessageException, self).__init__()
self.data = {'message': msg}
class AccessDeniedError(_MessageException):
"""No access to the resource."""
code = 403
class ValidationError(_MessageException):
"""Error during input validation."""
code = 400
class InvalidAnswerError(AccessDeniedError):
"""Submitting the wrong answer."""
default_message = 'Ha ha ha... No.'
class LoginError(AccessDeniedError):
"""Failing to login."""
default_message = 'Invalid username/password.'
class ServerError(_MessageException):
code = 500
================================================
FILE: scoreboard/logger.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import flask
class Formatter(logging.Formatter):
"""Custom formatter to handle application logging.
This formatter adds a "client" attribute that will log the user and client
information.
"""
def format(self, record):
if flask.request:
user = (('UID<%d>' % flask.g.uid)
if 'uid' in flask.g and flask.g.uid else '-')
record.client = "[{}/{}]".format(flask.request.remote_addr, user)
else:
record.client = ""
return super(Formatter, self).format(record)
================================================
FILE: scoreboard/mail.py
================================================
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from email.mime import text
import email.utils
import smtplib
import socket
import mailjet_rest
from scoreboard import main
app = main.get_app()
class MailFailure(Exception):
"""Inability to send mail."""
pass
def send(message, subject, to, to_name=None, sender=None, sender_name=None):
"""Send an email."""
sender = sender or app.config.get('MAIL_FROM')
sender_name = sender_name or app.config.get('MAIL_FROM_NAME') or ''
mail_provider = app.config.get('MAIL_PROVIDER')
if mail_provider is None:
app.logger.error('No MAIL_PROVIDER configured!')
raise MailFailure('No MAIL_PROVIDER configured!')
elif mail_provider == 'smtp':
_send_smtp(message, subject, to, to_name, sender, sender_name)
elif mail_provider == 'mailjet':
_send_mailjet(message, subject, to, to_name, sender, sender_name)
else:
app.logger.error('Invalid MAIL_PROVIDER configured!')
raise MailFailure('Invalid MAIL_PROVIDER configured!')
def _send_smtp(message, subject, to, to_name, sender, sender_name):
"""SMTP implementation of sending email."""
host = app.config.get('MAIL_HOST')
if not host:
raise MailFailure('SMTP Server Not Configured')
try:
server = smtplib.SMTP(host)
except (smtplib.SMTPConnectError, socket.error) as ex:
app.logger.error('Unable to send mail: %s', str(ex))
raise MailFailure('Error connecting to SMTP server.')
msg = text.MIMEText(message)
msg['Subject'] = subject
msg['To'] = email.utils.formataddr((to_name, to))
msg['From'] = email.utils.formataddr((sender_name, sender))
try:
if app.debug:
server.set_debuglevel(True)
server.sendmail(sender, [to], msg.as_string())
except (smtplib.SMTPException, socket.error) as ex:
app.logger.error('Unable to send mail: %s', str(ex))
raise MailFailure('Error sending mail to SMTP server.')
finally:
try:
server.quit()
except smtplib.SMTPException:
pass
def _send_mailjet(message, subject, to, to_name, sender, sender_name):
"""Mailjet implementation of sending email."""
api_key = app.config.get('MJ_APIKEY_PUBLIC')
api_secret = app.config.get('MJ_APIKEY_PRIVATE')
if not api_key or not api_secret:
app.logger.error('Missing MJ_APIKEY_PUBLIC/MJ_APIKEY_PRIVATE!')
return
# Note the data structures we use are api v3.1
client = mailjet_rest.Client(
auth=(api_key, api_secret),
api_url='https://api.mailjet.com/',
version='v3.1')
from_obj = {
"Email": sender,
}
if sender_name:
from_obj["Name"] = sender_name
to_obj = [{
"Email": to,
}]
if to_name:
to_obj[0]["Name"] = to_name
message = {
"From": from_obj,
"To": to_obj,
"Subject": subject,
"TextPart": message,
}
result = client.send.create(data={'Messages': [message]})
if result.status_code != 200:
app.logger.error(
'Error sending via mailjet: (%d) %r',
result.status_code, result.text)
raise MailFailure('Error sending via mailjet!')
try:
j = result.json()
except Exception:
app.logger.error('Error sending via mailjet: %r', result.text)
raise MailFailure('Error sending via mailjet!')
if j['Messages'][0]['Status'] != 'success':
app.logger.error('Error sending via mailjet: %r', j)
raise MailFailure('Error sending via mailjet!')
================================================
FILE: scoreboard/main.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import flask
from flask import logging as flask_logging
import logging
import os
from werkzeug import exceptions
from werkzeug import utils as werkzeug_utils
import flask_scss
from scoreboard import logger
# Singleton app instance
_app_singleton = None
def on_appengine():
"""Returns true if we're running on AppEngine."""
runtime = os.environ.get('SERVER_SOFTWARE', '')
gae_env = os.environ.get('GAE_ENV', '')
return ((gae_env != '') or
runtime.startswith('Development/') or
runtime.startswith('Google App Engine/'))
def create_app(config=None):
app = flask.Flask(
'scoreboard',
static_folder='../static',
template_folder='../templates',
)
app.config.from_object('scoreboard.config_defaults.Defaults')
if config is not None:
app.config.update(**config)
if not on_appengine():
# Configure Scss to watch the files
scss_compiler = flask_scss.Scss(
app, static_dir='static/css', asset_dir='static/scss')
scss_compiler.update_scss()
for c in exceptions.default_exceptions.keys():
app.register_error_handler(c, api_error_handler)
setup_logging(app)
return app
def load_config_file(app=None):
app = app or get_app()
try:
app.config.from_object('config')
except werkzeug_utils.ImportStringError:
pass
app.config.from_envvar('SCOREBOARD_CONFIG', silent=True)
setup_logging(app) # reset logs
def setup_logging(app):
log_formatter = logger.Formatter(
'%(asctime)s %(levelname)8s [%(filename)s:%(lineno)d] '
'%(client)s %(message)s')
# log to files unless on AppEngine
if not on_appengine():
# Main logger
if not (app.debug or app.config.get('TESTING')):
handler = logging.FileHandler(
app.config.get('LOGFILE', '/tmp/scoreboard.wsgi.log'))
handler.setLevel(logging.INFO)
handler.setFormatter(log_formatter)
app.logger.addHandler(handler)
else:
app.logger.handlers[0].setFormatter(log_formatter)
# Challenge logger
handler = logging.FileHandler(
app.config.get('CHALLENGELOG', '/tmp/scoreboard.challenge.log'))
handler.setLevel(logging.INFO)
handler.setFormatter(logger.Formatter(
'%(asctime)s %(client)s %(message)s'))
local_logger = logging.getLogger('scoreboard')
local_logger.addHandler(handler)
app.challenge_log = local_logger
else:
app.challenge_log = app.logger
try:
import google.cloud.logging
from google.cloud.logging import handlers
client = google.cloud.logging.Client()
client.setup_logging()
handler = handlers.CloudLoggingHandler(client)
app.logger.addHandler(handler)
handler.setLevel(logging.INFO)
return app
except ImportError as ex:
logging.error('Failed setting up logging: %s', ex)
if not app.logger.handlers:
app.logger.addHandler(flask_logging.default_handler)
app.logger.handlers[0].setFormatter(log_formatter)
logging.getLogger().handlers[0].setFormatter(log_formatter)
return app
def api_error_handler(ex):
"""Handle errors as appropriate depending on path."""
error_titles = {
401: 'Unauthorized',
403: 'Forbidden',
500: 'Internal Error',
}
try:
status_code = ex.code
except AttributeError:
status_code = 500
if flask.request.path.startswith('/api/'):
app = get_app()
app.logger.error(str(ex))
if app.config.get('DEBUG', False):
resp = flask.jsonify(message=str(ex))
else:
resp = flask.jsonify(message='Internal Server Error')
resp.status_code = status_code
return resp
return flask.make_response(
flask.render_template(
'error.html', exc=ex,
title=error_titles.get(status_code, 'Error')),
status_code)
def get_app():
global _app_singleton
if _app_singleton is None:
_app_singleton = create_app()
return _app_singleton
================================================
FILE: scoreboard/models.py
================================================
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import datetime
import flask
import flask_sqlalchemy
import hashlib
import hmac
import json
import logging
import math
import os
import pbkdf2
import re
import sqlalchemy as sqlalchemy_base
import time
from sqlalchemy import exc
from sqlalchemy import func
from sqlalchemy import orm
from sqlalchemy.ext import hybrid
from scoreboard import attachments
from scoreboard import errors
from scoreboard import main
from scoreboard import utils
app = main.get_app()
db = flask_sqlalchemy.SQLAlchemy(app)
class Team(db.Model):
"""A Team of Players (Team of 1 if not using Teams)."""
tid = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), unique=True, nullable=False, index=True)
score = db.Column(db.Integer, default=0) # Denormalized
last_solve = db.Column(db.DateTime, nullable=True)
players = db.relationship(
'User', backref=db.backref('team', lazy='joined'), lazy='dynamic')
answers = db.relationship('Answer', backref='team', lazy='select',
cascade='delete')
score_history = db.relationship(
'ScoreHistory',
backref=db.backref('team', lazy='joined'),
lazy='select', cascade='delete')
def __repr__(self):
return '<Team: %s>' % self.name.encode('utf-8')
def __str__(self):
return self.name
@property
def code(self):
secret_key = (app.config.get('TEAM_SECRET_KEY') or
app.config.get('SECRET_KEY'))
return hmac.new(utils.to_bytes(secret_key),
self.name.encode('utf-8'),
hashlib.sha256).hexdigest()[:12]
@property
def solves(self):
return len(self.answers)
def update_score(self):
old_score = self.score
self.score = sum(a.current_points for a in self.answers)
if self.score != old_score:
# Add score history entry
if not getattr(self, '_pending_sh', False):
ScoreHistory.add_entry(self)
self._pending_sh = True
def can_access(self, user=None):
"""Check if player can access team."""
user = user or User.current()
if user.admin:
return True
return user.team == self
@classmethod
def create(cls, name):
team = cls()
db.session.add(team)
team.name = name
return team
@classmethod
def get_by_name(cls, name):
try:
return cls.query.filter_by(name=name).one()
except exc.InvalidRequestError:
return None
@classmethod
def enumerate(cls, with_history=False, above_zero=False):
if with_history:
base = cls.query.options(orm.joinedload(cls.score_history))
else:
base = cls.query
if above_zero:
base = base.filter(cls.score > 0)
sorting = base.order_by(cls.score.desc(), cls.last_solve)
return enumerate(sorting.all(), 1)
@classmethod
def all(cls, with_history=True):
if with_history:
base = cls.query.options(orm.joinedload(cls.score_history))
else:
base = cls.query
base = base.options(orm.joinedload(cls.answers))
base = base.order_by(cls.name)
return base.all()
@classmethod
def current(cls):
try:
return flask.g.team
except AttributeError:
user = User.current()
if user:
flask.g.team = user.team
return user.team
else:
flask.g.team = None
class ScoreHistory(db.Model):
team_tid = db.Column(db.Integer, db.ForeignKey('team.tid'), nullable=False,
primary_key=True)
when = db.Column(db.DateTime, nullable=False, primary_key=True,
default=datetime.datetime.utcnow)
score = db.Column(db.Integer, default=0, nullable=False)
@classmethod
def add_entry(cls, team):
entry = cls()
entry.team = team
entry.score = team.score
db.session.merge(entry)
class User(db.Model):
"""A single User for login. Player or admin."""
uid = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
nick = db.Column(db.String(80), unique=True, nullable=False, index=True)
pwhash = db.Column(db.String(48)) # pbkdf2.crypt == 48 bytes
admin = db.Column(db.Boolean, default=False, index=True)
team_tid = db.Column(db.Integer, db.ForeignKey('team.tid'))
create_ip = db.Column(db.String(45)) # max 45 bytes for IPv6
last_login_ip = db.Column(db.String(45))
api_key = db.Column(db.String(32), index=True)
api_key_updated = db.Column(db.DateTime)
def set_password(self, password):
self.pwhash = pbkdf2.crypt(password)
def __repr__(self):
return '<User: %s <%s>>' % (self.nick.encode('utf-8'), self.email)
def __str__(self):
return self.nick
def promote(self):
"""Promote a user to admin."""
empty_team = self.team and set(self.team.players.all()) == set([self])
if self.team and len(self.team.answers):
raise AssertionError(
'Cannot promote player whose team has solved answers!')
self.admin = True
team = self.team
self.team = None
if empty_team:
db.session.delete(team)
def get_token(self, token_type='pwreset', expires=None):
"""Generate a user-specific token."""
expires = expires or int(time.time()) + 7200 # 2 hours
token_plain = '%d:%d:%s:%s' % (
self.uid, expires, token_type, self.pwhash)
mac = hmac.new(
utils.to_bytes(app.config.get('SECRET_KEY')),
utils.to_bytes(token_plain),
hashlib.sha1).digest()
token = utils.to_bytes('%d:' % expires) + mac
return base64.urlsafe_b64encode(token)
def verify_token(self, token, token_type='pwreset'):
"""Verify a user-specific token."""
token = utils.to_bytes(token)
try:
decoded = base64.urlsafe_b64decode(token)
expires, mac = decoded.split(b':', 1)
except ValueError:
raise errors.ValidationError('Invalid token.')
if float(expires) < time.time():
raise errors.ValidationError('Expired token.')
expected = self.get_token(token_type=token_type, expires=int(expires))
if not utils.compare_digest(expected, token):
raise errors.ValidationError('Invalid token.')
return True
def reset_api_key(self):
"""Reset a user's api key."""
new_key = os.urandom(16)
try:
self.api_key = new_key.hex() # Python 3
except AttributeError:
self.api_key = new_key.encode('hex') # Python 2
self.api_key_update = datetime.datetime.now()
@classmethod
def get_by_email(cls, email):
try:
return cls.query.filter_by(email=email).one()
except exc.InvalidRequestError:
return None
@classmethod
def get_by_nick(cls, nick):
try:
return cls.query.filter_by(nick=nick).one()
except exc.InvalidRequestError:
return None
@classmethod
def get_by_api_key(cls, token):
if not token:
return None
try:
return cls.query.filter_by(api_key=token).one()
except exc.InvalidRequestError:
return None
@classmethod
def login_user(cls, email, password):
try:
user = cls.query.filter_by(email=email).one()
except exc.InvalidRequestError:
return None
if pbkdf2.crypt(password, user.pwhash) == user.pwhash:
if flask.has_request_context():
user.last_login_ip = flask.request.remote_addr
db.session.commit()
return user
return None
@classmethod
def create(cls, email, nick, password, team=None):
first_user = True if not cls.query.count() else False
user = cls()
db.session.add(user)
user.email = email
user.nick = nick
user.set_password(password)
if not first_user:
user.team = team
else:
user.promote()
if flask.has_request_context():
user.create_ip = flask.request.remote_addr
return user
@classmethod
def current(cls):
try:
return flask.g.user
except AttributeError:
uid = flask.session.get('user')
if uid is not None:
# For some reason, .get() does not join!
user = cls.query.options(orm.joinedload(cls.team)).filter(
cls.uid == uid).first()
flask.g.user = user
flask.g.team = user.team
if user:
# Bump expiration time on session
utils.session_for_user(user)
return user
@classmethod
def all(cls):
return cls.query.order_by(
cls.admin.desc(),
cls.nick).all()
tag_challenge_association = db.Table(
'tag_chall_association', db.Model.metadata,
db.Column('challenge_cid', db.BigInteger,
db.ForeignKey('challenge.cid')),
db.Column('tag_tagslug', db.String(100),
db.ForeignKey('tag.tagslug')))
class Tag(db.Model):
"""A Tag to be Applied to Challenges"""
tagslug = db.Column(db.String(100), unique=True, primary_key=True,
nullable=False, index=True)
name = db.Column(db.String(100), unique=True, nullable=False)
description = db.Column(db.Text)
challenges = db.relationship('Challenge',
backref=db.backref('tags', lazy='joined'),
secondary='tag_chall_association',
lazy='joined')
def __repr__(self):
return '<Tag: %s/%s>' % (self.tagslug, self.name)
def slugify(self):
self.tagslug = '-'.join(w.lower() for w in re.split(r'\W+', self.name))
@classmethod
def create(cls, name, description):
tag = cls()
tag.name = name
tag.description = description
tag.slugify()
db.session.add(tag)
return tag
def get_challenges(self, unlocked_only=True, sort=True, force_query=False):
if (force_query or
'challenges' in sqlalchemy_base.inspect(self).unloaded):
return self._get_challenges_query(
unlocked_only=unlocked_only, sort=sort)
return self._get_challenges_cached(
unlocked_only=unlocked_only, sort=sort)
def _get_challenges_cached(self, unlocked_only=True, sort=True):
challenges = self.challenges
if unlocked_only:
challenges = [c for c in challenges if c.unlocked]
if sort:
challenges = sorted(challenges, key=lambda c: c.weight)
return challenges
def _get_challenges_query(self, unlocked_only=True, sort=True):
challenges = Challenge.query.filter(
Challenge.tags.any(tagslug=self.tagslug))
if unlocked_only:
unlocked_identity = True
challenges = challenges.filter(
Challenge.unlocked == unlocked_identity)
if not sort:
return challenges
return challenges.order_by(Challenge.weight)
class Challenge(db.Model):
"""A single challenge to be played."""
cid = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
points = db.Column(db.Integer, nullable=False)
min_points = db.Column(db.Integer, nullable=True)
validator = db.Column(db.String(24), nullable=False,
default='static_pbkdf2')
answer_hash = db.Column(db.String(48)) # Protect answers
unlocked = db.Column(db.Boolean, default=False)
weight = db.Column(db.Integer, nullable=False) # Order for display
prerequisite = db.Column(db.Text, nullable=False) # Prerequisite Metadata
cur_points = db.Column(db.Integer, nullable=True)
answers = db.relationship('Answer',
backref=db.backref('challenge', lazy='joined'),
lazy='select')
def __repr__(self):
return '<Challenge: %d/%s>' % (self.cid, self.name)
def is_answered(self, team=None, answers=None):
if team is None:
team = Team.current()
if not team:
return False
if answers is not None:
for a in answers:
if a.team_tid == team.tid and a.challenge_cid == self.cid:
return True
return False
return bool(Answer.query.filter(Answer.challenge == self,
Answer.team == team).count())
@hybrid.hybrid_property
def solves(self):
try:
return self._solves
except AttributeError:
self._solves = len(self.answers)
return self._solves
@solves.expression
def solves(cls):
return func.count(cls.answers)
@property
def answered(self):
if not Team.current():
return False
return self.is_answered(answers=Team.current().answers)
@property
def teaser(self):
if not app.config.get('TEASE_HIDDEN'):
return False
if not Team.current():
return False
return not self.unlocked_for_team(Team.current())
@property
def current_points(self):
mode = app.config.get('SCORING', 'plain')
value = self.points
if mode == 'plain':
self.cur_points = value
elif mode == 'progressive':
speed = app.config.get('SCORING_SPEED', 12)
min_points = 0 if self.min_points is None else self.min_points
self.cur_points = self.log_score(
value, min_points, speed, self.solves)
return self.cur_points
@staticmethod
def log_score(max_points, min_points, midpoint, solves):
# Algorithm designed by symmetric
# logit(u, l, m, s, x) =
# (u - l) * ((1.0 / (1.0 + exp((1.0/s) * (x - m)))) /
# (1.0 / (1.0 + exp((1.0/s) * (1 - m))))) + l
if solves == 0:
return max_points
def log_func(midpoint, solves):
spread = midpoint / 3.0
delta = solves - midpoint
return (
1.0 / (1.0 + math.exp((1.0 / spread) * delta)))
max_delta = (max_points - min_points)
base_point = log_func(midpoint, 1.0)
cur_point = log_func(midpoint, solves)
return math.ceil(max_delta * cur_point / base_point + min_points)
def unlocked_for_team(self, team):
"""Checks if prerequisites are met for this team."""
if not self.unlocked:
return False
if not self.prerequisite:
return True
try:
prereq = json.loads(self.prerequisite)
except ValueError:
logging.error('Unable to parse prerequisite data for challenge %d',
self.cid)
return False
if prereq['type'] == 'None':
return True
if not team:
return False
try:
eval_func = getattr(self, 'prereq_' + prereq['type'])
except AttributeError:
logging.error(
'Could not find prerequisite function for challenge %d',
self.cid)
return False
return eval_func(prereq, team)
def prereq_solved(self, prereq, team):
"""Require that another challenge be solved first."""
chall = Challenge.query.get(int(prereq['challenge']))
if not chall:
logging.error('Challenge %d prerequisite depends on '
'non-existent challenge %d.', self.cid,
int(prereq['challenge']))
return False
return chall.is_answered(team=team, answers=team.answers)
@classmethod
def create(cls, name, description, points, answer, unlocked=False,
validator='static_pbkdf2'):
challenge = cls()
challenge.name = name
challenge.description = description
challenge.cid = utils.generate_id()
challenge.points = points
challenge.answer_hash = answer
challenge.unlocked = unlocked
challenge.validator = validator
weight = db.session.query(db.func.max(Challenge.weight)).scalar()
challenge.weight = (weight + 1) if weight else 1
challenge.prerequisite = ''
db.session.add(challenge)
return challenge
def add_tags(self, tags):
for tag in tags:
self.tags.append(tag)
def delete(self):
db.session.delete(self)
def set_attachments(self, attachments):
aid_set = set()
old_attachments = list(self.attachments)
for a in attachments:
aid_set.add(a['aid'])
attachment = Attachment.query.get(a['aid'])
if not attachment:
logging.warning(
'Trying to add attachment %s that does not exist: %s' %
(a['filename'], a['aid']))
self.attachments.append(attachment)
for a in old_attachments:
if a.aid not in aid_set:
self.attachments.remove(a)
def set_prerequisite(self, prerequisite):
if not prerequisite:
self.prerequisite = ''
return
if 'type' in prerequisite and prerequisite['type'] == 'None':
self.prerequisite = ''
else:
self.prerequisite = json.dumps(prerequisite)
def set_tags(self, tags):
tag_set = set()
old_tags = list(self.tags)
for t in tags:
tag_set.add(t['tagslug'])
tag = Tag.query.get(t['tagslug'])
if tag:
self.tags.append(tag)
else:
app.logger.warning('Skipping tag %s which does not exist' %
t['tagslug'])
for t in old_tags:
if t.tagslug not in tag_set:
self.tags.remove(t)
def update_answers(self, exclude_team=None):
"""Update answers for variable scoring."""
mode = app.config.get('SCORING')
if mode == 'plain':
return
if mode == 'progressive':
for a in self.answers:
if a.team == exclude_team:
continue
a.team.update_score()
@classmethod
def get_joined_query(cls):
"""Get a prejoined-query with answers and teams."""
return cls.query.options(
orm.joinedload(cls.answers).joinedload(Answer.team))
attach_challenge_association = db.Table(
'attach_chall_association', db.Model.metadata,
db.Column(
'challenge_cid', db.BigInteger,
db.ForeignKey('challenge.cid')),
db.Column(
'attachment_aid', db.String(64),
db.ForeignKey('attachment.aid')))
class Attachment(db.Model):
"""Attachment to a challenge."""
aid = db.Column(db.String(64), primary_key=True)
filename = db.Column(db.String(100), nullable=False)
content_type = db.Column(db.String(100))
storage_path = db.Column(db.String(256))
challenges = db.relationship(
'Challenge', backref=db.backref('attachments', lazy='joined'),
secondary='attach_chall_association', lazy='joined')
def __str__(self):
return repr(self)
def __repr__(self):
return '<Attachment %s>' % self.aid
def delete(self, from_disk=True):
if from_disk:
try:
attachments.backend.delete(self)
except IOError as ex:
app.logger.exception("Couldn't delete: %s", str(ex))
db.session.delete(self)
def set_challenges(self, challenges):
cid_set = set()
old_challenges = list(self.challenges)
for a in challenges:
cid_set.add(a['cid'])
challenge = Challenge.query.get(a['cid'])
if not challenge:
app.logger.warning('No challenge found with cid %d' % a['cid'])
continue
self.challenges.append(challenge)
for a in old_challenges:
if a.cid not in cid_set:
self.challenges.remove(a)
@classmethod
def create(cls, aid, filename, content_type):
attachment = cls()
attachment.aid = aid
attachment.filename = filename
attachment.content_type = content_type
db.session.add(attachment)
return attachment
class Answer(db.Model):
"""Log a successfully submitted answer."""
challenge_cid = db.Column(
db.BigInteger, db.ForeignKey('challenge.cid'), primary_key=True)
team_tid = db.Column(
db.Integer, db.ForeignKey('team.tid'), primary_key=True)
timestamp = db.Column(db.DateTime)
answer_hash = db.Column(db.String(48)) # Store hash of team+answer
submit_ip = db.Column(db.String(45)) # Source IP for submission
first_blood = db.Column(db.Integer, default=0, nullable=False)
@classmethod
def create(cls, challenge, team, answer_text):
answer = cls()
answer.first_blood = 0
if not challenge.solves:
if app.config.get('FIRST_BLOOD_MIN', 0) <= challenge.points:
answer.first_blood = app.config.get('FIRST_BLOOD', 0)
answer.challenge = challenge
answer.team = team
answer.timestamp = datetime.datetime.utcnow()
if answer_text:
answer.answer_hash = pbkdf2.crypt(team.name + answer_text)
if flask.request:
answer.submit_ip = flask.request.remote_addr
db.session.add(answer)
# remove cache here
del challenge._solves
return answer
@property
def current_points(self):
if utils.GameTime.state(self.timestamp) == "AFTER":
return 0
return self.challenge.current_points + self.first_blood
class News(db.Model):
"""News updates & broadcasts."""
NEWS_TYPES = [
'Broadcast', # Admin broadcast
'Unicast', # Team-specific update
]
nid = db.Column(db.Integer, primary_key=True)
news_type = db.Column(db.Enum(*NEWS_TYPES), nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
author = db.Column(db.String(100))
message = db.Column(db.Text)
audience_team_tid = db.Column(db.Integer, db.ForeignKey('team.tid'))
audience_team = db.relationship('Team')
@classmethod
def broadcast(cls, author, message):
news = cls(
news_type='Broadcast',
author=author,
message=message)
db.session.add(news)
return news
@classmethod
def game_broadcast(cls, author=None, message=None):
if message is None:
raise ValueError('Missing message.')
author = author or app.config.get('SYSTEM_NAME')
if not utils.GameTime.open():
return
return cls.broadcast(author, message)
@classmethod
def unicast(cls, team, author, message):
news = cls(
news_type='Unicast',
author=author,
message=message)
if isinstance(team, Team):
news.audience_team = team
elif isinstance(team, int):
news.audience_team_tid = team
else:
raise ValueError('Invalid value for team.')
db.session.add(news)
return news
@classmethod
def for_team(cls, team, limit=10):
return cls.query.filter(
((cls.news_type != 'Unicast') |
(cls.audience_team == team))
).order_by(cls.timestamp.desc()).limit(limit)
@classmethod
def for_public(cls, limit=10):
return cls.query.filter(
cls.news_type != 'Unicast'
).order_by(cls.timestamp.desc()).limit(limit)
class Page(db.Model):
"""Represent static pages to be rendered with Markdown."""
path = db.Column(db.String(100), primary_key=True)
title = db.Column(db.String(100), nullable=False)
contents = db.Column(db.Text, nullable=False)
class NonceFlagUsed(db.Model):
"""Single-time used flags."""
challenge_cid = db.Column(db.BigInteger, db.ForeignKey('challenge.cid'),
primary_key=True)
nonce = db.Column(db.BigInteger, primary_key=True)
team_tid = db.Column(db.Integer, db.ForeignKey('team.tid'))
@classmethod
def create(cls, challenge, nonce, team):
entity = cls()
entity.challenge_cid = challenge.cid
entity.nonce = nonce
entity.team_tid = team.tid
db.session.add(entity)
# Shortcut for commiting
def commit():
db.session.commit()
================================================
FILE: scoreboard/rest.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import flask
import flask_restful
from flask_restful import fields
import json
import pytz
from scoreboard import attachments
from scoreboard import auth
from scoreboard import cache
from scoreboard import controllers
from scoreboard import context
from scoreboard import csrfutil
from scoreboard import errors
from scoreboard import main
from scoreboard import models
from scoreboard import utils
from scoreboard import validators
app = main.get_app()
api = flask_restful.Api(app)
context.ensure_setup()
# Custom fields
class ISO8601DateTime(fields.Raw):
"""Show datetimes as ISO8601."""
def format(self, value):
if value is None:
return None
if isinstance(value, (int, float)):
value = datetime.fromtimestamp(value)
if isinstance(value, (datetime.datetime, datetime.date)):
if getattr(value, 'tzinfo', True) is None:
value = value.replace(tzinfo=pytz.UTC)
return value.isoformat()
raise ValueError('Unable to convert %s to ISO8601.' % str(type(value)))
class PrerequisiteField(fields.Raw):
"""Prerequisite data."""
def format(self, value):
try:
data = json.loads(value)
except ValueError:
return {'type': 'None'}
return data
# Utility functions
@api.representation('application/json')
def output_json(data, code, headers=None):
"""Custom JSON output with JSONP buster."""
settings = {}
if app.debug:
settings['indent'] = 4
settings['sort_keys'] = True
dumped = json.dumps(data, **settings)
if not (headers and headers.pop('X-No-XSSI', None)):
dumped = ")]}',\n" + dumped + "\n"
resp = flask.make_response(dumped, code)
resp.headers.extend(headers or {})
return resp
def get_field(name, *args):
data = flask.request.get_json()
try:
return data[name]
except KeyError:
if args:
return args[0]
raise errors.ValidationError(
'Required field {} not given.'.format(name))
class User(flask_restful.Resource):
"""Wrap User model."""
decorators = [utils.login_required]
resource_fields = {
'uid': fields.Integer,
'email': fields.String,
'nick': fields.String,
'admin': fields.Boolean,
'team_tid': fields.Integer,
}
@flask_restful.marshal_with(resource_fields)
def get(self, user_id):
if not flask.g.uid == user_id and not flask.g.admin:
raise errors.AccessDeniedError('No access to that user.')
return models.User.query.get_or_404(user_id)
@flask_restful.marshal_with(resource_fields)
def put(self, user_id):
if not flask.g.uid == user_id and not flask.g.admin:
raise errors.AccessDeniedError('No access to that user.')
user = models.User.query.get_or_404(user_id)
data = flask.request.get_json()
if utils.is_admin() and 'admin' in data:
if data['admin'] and not user.admin:
try:
user.promote()
except AssertionError:
raise errors.ValidationError(
'Error promoting. Has player solved challenges?')
else:
user.admin = False
if data.get('password'):
user.set_password(data['password'])
if utils.is_admin():
user.nick = data['nick']
if not app.config.get('TEAMS') and user.team:
user.team.name = data['nick']
try:
models.commit()
except AssertionError:
raise errors.ValidationError(
'Error in updating user. Details are logged.')
return user
class UserList(flask_restful.Resource):
"""Registration and listing of users."""
resource_fields = {
'users': fields.Nested(User.resource_fields),
}
@utils.admin_required
@flask_restful.marshal_with(resource_fields)
def get(self):
return dict(users=models.User.all())
@flask_restful.marshal_with(User.resource_fields)
def post(self):
"""Register a new user."""
if utils.is_logged_in():
raise errors.ValidationError('Cannot register while logged in.')
data = flask.request.get_json()
if not data.get('nick', ''):
raise errors.ValidationError('Need a player nick.')
if (app.config.get('TEAMS') and
not data.get('team_name', '') and
not data.get('team_id', 0)):
app.logger.warning('User attempted to register without team.')
raise errors.ValidationError('Need a team name.')
if (app.config.get('INVITE_KEY') and
data.get('invite_key', '').strip() !=
app.config.get('INVITE_KEY')):
app.logger.warning(
'Attempted invite-only registration with invalid '
'invite key: %s', data.get('invite_key', ''))
raise errors.ValidationError('Invalid invite key!')
app.logger.debug('Passed registration validation for new user.')
user = auth.register(flask.request)
utils.session_for_user(user)
return user
class Team(flask_restful.Resource):
"""Manage single team."""
decorators = [utils.login_required]
history_fields = {
'when': ISO8601DateTime(),
'score': fields.Integer,
}
team_fields = {
'tid': fields.Integer,
'name': fields.String,
'score': fields.Integer,
'solves': fields.Integer,
}
solved_challenges = {
'cid': fields.Integer,
'name': fields.String,
'solved': ISO8601DateTime(),
'points': fields.Integer,
}
resource_fields = team_fields.copy()
resource_fields['players'] = fields.Nested(User.resource_fields)
resource_fields['score_history'] = fields.Nested(history_fields)
resource_fields['solved_challenges'] = fields.Nested(solved_challenges)
@flask_restful.marshal_with(resource_fields)
def get(self, team_id):
# TODO: this takes too many queries, fix to 1
team = models.Team.query.get_or_404(team_id)
return self._marshal_team(team, extended=True)
def _marshal_team(self, team, extended=False):
result = {}
for k in self.team_fields:
result[k] = getattr(team, k)
if extended:
challenges = []
for answer in team.answers:
challenges.append({
'solved': answer.timestamp,
'points': answer.current_points,
'name': answer.challenge.name,
'cid': answer.challenge_cid,
})
result['solved_challenges'] = challenges
result['score_history'] = team.score_history
else:
result['solved_challenges'] = []
result['score_history'] = []
if team.can_access():
result['players'] = list(team.players.all())
else:
result['players'] = []
return result
@utils.admin_required
@flask_restful.marshal_with(resource_fields)
def put(self, team_id):
team = models.Team.query.get_or_404(team_id)
app.logger.info('Update of team %r by %r.',
team, models.User.current())
data = flask.request.get_json()
# Writable fields
for field in ('name', 'score'):
setattr(team, field, data.get(field, getattr(team, field)))
models.commit()
cache.delete_team('team/%d')
return self._marshal_team(team)
class TeamList(flask_restful.Resource):
"""Get a list of all teams."""
resource_fields = {
'teams': fields.Nested(Team.team_fields),
}
@flask_restful.marshal_with(resource_fields)
def get(self):
return dict(teams=models.Team.all(with_history=False))
class TeamChange(flask_restful.Resource):
"""Endpoint for changing teams."""
resource_fields = {
'uid': fields.Integer,
'team_tid': fields.Integer,
'code': fields.String,
}
@utils.login_required
@flask_restful.marshal_with(resource_fields)
def put(self):
current = models.User.current()
if not (current.admin or current.uid == get_field('uid')):
raise errors.AccessDeniedError('Cannot Modify this User')
controllers.change_user_team(
get_field('uid'), get_field('team_tid'), get_field('code'))
class Session(flask_restful.Resource):
"""Represents a logged-in session, used for login/logout."""
team_fields = {
'tid': fields.Integer,
'name': fields.String,
'score': fields.Integer,
'code': fields.String,
}
resource_fields = {
'user': fields.Nested(User.resource_fields),
'team': fields.Nested(team_fields),
'redirect': fields.String,
}
@utils.login_required
@flask_restful.marshal_with(resource_fields)
def get(self):
"""Get the current session."""
return dict(
user=models.User.current(),
team=models.Team.current())
@flask_restful.marshal_with(resource_fields)
def post(self):
"""Login a user."""
user = auth.login_user(flask.request)
if not user:
redir = auth.get_login_uri()
if redir:
return dict(redirect=redir)
return {}
app.logger.info('%r logged in.', user)
utils.session_for_user(user)
return dict(user=user, team=user.team)
def delete(self):
auth.logout()
if flask.session.get('user', None):
app.logger.info('%r logging out.', models.User.current())
flask.session.clear()
try:
del flask.g.user
except: # noqa: E722
pass
try:
del flask.g.team
except: # noqa: E722
pass
return {'message': 'OK'}
class PasswordReset(flask_restful.Resource):
"""Setup for password reset."""
def get(self, email):
"""Send a password reset email."""
user = models.User.get_by_email(email)
if not user:
flask.abort(404)
controllers.offer_password_reset(user)
app.logger.info('Request password reset for %r.', user)
return {'message': 'Reset email sent.'}
def post(self, email):
"""Verify reset and set new password."""
# TODO: Move to controller
data = flask.request.get_json()
user = models.User.get_by_email(email)
if not user:
flask.abort(404)
token = data.get('token', '')
try:
user.verify_token(token)
except errors.ValidationError as ex:
app.logger.warning('Error validating password reset: %s', str(ex))
raise
except Exception as ex:
app.logger.exception(
'Unhandled exception during password reset: %s', str(ex))
raise
if data['password'] != data['password2']:
raise errors.ValidationError("Passwords don't match.")
user.set_password(data['password'])
app.logger.info('Password reset for %r.', user)
models.commit()
utils.session_for_user(user)
return {'message': 'Password reset.'}
api.add_resource(UserList, '/api/users')
api.add_resource(User, '/api/users/<int:user_id>')
api.add_resource(TeamChange, '/api/teams/change')
api.add_resource(TeamList, '/api/teams')
api.add_resource(Team, '/api/teams/<int:team_id>')
api.add_resource(Session, '/api/session')
if app.config.get('LOGIN_METHOD') == 'local':
api.add_resource(PasswordReset, '/api/pwreset/<email>')
class Challenge(flask_restful.Resource):
"""A single challenge."""
decorators = [utils.admin_required]
challenge_fields = {
'cid': fields.Integer,
'name': fields.String,
'points': fields.Integer,
'min_points': fields.Integer,
'current_points': fields.Integer,
'description': fields.String,
'unlocked': fields.Boolean,
'answered': fields.Boolean,
'solves': fields.Integer,
'weight': fields.Integer,
'prerequisite': PrerequisiteField,
'teaser': fields.Boolean,
'validator': fields.String,
}
attachment_fields = {
'aid': fields.String,
'filename': fields.String,
}
tags_fields = {
'tagslug': fields.String,
'name': fields.String,
}
team_fields = {
'name': fields.String,
'tid': fields.Integer,
}
answers_fields = {
'timestamp': fields.DateTime,
'team': fields.Nested(team_fields),
}
resource_fields = challenge_fields.copy()
resource_fields['attachments'] = fields.List(
fields.Nested(attachment_fields))
resource_fields['tags'] = fields.List(
fields.Nested(tags_fields))
resource_fields['answers'] = fields.List(
fields.Nested(answers_fields))
@flask_restful.marshal_with(resource_fields)
def get(self, challenge_id):
return models.Challenge.query.get_or_404(challenge_id)
@flask_restful.marshal_with(resource_fields)
def put(self, challenge_id):
challenge = models.Challenge.query.get_or_404(challenge_id)
data = flask.request.get_json()
old_unlocked = challenge.unlocked
for field in (
'name', 'description', 'points', 'min_points',
'unlocked', 'weight'):
setattr(
challenge, field, data.get(field, getattr(challenge, field)))
if 'validator' in data:
if not validators.IsValidator(data['validator']):
raise errors.ValidationError('Invalid validator.')
challenge.validator = data['validator']
if 'answer' in data and data['answer']:
answer = utils.normalize_input(data['answer'])
validator = validators.GetValidatorForChallenge(challenge)
validator.change_answer(answer)
if 'attachments' in data:
challenge.set_attachments(data['attachments'])
if 'prerequisite' in data:
challenge.set_prerequisite(data['prerequisite'])
else:
challenge.prerequisite = ''
if 'tags' in data:
challenge.set_tags(data['tags'])
if challenge.unlocked and not old_unlocked:
news = 'Challenge "%s" unlocked!' % challenge.name
models.News.game_broadcast(message=news)
app.logger.info('Challenge %s updated by %r.',
challenge, models.User.current())
models.commit()
cache.clear()
return challenge
def delete(self, challenge_id):
challenge = models.Challenge.query.get_or_404(challenge_id)
models.db.session.delete(challenge)
models.commit()
cache.clear()
class ChallengeList(flask_restful.Resource):
"""Bulk challenge management, includes:
- Create & manage challenges for admins.
- View challenge list for players.
"""
decorators = [utils.require_started]
resource_fields = {
'challenges': fields.Nested(Challenge.resource_fields)
}
@staticmethod
def _tease_challenge(chall):
"""Hide parts to be teased."""
res = {k: getattr(chall, k) for k in Challenge.resource_fields}
for f in ('description', 'attachments'):
del res[f]
return res
@flask_restful.marshal_with(resource_fields)
def get(self):
q = models.Challenge.get_joined_query()
challs = []
t = models.Team.current()
for chall in q.all():
if utils.is_admin() or chall.unlocked_for_team(t):
challs.append(chall)
elif chall.teaser:
challs.append(self._tease_challenge(chall))
return {'challenges': challs}
@utils.admin_required
@flask_restful.marshal_with(Challenge.resource_fields)
def post(self):
data = flask.request.get_json()
unlocked = data.get('unlocked', False)
answer = utils.normalize_input(data['answer'])
if not validators.IsValidator(data.get('validator', None)):
raise errors.ValidationError('Invalid validator.')
chall = models.Challenge.create(
data['name'],
data['description'],
data['points'],
'',
unlocked,
data.get('validator', validators.GetDefaultValidator()))
validator = validators.GetValidatorForChallenge(chall)
validator.change_answer(answer)
if 'min_points' in data:
chall.min_points = data['min_points']
else:
chall.min_points = chall.points
if 'attachments' in data:
chall.set_attachments(data['attachments'])
if 'prerequisite' in data:
chall.set_prerequisite(data['prerequisite'])
if 'tags' in data:
chall.set_tags(data['tags'])
if unlocked and utils.GameTime.open():
news = 'New challenge created: "%s"' % chall.name
models.News.game_broadcast(message=news)
models.commit()
app.logger.info('Challenge %s created by %r.',
chall, models.User.current())
return chall
class Tag(flask_restful.Resource):
"""Single tag for challenges."""
decorators = [utils.login_required, utils.require_started]
tag_fields = {
'name': fields.String,
'tagslug': fields.String,
'description': fields.String
}
resource_fields = tag_fields.copy()
resource_fields['challenges'] = fields.Nested(Challenge.resource_fields)
@flask_restful.marshal_with(resource_fields)
def get(self, tag_slug):
tag = models.Tag.query.get_or_404(tag_slug)
return self.get_challenges(tag)
@utils.admin_required
@flask_restful.marshal_with(resource_fields)
def put(self, tag_slug):
tag = models.Tag.query.get_or_404(tag_slug)
tag.name = get_field('name')
tag.description = get_field('description', tag.description)
app.logger.info('Tag %s updated by %r', tag, models.User.current())
models.commit()
cache.clear()
return self.get_challenges(tag)
@utils.admin_required
def delete(self, tag_slug):
tag = models.Tag.query.get_or_404(tag_slug)
models.db.session.delete(tag)
cache.clear()
models.commit()
@classmethod
def get_challenges(cls, tag):
if models.User.current() and models.User.current().admin:
challenges = tag.challenges
else:
raw = tag.get_challenges()
challenges = []
for ch in raw:
if ch.unlocked_for_team(models.Team.current()):
challenges.append(ch)
elif ch.teaser:
challenges.append(ChallengeList._tease_challenge(ch))
res = {k: getattr(tag, k) for k in cls.tag_fields}
res['challenges'] = list(challenges)
return res
class TagList(flask_restful.Resource):
"""List of all tags"""
decorators = [utils.require_started]
resource_fields = {
'tags': fields.Nested(Tag.tag_fields)
}
@cache.rest_team_cache('tags/%d')
@flask_restful.marshal_with(resource_fields)
def get(self):
q = models.Tag.query.all()
return dict(tags=q)
@utils.admin_required
@flask_restful.marshal_with(Tag.tag_fields)
def post(self):
tag = models.Tag.create(
get_field('name'),
get_field('description', ''))
models.commit()
app.logger.info('Tag %s created by %r.', tag, models.User.current())
cache.clear()
return tag
class Answer(flask_restful.Resource):
"""Submit an answer."""
decorators = [utils.login_required,
utils.require_submittable]
# TODO: get answers for admin?
def post(self):
data = flask.request.get_json()
if utils.is_admin():
return self.post_admin(data)
return self.post_player(data)
@utils.admin_required
def post_admin(self, data):
cid = data.get('cid', None)
tid = data.get('tid', None)
if not cid or not tid:
raise errors.ValidationError('Requires team and challenge.')
challenge = models.Challenge.query.get(data['cid'])
team = models.Team.query.get(data['tid'])
if not challenge or not team:
raise errors.ValidationError('Requires team and challenge.')
user = models.User.current()
app.challenge_log.info(
'Admin %s <%s> submitting flag for challenge %s <%d>, '
'team %s <%d>',
user.nick, user.email, challenge.name, challenge.cid,
team.name, team.tid)
try:
points = controllers.save_team_answer(challenge, team, None)
except (errors.IntegrityError, errors.FlushError) as ex:
app.logger.exception(
'Unable to save answer for %s/%s: %s',
str(data['tid']), str(data['tid']), str(ex))
models.db.session.rollback()
raise errors.AccessDeniedError(
'Unable to save answer for team. See log for details.')
cache.delete('cats/%d' % tid)
cache.delete('scoreboard')
return dict(points=points)
def post_player(self, data):
answer = utils.normalize_input(data['answer'])
try:
points = controllers.submit_answer(
data['cid'], answer, data.get('token'))
except (errors.IntegrityError, errors.FlushError) as exc:
app.logger.exception('Exception when saving answer: %s', exc)
models.db.session.rollback()
raise errors.AccessDeniedError(
'Previously solved or flag already used.')
cache.delete_team('cats/%d')
cache.delete('scoreboard')
return dict(points=points)
class Validator(flask_restful.Resource):
"""Allow admins to test an answer."""
decorators = [utils.admin_required]
def post(self):
data = flask.request.get_json()
answer = utils.normalize_input(data['answer'])
try:
correct = controllers.test_answer(data['cid'], answer)
except errors.IntegrityError:
raise errors.InvalidAnswerError('Invalid answer.')
if not correct:
raise errors.InvalidAnswerError('Invalid answer.')
return dict(message='Answer OK.')
api.add_resource(Tag, '/api/tags/<string:tag_slug>')
api.add_resource(TagList, '/api/tags')
api.add_resource(ChallengeList, '/api/challenges')
api.add_resource(Challenge, '/api/challenges/<int:challenge_id>')
api.add_resource(Answer, '/api/answers')
api.add_resource(Validator, '/api/validator')
class APIScoreboard(flask_restful.Resource):
"""Retrieve the scoreboard."""
line_fields = {
'position': fields.Integer,
'tid': fields.Integer,
'name': fields.String,
'score': fields.Integer,
'history': fields.Nested(Team.history_fields),
}
resource_fields = {
'scoreboard': fields.Nested(line_fields),
}
@cache.rest_cache('scoreboard')
@flask_restful.marshal_with(resource_fields)
def get(self):
opts = {
'with_history': True,
'above_zero': not app.config.get('SCOREBOARD_ZEROS'),
}
return dict(scoreboard=[
{'position': i, 'name': v.name, 'tid': v.tid,
'score': v.score, 'history': v.score_history}
for i, v in models.Team.enumerate(**opts)])
api.add_resource(APIScoreboard, '/api/scoreboard')
class Config(flask_restful.Resource):
"""Get basic app.config for the scoreboard.
This should not change often as it is highly-cached on the client.
"""
def get(self):
datefmt = ISO8601DateTime()
config = dict(
teams=app.config.get('TEAMS'),
sbname=app.config.get('TITLE'),
news_mechanism='poll',
news_poll_interval=app.config.get('NEWS_POLL_INTERVAL'),
csrf_token=csrfutil.get_csrf_token(),
rules=app.config.get('RULES'),
game_start=datefmt.format(utils.GameTime.start),
game_end=datefmt.format(utils.GameTime.end),
login_url=auth.get_login_uri(),
register_url=auth.get_register_uri(),
login_method=app.config.get('LOGIN_METHOD'),
scoring=app.config.get('SCORING'),
validators=validators.ValidatorMeta(),
proof_of_work_bits=int(app.config.get('PROOF_OF_WORK_BITS')),
invite_only=app.config.get('INVITE_KEY') is not None,
)
return config
api.add_resource(Config, '/api/config')
class News(flask_restful.Resource):
"""Display and manage news."""
resource_fields = {
'nid': fields.Integer,
'news_type': fields.String,
'timestamp': ISO8601DateTime,
'author': fields.String,
'message': fields.String,
}
@flask_restful.marshal_with(resource_fields)
def get(self):
if models.Team.current():
news = models.News.for_team(models.Team.current())
else:
news = models.News.for_public()
return list(news)
@utils.admin_required
@flask_restful.marshal_with(resource_fields)
def post(self):
data = flask.request.get_json()
tid = None
if 'tid' in data:
try:
tid = int(data['tid'])
except ValueError:
pass
author = models.User.current().nick
if tid:
item = models.News.unicast(tid, author, data['message'])
else:
item = models.News.broadcast(author, data['message'])
models.commit()
return item
api.add_resource(News, '/api/news')
class Page(flask_restful.Resource):
"""Create and retrieve static pages."""
resource_fields = {
'path': fields.String,
'title': fields.String,
'contents': fields.String,
}
@cache.rest_cache_path
@flask_restful.marshal_with(resource_fields)
def get(self, path):
app.logger.info('Path: %s', path)
return models.Page.query.get_or_404(path)
@utils.admin_required
@flask_restful.marshal_with(resource_fields)
def post(self, path):
data = flask.request.get_json()
page = models.Page.query.get(path)
if not page:
page = models.Page()
page.path = path
models.db.session.add(page)
page.path = data.get('path', path)
page.title = data.get('title', page.title)
page.contents = data.get('contents', page.contents)
models.commit()
return page
@utils.admin_required
def delete(self, path):
page = models.Page.query.get_or_404(path)
models.db.session.delete(page)
models.commit()
return {}
class PageList(flask_restful.Resource):
"""Retrieve all pages available"""
resource_fields = {
'pages': fields.Nested({
'path': fields.String,
'title': fields.String,
})
}
@flask_restful.marshal_with(resource_fields)
def get(self):
return dict(pages=models.Page.query.all())
api.add_resource(Page, '/api/page/<path:path>')
api.add_resource(PageList, '/api/page')
class Attachment(flask_restful.Resource):
""""Allow updating and deleting of individual files"""
attachment_fields = {
'aid': fields.String,
'filename': fields.String,
}
challenge_fields = {
'name': fields.String,
'cid': fields.Integer,
}
resource_fields = attachment_fields.copy()
resource_fields['challenges'] = fields.List(
fields.Nested(challenge_fields))
decorators = [utils.admin_required]
@flask_restful.marshal_with(resource_fields)
def get(self, aid):
return models.Attachment.query.get_or_404(aid)
@flask_restful.marshal_with(resource_fields)
def put(self, aid):
attachment = models.Attachment.query.get_or_404(aid)
attachment.filename = get_field('filename')
attachment.set_challenges(get_field('challenges'))
app.logger.info('Attachment %s updated by %r.',
attachment, models.User.current())
models.commit()
cache.clear()
return attachment
def delete(self, aid):
attachment = models.Attachment.query.get_or_404(aid)
# Probably do not need to delete from disk
attachment.delete()
app.logger.info('Attachment %s deleted by %r.',
attachment, models.User.current())
models.commit()
cache.clear()
class AttachmentList(flask_restful.Resource):
"""Allow uploading of files."""
resource_fields = {
'attachments': fields.Nested(Attachment.resource_fields)
}
decorators = [utils.admin_required]
def post(self):
app.logger.info('Uploading a new file.')
fp = flask.request.files['file']
app.logger.info('Using backend: %r', attachments.backend)
aid, fpath = attachments.backend.upload(fp)
app.logger.info('File uploaded to backend, got aid %s', aid)
attachment = models.Attachment.query.get(aid)
if not attachment:
models.Attachment.create(aid, fp.filename, fp.mimetype)
models.commit()
cache.clear()
return dict(aid=aid, fpath=fpath, content_type=fp.mimetype)
@flask_restful.marshal_with(resource_fields)
def get(self):
return dict(attachments=list(models.Attachment.query.all()))
api.add_resource(Attachment, '/api/attachments/<string:aid>')
api.add_resource(AttachmentList, '/api/attachments')
class APIKey(flask_restful.Resource):
"""Get/set API key for admins."""
decorators = [utils.admin_required]
resource_fields = {
'api_key': fields.String,
'api_key_updated': ISO8601DateTime(),
}
@flask_restful.marshal_with(resource_fields)
def post(self):
user = models.User.current()
app.logger.info('Resetting API key for %r.', user)
user.reset_api_key()
models.commit()
return user
@flask_restful.marshal_with(resource_fields)
def get(self):
return models.User.current()
def delete(self, keyid=None):
if keyid is None:
return self._delete_all()
user = models.User.current()
if keyid != user.api_key:
raise errors.AccessDeniedError('Cannot delete that key.')
user.api_key = None
user.api_key_updated = datetime.datetime.now()
models.commit()
return dict(status='OK')
def _delete_all(self):
for u in models.User.query.filter(
models.User.api_key != None).all(): # noqa: E711
u.api_key = None
u.api_key_updated = datetime.datetime.now()
models.commit()
return dict(status='OK')
api.add_resource(APIKey, '/api/apikey', '/api/apikey/<keyid>')
class BackupRestore(flask_restful.Resource):
"""Control for backup and restore."""
decorators = [utils.admin_required]
def get(self):
# TODO: refactor, this is messy
rv = {
'challenges': list(models.Challenge.query.all()),
'tags': list(models.Challenge.query.all()),
}
return (
rv,
200,
{'Content-Disposition': 'attachment; filename=challenges.json'})
def post(self):
# TODO: refactor, this is messy
raise NotImplementedError('Restore not implemented.')
challs = []
models.commit()
cache.clear()
return {'message': '%d Challenges imported.' % (len(challs),)}
api.add_resource(BackupRestore, '/api/backup')
class CTFTimeScoreFeed(flask_restful.Resource):
"""Provide a JSON feed to CTFTime.
At this time, it is only intended to cover the mandatory fields in the
feed: https://ctftime.org/json-scoreboard-feed
"""
def get(self):
standings = [{'pos': i, 'team': v.name, 'score': v.score}
for i, v in models.Team.enumerate()]
data = {'standings': standings}
return data, 200, {'X-No-XSSI': 1}
api.add_resource(CTFTimeScoreFeed, '/api/ctftime/scoreboard')
class Configz(flask_restful.Resource):
"""Dump the config."""
decorators = [utils.admin_required]
def get(self):
return repr(app.config)
api.add_resource(Configz, '/api/configz')
class ToolsRecalculate(flask_restful.Resource):
"""Recalculate the scores."""
decorators = [utils.admin_required]
def post(self):
changed = 0
for team in models.Team.query.all():
old = team.score
team.update_score()
changed += 1 if team.score != old else 0
models.commit()
cache.clear()
return {'message': ('Recalculated, %d changed.' % changed)}
api.add_resource(ToolsRecalculate, '/api/tools/recalculate')
class DBReset(flask_restful.Resource):
"""Reset various parts of the database."""
decorators = [utils.admin_required]
def post(self):
data = flask.request.get_json()
if data.get('ack') != 'ack':
raise ValueError('Requires ack!')
op = data.get('op', '')
if op == 'scores':
app.logger.info('Score reset requested by %r.',
models.User.current())
models.ScoreHistory.query.delete()
models.Answer.query.delete()
models.NonceFlagUsed.query.delete()
for team in models.Team.query.all():
team.score = 0
elif op == 'players':
app.logger.info('Player reset requested by %r.',
models.User.current())
models.User.query.filter(
models.User.admin == False).delete() # noqa: E712
models.Team.query.delete()
else:
raise ValueError('Unknown operation %s' % op)
models.commit()
cache.clear()
return {'message': 'Done'}
api.add_resource(DBReset, '/api/tools/reset')
================================================
FILE: scoreboard/tests/__init__.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
================================================
FILE: scoreboard/tests/base.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Base test module, MUST be imported first."""
import contextlib
import copy
import functools
import json
import logging
import os
import os.path
import pbkdf2
import time
import unittest
import flask
from flask import testing
import flask_testing
from sqlalchemy import event
from scoreboard import attachments
from scoreboard import cache
from scoreboard import main
from scoreboard import models
from scoreboard import utils
class BaseTestCase(flask_testing.TestCase):
"""Base TestCase for scoreboard.
Monkey-patches the app and db objects.
"""
TEST_CONFIG = dict(
PRESERVE_CONTEXT_ON_EXCEPTION=False,
SECRET_KEY='testing-session-key',
SQLALCHEMY_DATABASE_URI="sqlite://",
TEAMS=True,
TEAM_SECRET_KEY='different-secret',
TESTING=True,
DEBUG=False,
ATTACHMENT_BACKEND='test://volatile',
)
def create_app(self):
"""Called by flask_testing."""
app = main.get_app()
app.config.update(self.TEST_CONFIG)
attachments.patch("test")
main.setup_logging(app)
return app
def setUp(self):
"""Re-setup the DB to ensure a fresh instance."""
super(BaseTestCase, self).setUp()
# Reset config on each call
try:
app = main.get_app()
app.config = copy.deepcopy(self.app._SAVED_CONFIG)
except AttributeError:
self.app._SAVED_CONFIG = copy.deepcopy(app.config)
models.db.init_app(app)
models.db.create_all()
cache.global_cache = cache.cache.NullCache() # Reset cache
def tearDown(self):
models.db.session.remove()
models.db.drop_all()
super(BaseTestCase, self).tearDown()
def queryLimit(self, limit=None):
return MaxQueryBlock(self, limit)
def assertItemsEqual(self, a, b, msg=None):
a = list(a)
b = list(b)
a.sort()
b.sort()
if len(a) == len(b):
success = True
for c, d in zip(a, b):
if c != d:
success = False
break
if success:
return None
if msg is not None:
raise AssertionError(msg)
raise AssertionError('Items not equal: %r != %r', a, b)
class RestTestCase(BaseTestCase):
"""Special features for testing rest handlers."""
def setUp(self):
super(RestTestCase, self).setUp()
# Monkey patch pbkdf2 for speed
self._orig_pbkdf2 = pbkdf2.crypt
pbkdf2.crypt = self._pbkdf2_dummy
# Setup some special clients
self.admin_client = AdminClient(
self.app, self.app.response_class)
self.authenticated_client = AuthenticatedClient(
self.app, self.app.response_class)
def tearDown(self):
super(RestTestCase, self).tearDown()
pbkdf2.crypt = self._orig_pbkdf2
def postJSON(self, path, data, client=None):
client = client or self.client
return client.post(
path, data=json.dumps(data),
content_type='application/json')
def putJSON(self, path, data, client=None):
client = client or self.client
return client.put(
path, data=json.dumps(data),
content_type='application/json')
@contextlib.contextmanager
def swapClient(self, client):
old_client = self.client
self.client = client
yield
self.client = old_client
@staticmethod
def _pbkdf2_dummy(value, *unused_args):
return value
class AuthenticatedClient(testing.FlaskClient):
"""Like TestClient, but authenticated."""
def __init__(self, *args, **kwargs):
super(AuthenticatedClient, self).__init__(*args, **kwargs)
self.team = models.Team.create('team')
self.password = 'hunter2'
self.user = models.User.create(
'auth@example.com', 'Authenticated',
self.password, team=self.team)
models.db.session.commit()
self.uid = self.user.uid
self.tid = self.team.tid
def open(self, *args, **kwargs):
with self.session_transaction() as sess:
sess['user'] = self.uid
sess['team'] = self.tid
sess['expires'] = time.time() + 3600
return super(AuthenticatedClient, self).open(*args, **kwargs)
class AdminClient(testing.FlaskClient):
"""Like TestClient, but admin."""
def __init__(self, *args, **kwargs):
super(AdminClient, self).__init__(*args, **kwargs)
self.user = models.User.create('admin@example.net', 'Admin', 'hunter2')
self.user.admin = True
models.db.session.commit()
self.uid = self.user.uid
def open(self, *args, **kwargs):
with self.session_transaction() as sess:
sess['user'] = self.uid
sess['admin'] = True
sess['expires'] = time.time() + 3600
return super(AdminClient, self).open(*args, **kwargs)
class MaxQueryBlock(object):
"""Run a certain block with a maximum number of queries."""
def __init__(self, test=None, max_count=None):
self.max_count = max_count
self.queries = []
self._sql_listen_args = (
models.db.engine, 'before_cursor_execute',
self._count_query)
self.test_id = test.id() if test else ''
def __enter__(self):
event.listen(*self._sql_listen_args)
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
event.remove(*self._sql_listen_args)
if exc_type is not None:
return False
if self.test_id:
limit_msg = ((' Limit: %d.' % self.max_count)
if self.max_count is not None else '')
logging.info('%s executed %d queries.%s',
self.test_id, len(self.queries), limit_msg)
if self.max_count is None:
return
if len(self.queries) > self.max_count:
message = ('Maximum query count exceeded: limit %d, executed %d.\n'
'----QUERIES----\n%s\n----END----') % (
self.max_count,
len(self.queries),
'\n'.join(self.queries))
raise AssertionError(message)
@property
def query_count(self):
return len(self.queries)
def _count_query(self, unused_conn, unused_cursor, statement, parameters,
unused_context, unused_executemany):
statement = '%s (%s)' % (
statement, ', '.join(str(x) for x in parameters))
self.queries.append(statement)
logging.debug('SQLAlchemy: %s', statement)
def authenticated_test(f):
"""Swaps out the client for an authenticated client."""
@functools.wraps(f)
def wrapped_test(self):
with self.swapClient(self.authenticated_client):
return f(self)
return wrapped_test
def admin_test(f):
"""Swaps out the client for an admin client."""
@functools.wraps(f)
def wrapped_test(self):
with self.swapClient(self.admin_client):
return f(self)
return wrapped_test
def run_all_tests(pattern='*_test.py'):
"""This loads and runs all tests in scoreboard.tests."""
if os.getenv("DEBUG_TESTS"):
logging.getLogger().setLevel(logging.DEBUG)
else:
logging.getLogger().setLevel(logging.INFO)
test_dir = os.path.dirname(os.path.realpath(__file__))
top_dir = os.path.abspath(os.path.join(test_dir, '..'))
suite = unittest.defaultTestLoader.discover(
test_dir, pattern=pattern,
top_level_dir=top_dir)
result = unittest.TextTestRunner().run(suite)
return result.wasSuccessful()
def json_monkeypatch():
"""Automatically strip our XSSI header."""
def new_loads(data, *args, **kwargs):
try:
prefix = utils.to_bytes(")]}',\n")
if data.startswith(prefix):
data = data[len(prefix):]
return json.loads(data, *args, **kwargs)
except Exception as exc:
logging.exception('JSON monkeypatch failed: %s', exc)
flask.json.loads = new_loads
json_monkeypatch()
================================================
FILE: scoreboard/tests/cache_test.py
================================================
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cache test module."""
import flask
import mock
from scoreboard.tests import base
from scoreboard import cache
class BaseCacheTest(base.BaseTestCase):
"""Test core caching functionality."""
def setUp(self):
super(BaseCacheTest, self).setUp()
cache.global_cache = cache.cache.SimpleCache()
def makeMockGet(self, cache_type, cache_host=None):
orig_config = self.app.config
def mock_get(key):
if key == 'CACHE_TYPE':
return cache_type
elif key == 'MEMCACHE_HOST':
return cache_host
return orig_config.get(key)
return mock_get
def testBuildCaches(self):
"""Test that we can build the various types of caches."""
for ctype in ('memcached', 'local'):
with mock.patch.object(self.app, 'config') as m:
m.get = self.makeMockGet(ctype, 'localhost')
c = cache.CacheWrapper(self.app)
with self.assertRaises(AttributeError):
c._non_existent_attribute_really
def testRestCache_Basic(self):
m = mock.Mock()
m.__name__ = 'mockMethod'
m.return_value = 5
wrapped = cache.rest_cache(m)
self.assertEqual(5, wrapped())
self.assertEqual(5, wrapped()) # called twice for caching
m.assert_called_once()
def testRestCache_Override(self):
m = mock.Mock()
m.__name__ = 'mockMethod'
m.return_value = 8
wrapped = cache.rest_cache('key')(m)
self.assertEqual(8, wrapped())
self.assertEqual(8, wrapped()) # called twice for caching
m2 = mock.Mock()
m2.__name__ = 'mockMethod2'
m2.return_value = 42
wrapped2 = cache.rest_cache('key')(m2) # same key
self.assertEqual(8, wrapped2())
m.assert_called_once()
m2.assert_not_called()
def testRestCachePath(self):
m = mock.Mock()
m.__name__ = 'mockMethod'
m.return_value = 1337
wrapped = cache.rest_cache_path(m)
with self.app.test_request_context('/foo/bar'):
self.assertEqual(1337, wrapped())
m.return_value = 1338
with self.app.test_request_context('/foo/bar?baz=1'):
self.assertEqual(1337, wrapped())
with self.app.test_request_context('/foo/baz'):
self.assertEqual(1338, wrapped())
def testRestTeamCache_Basic(self):
m = mock.Mock()
m.__name__ = 'mockMethod'
m.return_value = 5
wrapped = cache.rest_team_cache(m)
with mock.patch.object(flask, 'g'):
flask.g.tid = 111
self.assertEqual(5, wrapped())
m.return_value = 555
self.assertEqual(5, wrapped()) # called twice for caching
m.assert_called_once()
flask.g.tid = 123
self.assertEqual(555, wrapped()) # different team?
def testRestTeamCache_Override(self):
m = mock.Mock()
m.__name__ = 'mockMethod'
m.return_value = 5
with self.assertRaises(ValueError):
cache.rest_team_cache('foo')(m)
wrapped = cache.rest_team_cache('foo-%d')(m)
with mock.patch.object(flask, 'g'):
flask.g.tid = 111
self.assertEqual(5, wrapped())
m.return_value = 555
self.assertEqual(5, wrapped()) # called twice for caching
m.assert_called_once()
flask.g.tid = 123
self.assertEqual(555, wrapped()) # different team?
def testRestCacheCaller_NonSerializable(self):
m = mock.Mock()
m.return_value = mock.Mock()
m.return_value.foo = 5
self.assertEqual(5, cache._rest_cache_caller(m, 'foo').foo)
m.assert_called_once()
def testRestCacheCaller_NonLoadable(self):
cache.global_cache.set('foo', '{ not valid json')
m = mock.Mock()
m.return_value = 5
self.assertEqual(5, cache._rest_cache_caller(m, 'foo'))
self.assertEqual(5, cache._rest_cache_caller(m, 'foo'))
m.assert_called_once()
def testRestAddCacheHeader(self):
foo = 'foo'
rv = cache._rest_add_cache_header((foo,))
self.assertEqual(foo, rv[0])
self.assertEqual(200, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
rv = cache._rest_add_cache_header((foo, 404))
self.assertEqual(foo, rv[0])
self.assertEqual(404, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
rv = cache._rest_add_cache_header((foo, 404, None))
self.assertEqual(foo, rv[0])
self.assertEqual(404, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
rv = cache._rest_add_cache_header((foo, 404, {foo: foo}))
self.assertEqual(foo, rv[0])
self.assertEqual(404, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
rv = cache._rest_add_cache_header({foo: foo})
self.assertTrue(foo in rv[0])
self.assertEqual(200, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
rv = cache._rest_add_cache_header(foo)
self.assertEqual(foo, rv[0])
self.assertEqual(200, rv[1])
self.assertTrue('X-Cache-Hit' in rv[2])
# Passthrough cases
bar = mock.Mock()
self.assertEqual(bar, cache._rest_add_cache_header(bar))
baz = (1, 2, 3, 4)
self.assertEqual(baz, cache._rest_add_cache_header(baz))
bang = (1, 2, 3)
self.assertEqual(bang, cache._rest_add_cache_header(bang))
================================================
FILE: scoreboard/tests/controllers_test.py
================================================
# Copyright 2019 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from scoreboard.tests import base
from scoreboard import controllers
from scoreboard import errors
class RegisterTest(base.BaseTestCase):
"""Test register_user controller."""
def testRegister_Normal(self):
rv = controllers.register_user('foo@bar.com', 'foo', 'pass')
self.assertIsNotNone(rv)
def testRegister_BadEmail(self):
"""Test variations on bad emails."""
for email in ('', 'frob', '//', '<foo@bar.com', 'foo@bar.com>'):
with self.assertRaises(errors.ValidationError):
controllers.register_user(email, 'foo', 'pass')
def testRegister_DupeNick(self):
self.app.config['TEAMS'] = False
controllers.register_user('foo@bar.com', 'foo', 'pass')
with self.assertRaises(errors.ValidationError):
controllers.register_user('bar@bar.com', 'foo', 'pass')
def testRegister_DupeTeam(self):
self.app.config['TEAMS'] = True
controllers.register_user(
'foo@bar.com', 'foo', 'pass', team_id='new',
team_name='faketeam')
with self.assertRaises(errors.ValidationError):
controllers.register_user(
'bar@bar.com', 'foo', 'pass', team_id='new',
team_name='faketeam')
def testRegister_DupeEmail(self):
self.app.config['TEAMS'] = False
controllers.register_user('foo@bar.com', 'foo', 'pass')
with self.assertRaises(errors.ValidationError):
controllers.register_user('foo@bar.com', 'sam', 'pass')
================================================
FILE: scoreboard/tests/csrfutil_test.py
================================================
# Copyright 2018 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import struct
import time
import jinja2
from werkzeug import exceptions
from scoreboard.tests import base
from scoreboard import csrfutil
try:
import mock
except ImportError:
from unittest import mock
class CSRFUtilTest(base.BaseTestCase):
"""Test CSRF protection."""
base_clock = 1523481076.571611
valid_token = 'dMvPWgUhaJbF18mqeuMyWspjUplsvb1x4Z139GbPFAjlzhLO'
test_user = 'user'
@mock.patch.object(time, 'time')
def testGetCSRFToken(self, mock_time):
mock_time.return_value = self.base_clock
self.assertEqual(self.valid_token,
csrfutil.get_csrf_token(self.test_user))
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyCSRFToken_Valid(self, mock_time):
mock_time.return_value = self.base_clock
self.assertTrue(csrfutil.verify_csrf_token(
self.valid_token, self.test_user))
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyCSRFToken_Expired(self, mock_time):
mock_time.return_value = self.base_clock + (60 * 60 * 60)
self.assertFalse(csrfutil.verify_csrf_token(
self.valid_token, self.test_user))
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyCSRFToken_InvalidSig(self, mock_time):
mock_time.return_value = self.base_clock
token = self.valid_token.replace('svb', 'xxx')
self.assertFalse(csrfutil.verify_csrf_token(token, self.test_user))
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyCSRFToken_TamperedTime(self, mock_time):
mock_time.return_value = self.base_clock
token = self.valid_token.replace('dMv', 'xxx')
self.assertFalse(csrfutil.verify_csrf_token(token, self.test_user))
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyCSRFToken_Truncated(self, mock_time):
mock_time.return_value = self.base_clock
token = self.valid_token[:4]
with self.assertRaises(struct.error):
csrfutil.verify_csrf_token(token, self.test_user)
mock_time.reset_mock()
token = 'a'
self.assertFalse(csrfutil.verify_csrf_token(token, self.test_user))
mock_time.assert_not_called()
def testDecorator_GET(self):
called = mock.Mock()
called.__name__ = 'called'
wrapped = csrfutil.csrf_protect(called)
with self.app.test_request_context('/'):
wrapped()
called.assert_called_once()
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testDecorator_Passes(self, mock_verify):
mock_verify.return_value = True
called = mock.Mock()
called.__name__ = 'called'
wrapped = csrfutil.csrf_protect(called)
with self.app.test_request_context('/?csrftoken=x', method='POST'):
wrapped()
called.assert_called_once()
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testDecorator_Fails(self, mock_verify):
mock_verify.return_value = False
called = mock.Mock()
called.__name__ = 'called'
wrapped = csrfutil.csrf_protect(called)
with self.app.test_request_context('/', method='POST'):
with self.assertRaises(exceptions.Forbidden):
wrapped()
called.assert_not_called()
@mock.patch.object(csrfutil, 'get_csrf_token')
def testGetCSRFField(self, mock_get_csrf_token):
mock_value = 'abcdef'
mock_get_csrf_token.return_value = mock_value
rv = csrfutil.get_csrf_field(user='foo')
mock_get_csrf_token.assert_called_once_with(user='foo')
self.assertTrue(isinstance(rv, jinja2.Markup))
self.assertTrue(mock_value in str(rv))
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testCSRFProtectionMiddleware_HeaderValid(self, mock_verify_csrf_token):
headers = [('X-XSRF-TOKEN', 'foo')]
mock_verify_csrf_token.return_value = True
with self.app.test_request_context(
'/', method='POST', headers=headers):
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = False
csrfutil.csrf_protection_request()
mock_get.assert_called_once()
mock_verify_csrf_token.assert_called_once_with('foo')
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testCSRFProtectionMiddleware_HeaderInvalid(
self, mock_verify_csrf_token):
headers = [('X-XSRF-TOKEN', 'foo')]
mock_verify_csrf_token.return_value = False
with self.app.test_request_context(
'/', method='POST', headers=headers):
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = False
with self.assertRaises(exceptions.Forbidden):
csrfutil.csrf_protection_request()
mock_get.assert_called_once()
mock_verify_csrf_token.assert_called_once_with('foo')
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testCSRFProtectionMiddleware_FormValid(self, mock_verify_csrf_token):
mock_verify_csrf_token.return_value = True
with self.app.test_request_context(
'/?csrftoken=foo', method='POST'):
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = False
csrfutil.csrf_protection_request()
mock_get.assert_called_once()
mock_verify_csrf_token.assert_called_once_with('foo')
@mock.patch.object(csrfutil, 'verify_csrf_token')
def testCSRFProtectionMiddleware_GET(self, mock_verify_csrf_token):
mock_verify_csrf_token.return_value = True
with self.app.test_request_context('/'):
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = False
csrfutil.csrf_protection_request()
mock_get.assert_not_called()
mock_verify_csrf_token.assert_not_called()
================================================
FILE: scoreboard/tests/data.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import json
import random
from scoreboard import models
def make_admin():
u = models.User.create('admin@example.com', 'admin', 'admin')
u.promote()
return u
def make_teams():
teams = []
for name in ('QQQ', 'Light Cats', 'Siberian Nopsled', 'PPP', 'Raelly',
'Toast', 'csh', 'ByTeh', 'See Sure', 'Skinniest', '213374U'):
teams.append(models.Team.create(name))
return teams
def make_players(teams):
players = []
for name in ('Ritam', 'Dr34dc0d3', 'alpha', 'beta', 'gamma', 'delta',
'Dade', 'Kate', 'zwad3', 'strikerkid', 'redpichu', 'n0pe',
'0xcdb'):
team = random.choice(teams)
players.append(models.User.create(
name.lower() + '@example.com', name, 'password', team=team))
return players
def make_tags():
tags = []
for name in ('x86', 'x64', 'MIPS', 'RISC', 'Fun'):
tags.append(models.Tag.create(name, 'Problems involving '+name))
return tags
def make_challenges(tags):
challs = []
chall_words = (
'Magic', 'Grand', 'Fast', 'Hash', 'Table', 'Password',
'Crypto', 'Alpha', 'Beta', 'Win', 'Socket', 'Ball',
'Stego', 'Word', 'Gamma', 'Native', 'Mine', 'Dump',
'Tangled', 'Hackers', 'Book', 'Delta', 'Shadow',
'Lose', 'Draw', 'Long', 'Pointer', 'Free', 'Not',
'Only', 'Live', 'Secret', 'Agent', 'Hax0r', 'Whiskey',
'Tango', 'Foxtrot')
for _ in range(25):
title = random.sample(chall_words, 3)
random.shuffle(title)
title = ' '.join(title)
flag = '_'.join(random.sample(chall_words, 4)).lower()
# Choose a random subset of tags
numtags = random.randint(0, len(tags)-1)
local_tags = random.sample(tags, numtags)
points = random.randint(1, 20) * 100
desc = 'Flag: ' + flag
ch = models.Challenge.create(
title, desc, points, flag,
unlocked=True)
ch.add_tags(local_tags)
if len(challs) % 8 == 7:
ch.prerequisite = json.dumps(
{'type': 'solved', 'challenge': challs[-1].cid})
# TODO: attachments
challs.append(ch)
models.commit()
return challs
def make_answers(teams, challs):
for team in teams:
times = sorted(
[random.randint(0, 24*60) for _ in range(16)],
reverse=True)
for ch in random.sample(challs, random.randint(4, 16)):
a = models.Answer.create(ch, team, '')
ago = datetime.timedelta(minutes=times.pop(0))
a.timestamp = datetime.datetime.utcnow() - ago
team.score += ch.points
h = models.ScoreHistory()
h.team = team
h.score = team.score
h.when = a.timestamp
models.db.session.add(h)
def create_all():
make_admin()
# Teams and players
teams = make_teams()
make_players(teams)
# Challenges
tags = make_tags()
models.commit() # Need IDs allocated
challs = make_challenges(tags)
# Submitted answers
make_answers(teams, challs)
models.commit()
================================================
FILE: scoreboard/tests/models_test.py
================================================
# Copyright 2019 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for models."""
import mock
import os
import time
from scoreboard.tests import base
from scoreboard import errors
from scoreboard import models
class TeamTest(base.BaseTestCase):
def setUp(self):
super(TeamTest, self).setUp()
self.team = models.Team.create('Test Team')
def testCreateTeam(self):
foo = 'Some team name'
rv = models.Team.create(foo)
self.assertTrue(isinstance(rv, models.Team))
self.assertEqual(foo, rv.name)
self.assertEqual(foo, str(rv))
self.assertTrue(foo in repr(rv))
def testUpdateScore(self):
foo = 'team'
t = models.Team.create(foo)
t.update_score()
self.assertEqual(0, t.score)
t.answers = [mock.MagicMock(), mock.MagicMock()]
t.answers[0].current_points = 100
t.answers[1].current_points = 200
t.update_score()
self.assertEqual(300, t.score)
def testGetByName(self):
foo = 'team'
models.Team.create(foo)
rv = models.Team.get_by_name(foo)
self.assertEqual(foo, rv.name)
self.assertIsNone(models.Team.get_by_name('does-not-exist'))
class UserTest(base.BaseTestCase):
def setUp(self):
super(UserTest, self).setUp()
self.team = models.Team.create('Test Team')
self.user = models.User.create('test@test.com', 'test', '', self.team)
models.commit()
def testGetByNick(self):
nick = 'test'
self.assertEqual(nick, models.User.get_by_nick(nick).nick)
self.assertIsNone(models.User.get_by_nick(nick*2))
def testStr(self):
self.assertEqual('test', str(self.user))
def testResetApiKey(self):
self.assertIsNone(self.user.api_key)
self.user.reset_api_key()
self.assertNotEqual('', self.user.api_key)
old_key = self.user.api_key
self.user.reset_api_key()
self.assertNotEqual(old_key, self.user.api_key)
with mock.patch.object(os, 'urandom') as mock_urandom:
mock_urandom.return_value = b'\x41' * 16
self.user.reset_api_key()
self.assertEqual('41'*16, self.user.api_key)
mock_urandom.assert_called_once_with(16)
@mock.patch.object(time, 'time')
def testGetToken(self, mock_time):
mock_time.return_value = 12345.678
self.user.pwhash = '$1$foo'
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
token = self.user.get_token()
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
self.assertEqual(b'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8=', token)
@mock.patch.object(time, 'time')
def testVerifyToken_full(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8='
mock_time.return_value = 12348.678
self.user.pwhash = '$1$foo'
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
self.assertTrue(self.user.verify_token(good_token))
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyToken_wrongType(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8='
mock_time.return_value = 12348.678
self.user.pwhash = '$1$foo'
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
with self.assertRaises(errors.ValidationError):
self.user.verify_token(good_token, token_type='non')
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
def testVerifyToken_badFormat(self):
with self.assertRaises(errors.ValidationError):
self.user.verify_token('!!!')
@mock.patch.object(time, 'time')
def testVerifyToken_expired(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8='
mock_time.return_value = 99912345.678
self.user.pwhash = '$1$foo'
with self.assertRaises(errors.ValidationError):
self.user.verify_token(good_token)
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyToken_invalidSig(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzgkW8fhAQ8='
mock_time.return_value = 12345.678
self.user.pwhash = '$1$foo'
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
with self.assertRaises(errors.ValidationError):
self.user.verify_token(good_token)
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyToken_perUser(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8='
mock_time.return_value = 12345.678
self.user.pwhash = '$1$foo'
self.user.uid = 55
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
with self.assertRaises(errors.ValidationError):
self.user.verify_token(good_token)
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
@mock.patch.object(time, 'time')
def testVerifyToken_perPass(self, mock_time):
good_token = 'MTk1NDU6P0O68xiZ-H9gOLWPLzFkW8fhAQ8='
mock_time.return_value = 12345.678
self.user.pwhash = '$1$foobar'
with mock.patch.object(self.app.config, 'get') as mock_get:
mock_get.return_value = 'foo'
with self.assertRaises(errors.ValidationError):
self.user.verify_token(good_token)
mock_get.assert_called_once_with('SECRET_KEY')
mock_time.assert_called_once_with()
def testGetByEmail(self):
self.assertEqual(
self.user.nick, models.User.get_by_email(self.user.email).nick)
self.assertIsNone(models.User.get_by_email('foo'))
def testGetByApiKey(self):
token = 'a'*32
self.user.api_key = token
models.commit()
self.assertEqual(
self.user.nick, models.User.get_by_api_key(token).nick)
self.assertIsNone(models.User.get_by_api_key(token[:-1]))
================================================
FILE: scoreboard/tests/rest_test.py
================================================
# Copyright 2016 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import flask
import json
import io
import mock
from werkzeug import datastructures
from scoreboard.tests import base
from scoreboard.tests import data
from scoreboard import models
from scoreboard import rest
from scoreboard import utils
from scoreboard import views
# Needed imports
_ = (rest, views)
def makeTestUser():
u = models.User.create('email@example.com', 'Nick', 'hunter2')
models.db.session.commit()
return u
def makeTestTeam(user):
t = models.Team.create('Test')
user.team = t
models.db.session.commit()
return t
def makeTestChallenges():
tags = data.make_tags()
challs = data.make_challenges(tags)
models.db.session.commit()
return challs
class ConfigzTest(base.RestTestCase):
PATH = '/api/configz'
def testGetFails(self):
with self.queryLimit(0):
response = self.client.get(self.PATH)
self.assert403(response)
testGetFailsNonAdmin = base.authenticated_test(testGetFails)
@base.admin_test
def testAdmin(self):
with self.queryLimit(0):
response = self.client.get(self.PATH)
self.assert200(response)
class PageTest(base.RestTestCase):
PATH = '/api/page/home'
PATH_NEW = '/api/page/new'
PATH_404 = '/api/page/404'
def setUp(self):
super(PageTest, self).setUp()
page = models.Page()
page.path = 'home'
page.title = 'Home'
page.contents = 'Home Page'
models.db.session.add(page)
models.db.session.commit()
self.page = page
def testGetAnonymous(self):
with self.queryLimit(1):
response = self.client.get(self.PATH)
self.assert200(response)
self.assertEqual(self.page.title, response.json['title'])
self.assertEqual(self.page.contents, response.json['contents'])
def testGetNonExistent(self):
with self.queryLimit(1):
self.assert404(self.client.get(self.PATH_404))
@base.admin_test
def testDeletePage(self):
with self.client as c:
self.assert200(c.get(self.PATH))
with self.queryLimit(1):
self.assert200(c.delete(self.PATH))
self.assert404(c.get(self.PATH))
@base.admin_test
def testCreatePage(self):
page_data = dict(
title='Test',
contents='Test Page Contents',
)
with self.queryLimit(3):
resp = self.postJSON(self.PATH_NEW, page_data)
self.assert200(resp)
self.assertEqual(page_data['title'], resp.json['title'])
self.assertEqual(page_data['contents'], resp.json['contents'])
@base.authenticated_test
def testCreatePageNonAdmin(self):
page_data = dict(
title='Test',
contents='Test Page Contents',
)
with self.queryLimit(0):
resp = self.postJSON(self.PATH_NEW, page_data)
self.assert403(resp)
@base.admin_test
def testUpdatePage(self):
page_data = dict(
title='Test',
contents='Test Page Contents',
)
with self.queryLimit(3):
resp = self.postJSON(self.PATH, page_data)
self.assert200(resp)
self.assertEqual(page_data['title'], resp.json['title'])
self.assertEqual(page_data['contents'], resp.json['contents'])
class UpdateTeam(base.RestTestCase):
PATH = '/api/teams/change'
_state = None
def createTeam(self, teamname):
team = models.Team.create(teamname)
models.db.session.commit()
return team
def changeTeam(self, tid, code):
return self.putJSON(self.PATH, {
'uid': self.authenticated_client.uid,
'team_tid': tid,
'code': code
})
def patchState(self, time='BEFORE'):
self._state = utils.GameTime.state
utils.GameTime.state = staticmethod(lambda: time)
def restoreState(self):
if self._state:
utils.GameTime.state = self._state
@base.authenticated_test
def testChangeTeam(self):
self.patchState()
test_team = self.createTeam('test')
resp = self.changeTeam(test_team.tid, test_team.code)
self.assert200(resp)
self.restoreState()
@base.authenticated_test
def testTeamChangeWorked(self):
self.patchState()
test_team = self.createTeam('test2')
self.changeTeam(test_team.tid, test_team.code)
tid = self.authenticated_client.user.team.tid
self.assertEqual(tid, test_team.tid)
self.restoreState()
@base.authenticated_test
def testEmptyTeamIsDeleted(self):
self.patchState()
test_team_first = self.createTeam('first')
test_team_second = self.createTeam('second')
self.changeTeam(test_team_first.tid, test_team_first.code)
self.changeTeam(test_team_second.tid, test_team_second.code)
tid = self.authenticated_client.user.team.tid
self.assertEqual(tid, test_team_second.tid)
self.assertIsNone(models.Team.query.get(test_team_first.tid))
self.restoreState()
@base.authenticated_test
def testTeamWithSolvesNotDeleted(self):
self.patchState()
test_team_first = self.createTeam('first')
test_team_second = self.createTeam('second')
self.changeTeam(test_team_first.tid, test_team_first.code)
chall = models.Challenge.create('Foo', 'Foo', 1, 'Foo')
models.Answer.create(chall, test_team_first, 'Foo')
self.changeTeam(test_team_second.tid, test_team_second.code)
tid = self.authenticated_client.user.team.tid
self.assertEqual(tid, test_team_second.tid)
self.assertIsNotNone(models.Team.query.get(test_team_first.tid))
self.restoreState()
@base.authenticated_test
def testCantSwitchAfterStart(self):
self.patchState('DURING')
test_team = self.createTeam('test')
resp = self.changeTeam(test_team.tid, test_team.code)
self.assert403(resp)
self.restoreState()
class AttachmentTest(base.RestTestCase):
PATH = '/api/attachments/%s'
ATTACHMENT_FIELDS = ('aid', 'filename', 'challenges')
text = b"This is a test"
name = "test.txt"
def uploadFile(self, filename, text):
with self.admin_client as c:
string = io.BytesIO()
string.write(text)
string.seek(0)
return c.post('/api/attachments', data={
'file': (string, filename)
})
def fetchFile(self, aid):
with self.admin_client as c:
return c.get(self.PATH % aid)
def testUploadFile(self):
resp = self.uploadFile(self.name, self.text)
self.assert200(resp)
# Calculated using an external sha256 tool
self.assertEquals(
resp.json['aid'],
"c7be1ed902fb8dd4d48997c6452f5d7e"
"509fbcdbe2808b16bcf4edce4c07d14e")
def testQueryFile(self):
postresp = self.uploadFile(self.name, self.text)
self.app.logger.info('Your JSON: %s', postresp.json)
getresp = self.fetchFile(postresp.json['aid'])
self.assert200(getresp)
def testFileQueryAID(self):
postresp = self.uploadFile(self.name, self.text)
getresp = self.fetchFile(postresp.json['aid'])
self.assertEqual(getresp.json['aid'], postresp.json['aid'])
def testFileQueryName(self):
postresp = self.uploadFile(self.name, self.text)
getresp = self.fetchFile(postresp.json['aid'])
self.assertEqual(getresp.json['filename'], self.name)
def testFileChallengesEmpty(self):
postresp = self.uploadFile(self.name, self.text)
getresp = self.fetchFile(postresp.json['aid'])
self.assertEqual(len(getresp.json['challenges']), 0)
def testRetrieveFile(self):
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
getresp = c.get('/attachments/%s' % postresp.json['aid'])
self.assert200(getresp)
def testFileRetrievalValue(self):
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
getresp = c.get('/attachment/%s' % postresp.json['aid'])
self.assertEqual(getresp.get_data(), self.text)
def testFileDelete(self):
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
delresp = c.delete('/api/attachments/%s' % postresp.json['aid'])
self.assert200(delresp)
def testDeletionRemovesFile(self):
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
c.delete('/api/attachments/%s' % postresp.json['aid'])
with self.admin_client as c:
getresp = c.get('/api/attachments/%s' % postresp.json['aid'])
self.assert404(getresp)
def testFileUpdate(self):
new_name = "file.png"
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
putresp = c.put(
'/api/attachments/%s' % postresp.json['aid'],
data=json.dumps({
'filename': new_name,
'aid': postresp.json['aid'],
'challenges': [],
}), content_type="application/json")
self.assert200(putresp)
def testUpdateChangesName(self):
new_name = "file.png"
postresp = self.uploadFile(self.name, self.text)
with self.admin_client as c:
c.put('/api/attachments/%s' % postresp.json['aid'],
data=json.dumps({
'filename': new_name,
'aid': postresp.json['aid'],
'challenges': [],
}), content_type="application/json")
getresp = c.get('/api/attachments/%s' % postresp.json['aid'])
self.assertEqual(getresp.json['filename'], new_name)
class UserTest(base.RestTestCase):
PATH = '/api/users/%d'
USER_FIELDS = ('admin', 'nick', 'email', 'team_tid', 'uid')
def testGetAnonymous(self):
path = self.PATH % makeTestUser().uid
with self.queryLimit(0):
self.assert403(self.client.get(path))
def testGetNonExistent(self):
path = self.PATH % 999
with self.queryLimit(0):
resp = self.client.get(path)
self.assert403(resp)
testGetNonExistentAuth = base.authenticated_test(
testGetNonExistent)
@base.admin_test
def testGetNonExistentAdmin(self):
path = self.PATH % 999
with self.queryLimit(1):
resp = self.client.get(path)
self.assert404(resp)
@base.authenticated_test
def testGetSelf(self):
user = self.authenticated_client.user
with self.queryLimit(1):
resp = self.client.get(self.PATH % user.uid)
self.assert200(resp)
self.assertEqual(user.email, resp.json['email'])
self.assertEqual(user.nick, resp.json['nick'])
self.assertEqual(user.admin, resp.json['admin'])
@base.authenticated_test
def testUpdateUser(self):
user = self.authenticated_client.user
data = {'password': 'hunter3'} # for security
with self.queryLimit(2):
self.assert200(self.putJSON(
self.PATH % user.uid,
data))
@base.authenticated_test
def testUpdateUserNoAccess(self):
uid = self.admin_client.user.uid
data = {'password': 'hunter3'}
with self.queryLimit(0):
resp = self.putJSON(self.PATH % uid, data)
self.assert403(resp)
@base.admin_test
def testUpdateUserAdmin(self):
uid = self.authenticated_client.user.uid
data = {'nick': 'Lame'}
with self.queryLimit(2):
resp = self.putJSON(self.PATH % uid, data)
self.assert200(resp)
self.assertEqual('Lame', resp.json['nick'])
self.assertNotEqual(
'Lame',
self.authenticated_client.user.team.name)
@base.admin_test
def testUpdateUsersNoTeams(self):
uid = self.authenticated_client.user.uid
self.app.config['TEAMS'] = False
data = {'nick': 'Lame'}
with self.queryLimit(3):
resp = self.putJSON(self.PATH % uid, data)
self.assert200(resp)
self.assertEqual('Lame', resp.json['nick'])
self.assertEqual('Lame', self.authenticated_client.user.team.name)
@base.admin_test
def testUpdateUserPromote(self):
user = self.authenticated_client.user
data = {'nick': user.nick, 'admin': True}
# yes, this is a lot, but promoting is infrequent
with self.queryLimit(7):
resp = self.putJSON(self.PATH % user.uid, data)
self.assert200(resp)
self.assertTrue(resp.json['admin'])
@base.admin_test
def testUpdateUserDemote(self):
user = self.admin_client.user
data = {'nick': user.nick, 'admin': False}
with self.queryLimit(3):
resp = self.putJSON(self.PATH % user.uid, data)
self.assert200(resp)
self.assertFalse(resp.json['admin'])
@base.authenticated_test
def testUpdateUserNoSelfPromotion(self):
uid = self.authenticated_client.user.uid
data = {'admin': True}
with self.queryLimit(1):
resp = self.putJSON(self.PATH % uid, data)
self.assert200(resp)
self.assertFalse(resp.json['admin'])
@base.admin_test
def testUpdateUserNoAnswers(self):
user = self.authenticated_client.user
team = self.authenticated_client.user.team
chall = models.Challenge.create('Foo', 'Foo', 1, 'Foo')
models.Answer.create(chall, team, 'Foo')
models.db.session.commit()
data = {'nick': user.nick, 'admin': True}
with self.queryLimit(3):
resp = self.putJSON(self.PATH % user.uid, data)
self.assert400(resp)
user = models.User.query.get(user.uid)
self.assertFalse(user.admin)
@base.authenticated_test
def testGetUsersNoAccess(self):
with self.queryLimit(0):
resp = self.client.get('/api/users')
self.assert403(resp)
@base.admin_test
def testGetUsers(self):
self.admin_client.user
with self.queryLimit(1):
resp = self.client.get('/api/users')
self.assert200(resp)
self.assertIsInstance(resp.json, dict)
self.assertIn('users', resp.json)
self.assertIsInstance(resp.json['users'], list)
users = resp.json['users']
self.assertEqual(2, len(users))
for u in users:
self.assertItemsEqual(self.USER_FIELDS, u.keys())
@staticmethod
def default_data():
return {
'email': 'test@example.com',
'nick': 'test3',
'password': 'test3',
'team_id': 'new',
'team_name': 'New Team',
'team_code': None,
}
def testRegisterUserNewTeam(self):
data = self.default_data()
# TODO: maybe optimize?
with self.client:
with self.queryLimit(9):
resp = self.postJSON('/api/users', data)
self.assert200(resp)
self.assertItemsEqual(self.USER_FIELDS, resp.json.keys())
self.assertEqual(resp.json['uid'], flask.session['user'])
self.assertEqual(resp.json['admin'], flask.session['admin'])
self.assertEqual(resp.json['team_tid'], flask.session['team'])
def testRegisterUserExistingTeam(self):
team = self.authenticated_client.team
data = self.default_data()
data.update({
'team_id': team.tid,
'team_name': None,
'team_code': team.code,
})
with self.client:
with self.queryLimit(8):
resp = self.postJSON('/api/users', data)
self.assert200(resp)
self.assertItemsEqual(self.USER_FIELDS, resp.json.keys())
self.assertEqual(resp.json['uid'], flask.session['user'])
self.assertEqual(resp.json['admin'], flask.session['admin'])
self.assertEqual(resp.json['team_tid'], flask.session['team'])
self.assertEqual(team.tid, resp.json['team_tid'])
def testRegisterUserTeamWrongCode(self):
team = self.authenticated_client.team
data = self.default_data()
data.update({
'team_id': team.tid,
'team_name': None,
'team_code': 'xxx',
})
with self.client:
with self.queryLimit(1):
resp = self.postJSON('/api/users', data)
self.assert400(resp)
@base.authenticated_test
def testRegisterUserLoggedInFails(self):
data = self.default_data()
with self.queryLimit(0):
resp = self.postJSON('/api/users', data)
self.assert400(resp)
def testRegisterUserNoNick(self):
data = self.default_data()
del data['nick']
with self.queryLimit(0):
self.assert400(self.postJSON('/api/users', data))
def testRegisterUserNoTeam(self):
data = self.default_data()
del data['team_name']
del data['team_id']
with self.queryLimit(0):
self.assert400(self.postJSON('/api/users', data))
def testRegisterUserInviteKey(self):
self.app.config['INVITE_KEY'] = 'foobar'
data = self.default_data()
data['invite_key'] = self.app.config['INVITE_KEY']
with self.client:
resp = self.postJSON('/api/users', data)
self.assert200(resp)
def testRegisterUserNoInviteKey(self):
self.app.config['INVITE_KEY'] = 'foobar'
data = self.default_data()
with self.client:
with self.queryLimit(0):
resp = self.postJSON('/api/users', data)
self.assert400(resp)
def testRegisterUserWrongInviteKey(self):
self.app.config['INVITE_KEY'] = 'foobar'
data = self.default_data()
data['invite_key'] = 'notright'
with self.client:
with self.queryLimit(0):
resp = self.postJSON('/api/users', data)
self.assert400(resp)
class TeamTest(base.RestTestCase):
LIST_URL = '/api/teams'
def setUp(self):
super(TeamTest, self).setUp()
self.user = makeTestUser()
self.team = makeTestTeam(self.user)
self.team_path = '/api/teams/%d' % self.team.tid
@base.authenticated_test
def testGetTeam(self):
with self.queryLimit(4):
resp = self.client.get(self.team_path)
self.assert200(resp)
self.assertEqual(0, len(resp.json['players']))
self.assertEqual(self.team.name, resp.json['name'])
# TODO: check other fields
def testGetTeamAnonymous(self):
with self.queryLimit(0):
self.assert403(self.client.get(self.team_path))
@base.admin_test
def testGetTeamAdmin(self):
with self.queryLimit(4):
resp = self.client.get(self.team_path)
self.assert200(resp)
self.assertEqual(1, len(resp.json['players']))
@base.admin_test
def testUpdateTeamAdmin(self):
data = {'name': 'Updated'}
with self.queryLimit(6):
resp = self.putJSON(self.team_path, data)
self.assert200(resp)
self.assertEqual('Updated', resp.json['name'])
team = models.Team.query.get(self.team.tid)
self.assertEqual('Updated', team.name)
def testGetTeamList(self):
with self.client as c:
with self.queryLimit(3) as ctr:
resp = c.get(self.LIST_URL)
n_queries = ctr.query_count
self.assert200(resp)
models.Team.create('Test 2')
models.Team.create('Test 3')
models.Team.create('Test 4')
models.db.session.commit()
with self.queryLimit(n_queries):
resp = c.get(self.LIST_URL)
self.assert200(resp)
class SessionTest(base.RestTestCase):
PATH = '/api/session'
def testGetSessionAnonymous(self):
self.assert403(self.client.get(self.PATH))
@base.authenticated_test
def testGetSessionAuthenticated(self):
with self.queryLimit(1):
resp = self.client.get(self.PATH)
self.assert200(resp)
self.assertEqual(
self.authenticated_client.user.nick,
resp.json['user']['nick'])
self.assertEqual(
self.authenticated_client.team.name,
resp.json['team']['name'])
@base.admin_test
def testGetSessionAdmin(self):
with self.queryLimit(1):
resp = self.client.get(self.PATH)
self.assert200(resp)
self.assertEqual(
self.admin_client.user.nick,
resp.json['user']['nick'])
self.assertTrue(resp.json['user']['admin'])
self.assertItemsEqual(
{'tid': 0, 'score': 0, 'name': None,
'code': None},
resp.json['team'])
def testSessionLoginSucceeds(self):
data = {
'email': self.authenticated_client.user.email,
'password': self.authenticated_client.password,
}
with self.client:
with self.queryLimit(4):
resp = self.postJSON(self.PATH, data)
self.assert200(resp)
self.assertEqual(
flask.session['user'],
self.authenticated_client.user.uid)
self.assertEqual(
flask.session['team'],
self.authenticated_client.team.tid)
self.assertFalse(flask.session['admin'])
self.assertEqual(
flask.g.user.email,
self.authenticated_client.user.email)
def testSessionLoginFailsBadPassword(self):
data = {
'email': self.authenticated_client.user.email,
'password': 'wrong',
}
with self.client:
with self.queryLimit(1):
resp = self.postJSON(self.PATH, data)
self.assert403(resp)
self.assertIsNone(flask.session.get('user'))
self.assertIsNone(flask.session.get('team'))
self.assertIsNone(flask.session.get('admin'))
def testSessionLoginFailsBadUser(self):
data = {
'email': 'no@example.com',
'password': 'wrong',
}
with self.client:
with self.queryLimit(1):
resp = self.postJSON(self.PATH, data)
self.assert403(resp)
self.assertIsNone(flask.session.get('user'))
self.assertIsNone(flask.session.get('team'))
self.assertIsNone(flask.session.get('admin'))
@base.admin_test
def testSessionLoginAlreadyLoggedIn(self):
data = {
'email': self.authenticated_client.user.email,
'password': self.authenticated_client.password,
}
# This makes sure admin->non-admin downgrades properly
with self.client:
with self.queryLimit(4):
resp = self.postJSON(self.PATH, data)
self.assert200(resp)
self.assertEqual(
flask.session['user'],
self.authenticated_client.user.uid)
self.assertEqual(
flask.session['team'],
self.authenticated_client.team.tid)
self.assertFalse(flask.session['admin'])
self.assertEqual(
flask.g.user.email,
self.authenticated_client.user.email)
@base.authenticated_test
def testSessionLogout(self):
with self.client as c:
with self.queryLimit(1):
resp = c.delete(self.PATH)
self.assert200(resp)
self.assertIsNone(flask.session.get('user'))
self.assertIsNone(flask.session.get('team'))
self.assertIsNone(flask.session.get('admin'))
def testSessionLogoutAnonymous(self):
with self.client as c:
with self.queryLimit(0):
resp = c.delete(self.PATH)
self.assert200(resp)
self.assertIsNone(flask.session.get('user'))
self.assertIsNone(flask.session.get('team'))
self.assertIsNone(flask.session.get('admin'))
def testGetSessionWithApiKey(self):
""
gitextract_m6halfw6/ ├── .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 │ ├── 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-route.js │ │ ├── angular-sanitize.js │ │ └── angular.js │ ├── bootstrap/ │ │ ├── LICENSE │ │ ├── bootstrap.css │ │ └── bootstrap.js │ ├── bootstrap-theme/ │ │ └── bootstrap-theme.css │ ├── chart/ │ │ ├── Chart.Scatter.js │ │ ├── Chart.js │ │ ├── LICENSE.md │ │ └── Scatter.LICENSE.md │ ├── jquery/ │ │ ├── LICENSE.txt │ │ └── jquery.js │ ├── moment/ │ │ ├── LICENSE │ │ └── moment.js │ └── pagedown/ │ ├── LICENSE.txt │ ├── Markdown.Converter.js │ ├── Markdown.Editor.js │ └── Markdown.Sanitizer.js ├── templates/ │ ├── base.html │ ├── error.html │ ├── index.html │ └── pwreset.eml └── tests.py
SYMBOL INDEX (1321 symbols across 47 files)
FILE: main.py
function main (line 22) | def main(argv):
FILE: scoreboard/attachments/__init__.py
function get_backend_path (line 38) | def get_backend_path():
function get_backend_type (line 43) | def get_backend_type():
function get_backend (line 49) | def get_backend(_backend_type):
function patch (line 62) | def patch(_backend_type):
FILE: scoreboard/attachments/file.py
function attachment_dir (line 37) | def attachment_dir(create=False):
function send (line 56) | def send(attachment):
function delete (line 65) | def delete(attachment):
function upload (line 71) | def upload(fp):
FILE: scoreboard/attachments/gcs.py
function get_bucket (line 46) | def get_bucket(path=None):
function send (line 52) | def send(attachment):
function delete (line 72) | def delete(attachment):
function upload (line 82) | def upload(fp):
FILE: scoreboard/attachments/testing.py
function send (line 34) | def send(attachment):
function delete (line 41) | def delete(attachment):
function upload (line 46) | def upload(fp):
FILE: scoreboard/auth/local.py
function login_user (line 23) | def login_user(flask_request):
function get_login_uri (line 33) | def get_login_uri():
function get_register_uri (line 37) | def get_register_uri():
function logout (line 41) | def logout():
function register (line 45) | def register(flask_request):
FILE: scoreboard/cache.py
class CacheWrapper (line 27) | class CacheWrapper(object):
method __init__ (line 29) | def __init__(self, app):
method __getattr__ (line 39) | def __getattr__(self, name):
function rest_cache (line 46) | def rest_cache(f_or_key):
function rest_cache_path (line 69) | def rest_cache_path(f):
function rest_team_cache (line 79) | def rest_team_cache(f_or_key):
function delete (line 107) | def delete(key):
function clear (line 112) | def clear():
function delete_team (line 117) | def delete_team(base_key):
function _rest_cache_caller (line 124) | def _rest_cache_caller(f, cache_key, *args, **kwargs):
function _rest_add_cache_header (line 140) | def _rest_add_cache_header(rv, hit=False):
FILE: scoreboard/config_defaults.py
class Defaults (line 18) | class Defaults(object):
FILE: scoreboard/context.py
function get_csp_policy (line 53) | def get_csp_policy():
function load_globals (line 77) | def load_globals():
function load_apikey (line 98) | def load_apikey():
function add_headers (line 119) | def add_headers(response):
function util_contexts (line 129) | def util_contexts():
function receive_before_cursor_execute (line 138) | def receive_before_cursor_execute(
function count_queries (line 144) | def count_queries(response):
function ensure_setup (line 152) | def ensure_setup():
FILE: scoreboard/controllers.py
function register_user (line 39) | def register_user(email, nick, password, team_id=None,
function change_user_team (line 95) | def change_user_team(uid, team_tid, code):
function submit_answer (line 123) | def submit_answer(cid, answer, token):
function save_team_answer (line 169) | def save_team_answer(challenge, team, answer):
function test_answer (line 185) | def test_answer(cid, answer):
function offer_password_reset (line 195) | def offer_password_reset(user):
FILE: scoreboard/csrfutil.py
function _get_csrf_token (line 33) | def _get_csrf_token(user=None, expires=None):
function get_csrf_token (line 43) | def get_csrf_token(*args, **kwargs):
function verify_csrf_token (line 49) | def verify_csrf_token(token, user=None):
function csrf_protect (line 61) | def csrf_protect(f):
function get_csrf_field (line 74) | def get_csrf_field(*args, **kwargs):
function csrf_protection_request (line 83) | def csrf_protection_request():
function add_csrf_protection (line 97) | def add_csrf_protection(resp):
function csrf_context_processor (line 104) | def csrf_context_processor():
FILE: scoreboard/errors.py
class _MessageException (line 22) | class _MessageException(exceptions.HTTPException):
method __init__ (line 27) | def __init__(self, msg=None):
class AccessDeniedError (line 33) | class AccessDeniedError(_MessageException):
class ValidationError (line 38) | class ValidationError(_MessageException):
class InvalidAnswerError (line 43) | class InvalidAnswerError(AccessDeniedError):
class LoginError (line 48) | class LoginError(AccessDeniedError):
class ServerError (line 53) | class ServerError(_MessageException):
FILE: scoreboard/logger.py
class Formatter (line 20) | class Formatter(logging.Formatter):
method format (line 27) | def format(self, record):
FILE: scoreboard/mail.py
class MailFailure (line 26) | class MailFailure(Exception):
function send (line 31) | def send(message, subject, to, to_name=None, sender=None, sender_name=No...
function _send_smtp (line 48) | def _send_smtp(message, subject, to, to_name, sender, sender_name):
function _send_mailjet (line 80) | def _send_mailjet(message, subject, to, to_name, sender, sender_name):
FILE: scoreboard/main.py
function on_appengine (line 29) | def on_appengine():
function create_app (line 38) | def create_app(config=None):
function load_config_file (line 61) | def load_config_file(app=None):
function setup_logging (line 71) | def setup_logging(app):
function api_error_handler (line 117) | def api_error_handler(ex):
function get_app (line 144) | def get_app():
FILE: scoreboard/models.py
class Team (line 44) | class Team(db.Model):
method __repr__ (line 59) | def __repr__(self):
method __str__ (line 62) | def __str__(self):
method code (line 66) | def code(self):
method solves (line 74) | def solves(self):
method update_score (line 77) | def update_score(self):
method can_access (line 86) | def can_access(self, user=None):
method create (line 94) | def create(cls, name):
method get_by_name (line 101) | def get_by_name(cls, name):
method enumerate (line 108) | def enumerate(cls, with_history=False, above_zero=False):
method all (line 119) | def all(cls, with_history=True):
method current (line 129) | def current(cls):
class ScoreHistory (line 141) | class ScoreHistory(db.Model):
method add_entry (line 149) | def add_entry(cls, team):
class User (line 156) | class User(db.Model):
method set_password (line 170) | def set_password(self, password):
method __repr__ (line 173) | def __repr__(self):
method __str__ (line 176) | def __str__(self):
method promote (line 179) | def promote(self):
method get_token (line 191) | def get_token(self, token_type='pwreset', expires=None):
method verify_token (line 203) | def verify_token(self, token, token_type='pwreset'):
method reset_api_key (line 218) | def reset_api_key(self):
method get_by_email (line 228) | def get_by_email(cls, email):
method get_by_nick (line 235) | def get_by_nick(cls, nick):
method get_by_api_key (line 242) | def get_by_api_key(cls, token):
method login_user (line 251) | def login_user(cls, email, password):
method create (line 264) | def create(cls, email, nick, password, team=None):
method current (line 280) | def current(cls):
method all (line 297) | def all(cls):
class Tag (line 311) | class Tag(db.Model):
method __repr__ (line 323) | def __repr__(self):
method slugify (line 326) | def slugify(self):
method create (line 330) | def create(cls, name, description):
method get_challenges (line 338) | def get_challenges(self, unlocked_only=True, sort=True, force_query=Fa...
method _get_challenges_cached (line 346) | def _get_challenges_cached(self, unlocked_only=True, sort=True):
method _get_challenges_query (line 354) | def _get_challenges_query(self, unlocked_only=True, sort=True):
class Challenge (line 366) | class Challenge(db.Model):
method __repr__ (line 385) | def __repr__(self):
method is_answered (line 388) | def is_answered(self, team=None, answers=None):
method solves (line 402) | def solves(self):
method solves (line 410) | def solves(cls):
method answered (line 414) | def answered(self):
method teaser (line 420) | def teaser(self):
method current_points (line 428) | def current_points(self):
method log_score (line 441) | def log_score(max_points, min_points, midpoint, solves):
method unlocked_for_team (line 459) | def unlocked_for_team(self, team):
method prereq_solved (line 484) | def prereq_solved(self, prereq, team):
method create (line 495) | def create(cls, name, description, points, answer, unlocked=False,
method add_tags (line 511) | def add_tags(self, tags):
method delete (line 515) | def delete(self):
method set_attachments (line 518) | def set_attachments(self, attachments):
method set_prerequisite (line 535) | def set_prerequisite(self, prerequisite):
method set_tags (line 544) | def set_tags(self, tags):
method update_answers (line 561) | def update_answers(self, exclude_team=None):
method get_joined_query (line 573) | def get_joined_query(cls):
class Attachment (line 589) | class Attachment(db.Model):
method __str__ (line 601) | def __str__(self):
method __repr__ (line 604) | def __repr__(self):
method delete (line 607) | def delete(self, from_disk=True):
method set_challenges (line 615) | def set_challenges(self, challenges):
method create (line 632) | def create(cls, aid, filename, content_type):
class Answer (line 641) | class Answer(db.Model):
method create (line 654) | def create(cls, challenge, team, answer_text):
method current_points (line 673) | def current_points(self):
class News (line 680) | class News(db.Model):
method broadcast (line 697) | def broadcast(cls, author, message):
method game_broadcast (line 706) | def game_broadcast(cls, author=None, message=None):
method unicast (line 715) | def unicast(cls, team, author, message):
method for_team (line 730) | def for_team(cls, team, limit=10):
method for_public (line 737) | def for_public(cls, limit=10):
class Page (line 743) | class Page(db.Model):
class NonceFlagUsed (line 751) | class NonceFlagUsed(db.Model):
method create (line 760) | def create(cls, challenge, nonce, team):
function commit (line 769) | def commit():
FILE: scoreboard/rest.py
class ISO8601DateTime (line 40) | class ISO8601DateTime(fields.Raw):
method format (line 43) | def format(self, value):
class PrerequisiteField (line 55) | class PrerequisiteField(fields.Raw):
method format (line 58) | def format(self, value):
function output_json (line 68) | def output_json(data, code, headers=None):
function get_field (line 85) | def get_field(name, *args):
class User (line 96) | class User(flask_restful.Resource):
method get (line 110) | def get(self, user_id):
method put (line 116) | def put(self, user_id):
class UserList (line 145) | class UserList(flask_restful.Resource):
method get (line 154) | def get(self):
method post (line 158) | def post(self):
class Team (line 183) | class Team(flask_restful.Resource):
method get (line 210) | def get(self, team_id):
method _marshal_team (line 215) | def _marshal_team(self, team, extended=False):
method put (line 241) | def put(self, team_id):
class TeamList (line 254) | class TeamList(flask_restful.Resource):
method get (line 262) | def get(self):
class TeamChange (line 266) | class TeamChange(flask_restful.Resource):
method put (line 277) | def put(self):
class Session (line 285) | class Session(flask_restful.Resource):
method get (line 303) | def get(self):
method post (line 310) | def post(self):
method delete (line 322) | def delete(self):
class PasswordReset (line 338) | class PasswordReset(flask_restful.Resource):
method get (line 341) | def get(self, email):
method post (line 350) | def post(self, email):
class Challenge (line 387) | class Challenge(flask_restful.Resource):
method get (line 433) | def get(self, challenge_id):
method put (line 437) | def put(self, challenge_id):
method delete (line 473) | def delete(self, challenge_id):
class ChallengeList (line 480) | class ChallengeList(flask_restful.Resource):
method _tease_challenge (line 494) | def _tease_challenge(chall):
method get (line 502) | def get(self):
method post (line 515) | def post(self):
class Tag (line 551) | class Tag(flask_restful.Resource):
method get (line 565) | def get(self, tag_slug):
method put (line 571) | def put(self, tag_slug):
method delete (line 582) | def delete(self, tag_slug):
method get_challenges (line 589) | def get_challenges(cls, tag):
class TagList (line 605) | class TagList(flask_restful.Resource):
method get (line 616) | def get(self):
method post (line 622) | def post(self):
class Answer (line 632) | class Answer(flask_restful.Resource):
method post (line 640) | def post(self):
method post_admin (line 647) | def post_admin(self, data):
method post_player (line 675) | def post_player(self, data):
class Validator (line 690) | class Validator(flask_restful.Resource):
method post (line 695) | def post(self):
class APIScoreboard (line 715) | class APIScoreboard(flask_restful.Resource):
method get (line 731) | def get(self):
class Config (line 745) | class Config(flask_restful.Resource):
method get (line 751) | def get(self):
class News (line 776) | class News(flask_restful.Resource):
method get (line 788) | def get(self):
method post (line 797) | def post(self):
class Page (line 817) | class Page(flask_restful.Resource):
method get (line 828) | def get(self, path):
method post (line 834) | def post(self, path):
method delete (line 848) | def delete(self, path):
class PageList (line 855) | class PageList(flask_restful.Resource):
method get (line 866) | def get(self):
class Attachment (line 874) | class Attachment(flask_restful.Resource):
method get (line 894) | def get(self, aid):
method put (line 898) | def put(self, aid):
method delete (line 909) | def delete(self, aid):
class AttachmentList (line 920) | class AttachmentList(flask_restful.Resource):
method post (line 929) | def post(self):
method get (line 943) | def get(self):
class APIKey (line 951) | class APIKey(flask_restful.Resource):
method post (line 962) | def post(self):
method get (line 970) | def get(self):
method delete (line 973) | def delete(self, keyid=None):
method _delete_all (line 984) | def _delete_all(self):
class BackupRestore (line 996) | class BackupRestore(flask_restful.Resource):
method get (line 1000) | def get(self):
method post (line 1011) | def post(self):
class CTFTimeScoreFeed (line 1024) | class CTFTimeScoreFeed(flask_restful.Resource):
method get (line 1031) | def get(self):
class Configz (line 1041) | class Configz(flask_restful.Resource):
method get (line 1046) | def get(self):
class ToolsRecalculate (line 1053) | class ToolsRecalculate(flask_restful.Resource):
method post (line 1058) | def post(self):
class DBReset (line 1072) | class DBReset(flask_restful.Resource):
method post (line 1077) | def post(self):
FILE: scoreboard/tests/base.py
class BaseTestCase (line 40) | class BaseTestCase(flask_testing.TestCase):
method create_app (line 57) | def create_app(self):
method setUp (line 65) | def setUp(self):
method tearDown (line 78) | def tearDown(self):
method queryLimit (line 83) | def queryLimit(self, limit=None):
method assertItemsEqual (line 86) | def assertItemsEqual(self, a, b, msg=None):
class RestTestCase (line 104) | class RestTestCase(BaseTestCase):
method setUp (line 107) | def setUp(self):
method tearDown (line 118) | def tearDown(self):
method postJSON (line 122) | def postJSON(self, path, data, client=None):
method putJSON (line 128) | def putJSON(self, path, data, client=None):
method swapClient (line 135) | def swapClient(self, client):
method _pbkdf2_dummy (line 142) | def _pbkdf2_dummy(value, *unused_args):
class AuthenticatedClient (line 146) | class AuthenticatedClient(testing.FlaskClient):
method __init__ (line 149) | def __init__(self, *args, **kwargs):
method open (line 160) | def open(self, *args, **kwargs):
class AdminClient (line 168) | class AdminClient(testing.FlaskClient):
method __init__ (line 171) | def __init__(self, *args, **kwargs):
method open (line 178) | def open(self, *args, **kwargs):
class MaxQueryBlock (line 186) | class MaxQueryBlock(object):
method __init__ (line 189) | def __init__(self, test=None, max_count=None):
method __enter__ (line 197) | def __enter__(self):
method __exit__ (line 201) | def __exit__(self, exc_type, exc_value, exc_traceback):
method query_count (line 221) | def query_count(self):
method _count_query (line 224) | def _count_query(self, unused_conn, unused_cursor, statement, parameters,
function authenticated_test (line 232) | def authenticated_test(f):
function admin_test (line 241) | def admin_test(f):
function run_all_tests (line 250) | def run_all_tests(pattern='*_test.py'):
function json_monkeypatch (line 265) | def json_monkeypatch():
FILE: scoreboard/tests/cache_test.py
class BaseCacheTest (line 25) | class BaseCacheTest(base.BaseTestCase):
method setUp (line 28) | def setUp(self):
method makeMockGet (line 32) | def makeMockGet(self, cache_type, cache_host=None):
method testBuildCaches (line 43) | def testBuildCaches(self):
method testRestCache_Basic (line 52) | def testRestCache_Basic(self):
method testRestCache_Override (line 61) | def testRestCache_Override(self):
method testRestCachePath (line 76) | def testRestCachePath(self):
method testRestTeamCache_Basic (line 89) | def testRestTeamCache_Basic(self):
method testRestTeamCache_Override (line 103) | def testRestTeamCache_Override(self):
method testRestCacheCaller_NonSerializable (line 119) | def testRestCacheCaller_NonSerializable(self):
method testRestCacheCaller_NonLoadable (line 126) | def testRestCacheCaller_NonLoadable(self):
method testRestAddCacheHeader (line 134) | def testRestAddCacheHeader(self):
FILE: scoreboard/tests/controllers_test.py
class RegisterTest (line 21) | class RegisterTest(base.BaseTestCase):
method testRegister_Normal (line 24) | def testRegister_Normal(self):
method testRegister_BadEmail (line 28) | def testRegister_BadEmail(self):
method testRegister_DupeNick (line 34) | def testRegister_DupeNick(self):
method testRegister_DupeTeam (line 40) | def testRegister_DupeTeam(self):
method testRegister_DupeEmail (line 50) | def testRegister_DupeEmail(self):
FILE: scoreboard/tests/csrfutil_test.py
class CSRFUtilTest (line 31) | class CSRFUtilTest(base.BaseTestCase):
method testGetCSRFToken (line 39) | def testGetCSRFToken(self, mock_time):
method testVerifyCSRFToken_Valid (line 46) | def testVerifyCSRFToken_Valid(self, mock_time):
method testVerifyCSRFToken_Expired (line 53) | def testVerifyCSRFToken_Expired(self, mock_time):
method testVerifyCSRFToken_InvalidSig (line 60) | def testVerifyCSRFToken_InvalidSig(self, mock_time):
method testVerifyCSRFToken_TamperedTime (line 67) | def testVerifyCSRFToken_TamperedTime(self, mock_time):
method testVerifyCSRFToken_Truncated (line 74) | def testVerifyCSRFToken_Truncated(self, mock_time):
method testDecorator_GET (line 84) | def testDecorator_GET(self):
method testDecorator_Passes (line 93) | def testDecorator_Passes(self, mock_verify):
method testDecorator_Fails (line 103) | def testDecorator_Fails(self, mock_verify):
method testGetCSRFField (line 114) | def testGetCSRFField(self, mock_get_csrf_token):
method testCSRFProtectionMiddleware_HeaderValid (line 123) | def testCSRFProtectionMiddleware_HeaderValid(self, mock_verify_csrf_to...
method testCSRFProtectionMiddleware_HeaderInvalid (line 135) | def testCSRFProtectionMiddleware_HeaderInvalid(
method testCSRFProtectionMiddleware_FormValid (line 149) | def testCSRFProtectionMiddleware_FormValid(self, mock_verify_csrf_token):
method testCSRFProtectionMiddleware_GET (line 160) | def testCSRFProtectionMiddleware_GET(self, mock_verify_csrf_token):
FILE: scoreboard/tests/data.py
function make_admin (line 22) | def make_admin():
function make_teams (line 28) | def make_teams():
function make_players (line 36) | def make_players(teams):
function make_tags (line 47) | def make_tags():
function make_challenges (line 54) | def make_challenges(tags):
function make_answers (line 87) | def make_answers(teams, challs):
function create_all (line 104) | def create_all():
FILE: scoreboard/tests/models_test.py
class TeamTest (line 27) | class TeamTest(base.BaseTestCase):
method setUp (line 29) | def setUp(self):
method testCreateTeam (line 33) | def testCreateTeam(self):
method testUpdateScore (line 41) | def testUpdateScore(self):
method testGetByName (line 52) | def testGetByName(self):
class UserTest (line 60) | class UserTest(base.BaseTestCase):
method setUp (line 62) | def setUp(self):
method testGetByNick (line 68) | def testGetByNick(self):
method testStr (line 73) | def testStr(self):
method testResetApiKey (line 76) | def testResetApiKey(self):
method testGetToken (line 90) | def testGetToken(self, mock_time):
method testVerifyToken_full (line 101) | def testVerifyToken_full(self, mock_time):
method testVerifyToken_wrongType (line 112) | def testVerifyToken_wrongType(self, mock_time):
method testVerifyToken_badFormat (line 123) | def testVerifyToken_badFormat(self):
method testVerifyToken_expired (line 128) | def testVerifyToken_expired(self, mock_time):
method testVerifyToken_invalidSig (line 137) | def testVerifyToken_invalidSig(self, mock_time):
method testVerifyToken_perUser (line 149) | def testVerifyToken_perUser(self, mock_time):
method testVerifyToken_perPass (line 162) | def testVerifyToken_perPass(self, mock_time):
method testGetByEmail (line 173) | def testGetByEmail(self):
method testGetByApiKey (line 178) | def testGetByApiKey(self):
FILE: scoreboard/tests/rest_test.py
function makeTestUser (line 34) | def makeTestUser():
function makeTestTeam (line 40) | def makeTestTeam(user):
function makeTestChallenges (line 47) | def makeTestChallenges():
class ConfigzTest (line 54) | class ConfigzTest(base.RestTestCase):
method testGetFails (line 58) | def testGetFails(self):
method testAdmin (line 66) | def testAdmin(self):
class PageTest (line 72) | class PageTest(base.RestTestCase):
method setUp (line 78) | def setUp(self):
method testGetAnonymous (line 88) | def testGetAnonymous(self):
method testGetNonExistent (line 95) | def testGetNonExistent(self):
method testDeletePage (line 100) | def testDeletePage(self):
method testCreatePage (line 108) | def testCreatePage(self):
method testCreatePageNonAdmin (line 120) | def testCreatePageNonAdmin(self):
method testUpdatePage (line 130) | def testUpdatePage(self):
class UpdateTeam (line 142) | class UpdateTeam(base.RestTestCase):
method createTeam (line 148) | def createTeam(self, teamname):
method changeTeam (line 153) | def changeTeam(self, tid, code):
method patchState (line 160) | def patchState(self, time='BEFORE'):
method restoreState (line 164) | def restoreState(self):
method testChangeTeam (line 169) | def testChangeTeam(self):
method testTeamChangeWorked (line 177) | def testTeamChangeWorked(self):
method testEmptyTeamIsDeleted (line 186) | def testEmptyTeamIsDeleted(self):
method testTeamWithSolvesNotDeleted (line 199) | def testTeamWithSolvesNotDeleted(self):
method testCantSwitchAfterStart (line 216) | def testCantSwitchAfterStart(self):
class AttachmentTest (line 224) | class AttachmentTest(base.RestTestCase):
method uploadFile (line 232) | def uploadFile(self, filename, text):
method fetchFile (line 241) | def fetchFile(self, aid):
method testUploadFile (line 245) | def testUploadFile(self):
method testQueryFile (line 254) | def testQueryFile(self):
method testFileQueryAID (line 260) | def testFileQueryAID(self):
method testFileQueryName (line 265) | def testFileQueryName(self):
method testFileChallengesEmpty (line 270) | def testFileChallengesEmpty(self):
method testRetrieveFile (line 275) | def testRetrieveFile(self):
method testFileRetrievalValue (line 281) | def testFileRetrievalValue(self):
method testFileDelete (line 287) | def testFileDelete(self):
method testDeletionRemovesFile (line 293) | def testDeletionRemovesFile(self):
method testFileUpdate (line 301) | def testFileUpdate(self):
method testUpdateChangesName (line 314) | def testUpdateChangesName(self):
class UserTest (line 328) | class UserTest(base.RestTestCase):
method testGetAnonymous (line 333) | def testGetAnonymous(self):
method testGetNonExistent (line 338) | def testGetNonExistent(self):
method testGetNonExistentAdmin (line 348) | def testGetNonExistentAdmin(self):
method testGetSelf (line 355) | def testGetSelf(self):
method testUpdateUser (line 365) | def testUpdateUser(self):
method testUpdateUserNoAccess (line 374) | def testUpdateUserNoAccess(self):
method testUpdateUserAdmin (line 382) | def testUpdateUserAdmin(self):
method testUpdateUsersNoTeams (line 394) | def testUpdateUsersNoTeams(self):
method testUpdateUserPromote (line 405) | def testUpdateUserPromote(self):
method testUpdateUserDemote (line 415) | def testUpdateUserDemote(self):
method testUpdateUserNoSelfPromotion (line 424) | def testUpdateUserNoSelfPromotion(self):
method testUpdateUserNoAnswers (line 433) | def testUpdateUserNoAnswers(self):
method testGetUsersNoAccess (line 447) | def testGetUsersNoAccess(self):
method testGetUsers (line 453) | def testGetUsers(self):
method default_data (line 467) | def default_data():
method testRegisterUserNewTeam (line 477) | def testRegisterUserNewTeam(self):
method testRegisterUserExistingTeam (line 489) | def testRegisterUserExistingTeam(self):
method testRegisterUserTeamWrongCode (line 507) | def testRegisterUserTeamWrongCode(self):
method testRegisterUserLoggedInFails (line 521) | def testRegisterUserLoggedInFails(self):
method testRegisterUserNoNick (line 527) | def testRegisterUserNoNick(self):
method testRegisterUserNoTeam (line 533) | def testRegisterUserNoTeam(self):
method testRegisterUserInviteKey (line 540) | def testRegisterUserInviteKey(self):
method testRegisterUserNoInviteKey (line 548) | def testRegisterUserNoInviteKey(self):
method testRegisterUserWrongInviteKey (line 556) | def testRegisterUserWrongInviteKey(self):
class TeamTest (line 566) | class TeamTest(base.RestTestCase):
method setUp (line 570) | def setUp(self):
method testGetTeam (line 577) | def testGetTeam(self):
method testGetTeamAnonymous (line 585) | def testGetTeamAnonymous(self):
method testGetTeamAdmin (line 590) | def testGetTeamAdmin(self):
method testUpdateTeamAdmin (line 597) | def testUpdateTeamAdmin(self):
method testGetTeamList (line 606) | def testGetTeamList(self):
class SessionTest (line 621) | class SessionTest(base.RestTestCase):
method testGetSessionAnonymous (line 625) | def testGetSessionAnonymous(self):
method testGetSessionAuthenticated (line 629) | def testGetSessionAuthenticated(self):
method testGetSessionAdmin (line 641) | def testGetSessionAdmin(self):
method testSessionLoginSucceeds (line 654) | def testSessionLoginSucceeds(self):
method testSessionLoginFailsBadPassword (line 674) | def testSessionLoginFailsBadPassword(self):
method testSessionLoginFailsBadUser (line 687) | def testSessionLoginFailsBadUser(self):
method testSessionLoginAlreadyLoggedIn (line 701) | def testSessionLoginAlreadyLoggedIn(self):
method testSessionLogout (line 723) | def testSessionLogout(self):
method testSessionLogoutAnonymous (line 732) | def testSessionLogoutAnonymous(self):
method testGetSessionWithApiKey (line 741) | def testGetSessionWithApiKey(self):
method testGetSessionWithBadApiKey (line 762) | def testGetSessionWithBadApiKey(self):
class ChallengeTest (line 784) | class ChallengeTest(base.RestTestCase):
method setUp (line 789) | def setUp(self):
method testGetListAnonymous (line 795) | def testGetListAnonymous(self):
method testGetListAuthenticated (line 801) | def testGetListAuthenticated(self):
method testGetListAdmin (line 808) | def testGetListAdmin(self):
method newChallengeData (line 814) | def newChallengeData(self):
method testCreateChallengeAnonymous (line 824) | def testCreateChallengeAnonymous(self):
method testCreateChallenge (line 834) | def testCreateChallenge(self):
method getUpdateData (line 844) | def getUpdateData(self):
method testUpdateChallenge (line 853) | def testUpdateChallenge(self):
method testUpdateChallengeAnonymous (line 861) | def testUpdateChallengeAnonymous(self):
method testGetSingleton (line 870) | def testGetSingleton(self):
method testGetSingletonAnonymous (line 877) | def testGetSingletonAnonymous(self):
method testDeleteChallenge (line 885) | def testDeleteChallenge(self):
method testDeleteChallengeAnonymous (line 889) | def testDeleteChallengeAnonymous(self):
class ScoreboardTest (line 897) | class ScoreboardTest(base.RestTestCase):
method setUp (line 901) | def setUp(self):
method testGetScoreboard (line 905) | def testGetScoreboard(self):
class AnswerTest (line 911) | class AnswerTest(base.RestTestCase):
method setUp (line 915) | def setUp(self):
method testSubmitAnonymous (line 925) | def testSubmitAnonymous(self):
method testSubmitAdmin_Regular (line 933) | def testSubmitAdmin_Regular(self):
method testSubmitAdmin_Override (line 941) | def testSubmitAdmin_Override(self):
method testSubmitCorrect (line 953) | def testSubmitCorrect(self):
method testSubmitIncorrect (line 963) | def testSubmitIncorrect(self):
method testSubmitDouble (line 975) | def testSubmitDouble(self):
method testSubmit_ProofOfWork (line 988) | def testSubmit_ProofOfWork(self):
method testSubmit_ProofOfWorkFails (line 1007) | def testSubmit_ProofOfWorkFails(self):
class ConfigTest (line 1027) | class ConfigTest(base.RestTestCase):
method makeTestGetConfig (line 1031) | def makeTestGetConfig(extra_keys=None):
class APIKeyTest (line 1067) | class APIKeyTest(base.RestTestCase):
method setUp (line 1071) | def setUp(self):
method testGetApiKey (line 1077) | def testGetApiKey(self):
method testUpdateApiKey (line 1087) | def testUpdateApiKey(self):
method testDelete_Own (line 1101) | def testDelete_Own(self):
method testDelete_All (line 1111) | def testDelete_All(self):
method testGetApiKey_Denied (line 1123) | def testGetApiKey_Denied(self):
method testUpdateApiKey_Denied (line 1131) | def testUpdateApiKey_Denied(self):
method testDeleteApiKey_All_Denied (line 1139) | def testDeleteApiKey_All_Denied(self):
class NewsTest (line 1148) | class NewsTest(base.RestTestCase):
method setUp (line 1152) | def setUp(self):
method testGetNews (line 1160) | def testGetNews(self):
method testGetNewsAuthenticated (line 1169) | def testGetNewsAuthenticated(self):
method testCreateNews (line 1175) | def testCreateNews(self):
method testCreateNewsAdmin (line 1185) | def testCreateNewsAdmin(self):
method testCreateTeamNewsAdmin (line 1196) | def testCreateTeamNewsAdmin(self):
class CTFTimeTest (line 1212) | class CTFTimeTest(base.RestTestCase):
method testGetScoreboard (line 1216) | def testGetScoreboard(self):
FILE: scoreboard/tests/utils_test.py
class NormalizeInputTest (line 20) | class NormalizeInputTest(base.BaseTestCase):
method testNormalizeInput (line 22) | def testNormalizeInput(self):
class ProofOfWorkTest (line 29) | class ProofOfWorkTest(base.BaseTestCase):
method testValidateProofOfWork_Succeeds (line 31) | def testValidateProofOfWork_Succeeds(self):
method testValidateProofOfWork_SucceedsUnicode (line 37) | def testValidateProofOfWork_SucceedsUnicode(self):
method testValidateProofOfWork_FailsWrongVal (line 43) | def testValidateProofOfWork_FailsWrongVal(self):
method testValidateProofOfWork_FailsWrongKey (line 49) | def testValidateProofOfWork_FailsWrongKey(self):
method testValidateProofOfWork_FailsMoreBits (line 55) | def testValidateProofOfWork_FailsMoreBits(self):
method testValidateProofOfWork_FailsInvalidBase64 (line 61) | def testValidateProofOfWork_FailsInvalidBase64(self):
FILE: scoreboard/tests/validators_test.py
class ChallengeStub (line 22) | class ChallengeStub(object):
method __init__ (line 24) | def __init__(self, answer, validator='static_pbkdf2'):
class StaticValidatorTest (line 29) | class StaticValidatorTest(base.BaseTestCase):
method testStaticValidator (line 31) | def testStaticValidator(self):
class CaseStaticValidatorTest (line 40) | class CaseStaticValidatorTest(base.BaseTestCase):
method testCaseStaticValidator (line 42) | def testCaseStaticValidator(self):
class RegexValidatorTest (line 57) | class RegexValidatorTest(base.BaseTestCase):
method makeValidator (line 59) | def makeValidator(self, regex):
method testRegexWorks (line 64) | def testRegexWorks(self):
method testRegexChangeWorks (line 72) | def testRegexChangeWorks(self):
class RegexCaseValidatorTest (line 81) | class RegexCaseValidatorTest(base.BaseTestCase):
method makeValidator (line 83) | def makeValidator(self, regex):
method testRegexWorks (line 88) | def testRegexWorks(self):
method testRegexChangeWorks (line 96) | def testRegexChangeWorks(self):
class NonceValidatorTest (line 105) | class NonceValidatorTest(base.BaseTestCase):
method setUp (line 107) | def setUp(self):
method testNonceValidator_Basic (line 117) | def testNonceValidator_Basic(self):
method testNonceValidator_Dupe (line 121) | def testNonceValidator_Dupe(self):
FILE: scoreboard/utils.py
function is_logged_in (line 45) | def is_logged_in():
function login_required (line 52) | def login_required(f):
function admin_required (line 63) | def admin_required(f):
function team_required (line 83) | def team_required(f):
function is_admin (line 96) | def is_admin():
function session_for_user (line 104) | def session_for_user(user):
function get_required_field (line 119) | def get_required_field(name, verbose_name=None):
function parse_bool (line 129) | def parse_bool(b):
function compare_digest (line 134) | def compare_digest(a, b):
function absolute_url (line 141) | def absolute_url(path):
function generate_id (line 146) | def generate_id():
function normalize_input (line 151) | def normalize_input(answer):
function validate_proof_of_work (line 156) | def validate_proof_of_work(val, key, nbits):
function urlsafe_b64decode_nopadding (line 184) | def urlsafe_b64decode_nopadding(val):
function to_bytes (line 190) | def to_bytes(val):
class GameTime (line 202) | class GameTime(object):
method setup (line 206) | def setup(cls):
method countdown (line 215) | def countdown(cls, end=False):
method state (line 223) | def state(cls, now=None):
method open (line 233) | def open(cls, after_end=False):
method over (line 241) | def over(cls):
method require_open (line 247) | def require_open(cls, f, after_end=False, or_admin=True):
method require_started (line 258) | def require_started(cls, f):
method require_not_started (line 263) | def require_not_started(cls, f):
method require_submittable (line 273) | def require_submittable(cls, f):
method message (line 279) | def message(cls):
method _parsedate (line 288) | def _parsedate(datestr):
FILE: scoreboard/validators/__init__.py
function GetDefaultValidator (line 33) | def GetDefaultValidator():
function GetValidatorForChallenge (line 37) | def GetValidatorForChallenge(challenge):
function ValidatorNames (line 42) | def ValidatorNames():
function ValidatorMeta (line 46) | def ValidatorMeta():
function IsValidator (line 57) | def IsValidator(name):
FILE: scoreboard/validators/base.py
class BaseValidator (line 16) | class BaseValidator(object):
method __init__ (line 23) | def __init__(self, challenge):
method validate_answer (line 26) | def validate_answer(self, answer, team):
method change_answer (line 32) | def change_answer(self, answer):
FILE: scoreboard/validators/nonce.py
class BaseNonceValidator (line 31) | class BaseNonceValidator(base.BaseValidator):
method __init__ (line 38) | def __init__(self, *args, **kwargs):
method _decode (line 47) | def _decode(buf):
method _encode (line 51) | def _encode(buf):
method validate_answer (line 54) | def validate_answer(self, answer, team):
method compute_authenticator (line 81) | def compute_authenticator(self, nonce):
method make_answer (line 89) | def make_answer(self, nonce):
method unpack_nonce (line 99) | def unpack_nonce(cls, nonce):
class Base32Validator (line 104) | class Base32Validator(BaseNonceValidator):
method __init__ (line 106) | def __init__(self, *args, **kwargs):
method _encode (line 112) | def _encode(buf):
method _decode (line 116) | def _decode(buf):
class Nonce_16_64_Base32_Validator (line 121) | class Nonce_16_64_Base32_Validator(Base32Validator):
class Nonce_24_56_Base32_Validator (line 128) | class Nonce_24_56_Base32_Validator(Base32Validator):
class Nonce_32_88_Base32_Validator (line 135) | class Nonce_32_88_Base32_Validator(Base32Validator):
FILE: scoreboard/validators/per_team.py
class PerTeamValidator (line 22) | class PerTeamValidator(base.BaseValidator):
method validate_answer (line 29) | def validate_answer(self, answer, team):
method construct_mac (line 36) | def construct_mac(self, team):
FILE: scoreboard/validators/regex.py
class RegexValidator (line 20) | class RegexValidator(base.BaseValidator):
method validate_answer (line 31) | def validate_answer(self, answer, unused_team):
class RegexCaseValidator (line 38) | class RegexCaseValidator(RegexValidator):
FILE: scoreboard/validators/static_pbkdf2.py
class StaticPBKDF2Validator (line 21) | class StaticPBKDF2Validator(base.BaseValidator):
method validate_answer (line 26) | def validate_answer(self, answer, unused_team):
method change_answer (line 33) | def change_answer(self, answer):
class CaseStaticPBKDF2Validator (line 37) | class CaseStaticPBKDF2Validator(StaticPBKDF2Validator):
method validate_answer (line 42) | def validate_answer(self, answer, team):
method change_answer (line 48) | def change_answer(self, answer):
FILE: scoreboard/views.py
function handle_404 (line 29) | def handle_404(ex):
function render_pwreset (line 43) | def render_pwreset(unused):
function render_index (line 49) | def render_index():
function download (line 68) | def download(filename):
function createdb (line 86) | def createdb():
FILE: static/js/services/session.js
function getss (line 104) | function getss(){
FILE: static/third_party/angular/angular-resource.js
function isValidDottedPath (line 15) | function isValidDottedPath(path) {
function lookupDottedPath (line 20) | function lookupDottedPath(obj, path) {
function shallowClearAndCopy (line 35) | function shallowClearAndCopy(src, dst) {
function Route (line 601) | function Route(template, defaults) {
function resourceFactory (line 681) | function resourceFactory(url, paramDefaults, actions, options) {
FILE: static/third_party/angular/angular-route.js
function shallowCopy (line 15) | function shallowCopy(src, dst) {
function routeToRegExp (line 49) | function routeToRegExp(path, opts) {
function $RouteProvider (line 129) | function $RouteProvider() {
function instantiateRoute (line 934) | function instantiateRoute($injector) {
function $RouteParamsProvider (line 978) | function $RouteParamsProvider() {
function ngViewFactory (line 1160) | function ngViewFactory($route, $anchorScroll, $animate) {
function ngViewFillContentFactory (line 1237) | function ngViewFillContentFactory($compile, $controller, $route) {
FILE: static/third_party/angular/angular-sanitize.js
function $SanitizeProvider (line 152) | function $SanitizeProvider() {
function sanitizeText (line 703) | function sanitizeText(chars) {
function addText (line 883) | function addText(text) {
function addLink (line 890) | function addLink(url, text) {
FILE: static/third_party/angular/angular.js
function errorHandlingConfig (line 46) | function errorHandlingConfig(config) {
function isValidObjectMaxDepth (line 64) | function isValidObjectMaxDepth(maxDepth) {
function minErr (line 99) | function minErr(module, ErrorConstructor) {
function isArrayLike (line 318) | function isArrayLike(obj) {
function forEach (line 374) | function forEach(obj, iterator, context) {
function forEachSorted (line 416) | function forEachSorted(obj, iterator, context) {
function reverseParams (line 430) | function reverseParams(iteratorFn) {
function nextUid (line 444) | function nextUid() {
function setHashKey (line 454) | function setHashKey(obj, h) {
function baseExtend (line 463) | function baseExtend(dst, objs, deep) {
function extend (line 515) | function extend(dst) {
function merge (line 554) | function merge(dst) {
function toInt (line 560) | function toInt(str) {
function inherit (line 570) | function inherit(parent, extra) {
function noop (line 590) | function noop() {}
function identity (line 622) | function identity($) {return $;}
function valueFn (line 626) | function valueFn(value) {return function valueRef() {return value;};}
function hasCustomToString (line 628) | function hasCustomToString(obj) {
function isUndefined (line 645) | function isUndefined(value) {return typeof value === 'undefined';}
function isDefined (line 660) | function isDefined(value) {return typeof value !== 'undefined';}
function isObject (line 676) | function isObject(value) {
function isBlankObject (line 687) | function isBlankObject(value) {
function isString (line 704) | function isString(value) {return typeof value === 'string';}
function isNumber (line 725) | function isNumber(value) {return typeof value === 'number';}
function isDate (line 740) | function isDate(value) {
function isArray (line 757) | function isArray(arr) {
function isError (line 769) | function isError(value) {
function isFunction (line 791) | function isFunction(value) {return typeof value === 'function';}
function isRegExp (line 801) | function isRegExp(value) {
function isWindow (line 813) | function isWindow(obj) {
function isScope (line 818) | function isScope(obj) {
function isFile (line 823) | function isFile(obj) {
function isFormData (line 828) | function isFormData(obj) {
function isBlob (line 833) | function isBlob(obj) {
function isBoolean (line 838) | function isBoolean(value) {
function isPromiseLike (line 843) | function isPromiseLike(obj) {
function isTypedArray (line 849) | function isTypedArray(value) {
function isArrayBuffer (line 853) | function isArrayBuffer(obj) {
function isElement (line 885) | function isElement(node) {
function makeMap (line 895) | function makeMap(str) {
function nodeName_ (line 904) | function nodeName_(element) {
function includes (line 908) | function includes(array, obj) {
function arrayRemove (line 912) | function arrayRemove(array, value) {
function copy (line 1007) | function copy(source, destination, maxDepth) {
function simpleCompare (line 1154) | function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }
function equals (line 1220) | function equals(o1, o2) {
function noUnsafeEval (line 1286) | function noUnsafeEval() {
function concat (line 1351) | function concat(array1, array2, index) {
function sliceArgs (line 1355) | function sliceArgs(args, startIndex) {
function bind (line 1377) | function bind(self, fn) {
function toJsonReplacer (line 1398) | function toJsonReplacer(key, value) {
function toJson (line 1451) | function toJson(obj, pretty) {
function fromJson (line 1472) | function fromJson(json) {
function timezoneToOffset (line 1480) | function timezoneToOffset(timezone, fallback) {
function addDateMinutes (line 1489) | function addDateMinutes(date, minutes) {
function convertTimezoneToLocal (line 1496) | function convertTimezoneToLocal(date, timezone, reverse) {
function startingTag (line 1507) | function startingTag(element) {
function tryDecodeURIComponent (line 1532) | function tryDecodeURIComponent(value) {
function parseKeyValue (line 1545) | function parseKeyValue(/**string*/keyValue) {
function toKeyValue (line 1572) | function toKeyValue(obj) {
function encodeUriSegment (line 1600) | function encodeUriSegment(val) {
function encodeUriQuery (line 1619) | function encodeUriQuery(val, pctEncodeSpaces) {
function getNgAttribute (line 1631) | function getNgAttribute(element, ngAttr) {
function allowAutoBootstrap (line 1642) | function allowAutoBootstrap(document) {
function angularInit (line 1836) | function angularInit(element, bootstrap) {
function bootstrap (line 1929) | function bootstrap(element, modules, config) {
function reloadWithDebugInfo (line 2007) | function reloadWithDebugInfo() {
function getTestability (line 2020) | function getTestability(rootElement) {
function snake_case (line 2030) | function snake_case(name, separator) {
function bindJQuery (line 2038) | function bindJQuery() {
function assertArg (line 2092) | function assertArg(arg, name, reason) {
function assertArgFn (line 2099) | function assertArgFn(arg, name, acceptArrayAnnotation) {
function assertNotHasOwnProperty (line 2114) | function assertNotHasOwnProperty(name, context) {
function getter (line 2128) | function getter(obj, path, bindFnToScope) {
function getBlockNodes (line 2152) | function getBlockNodes(nodes) {
function createMap (line 2182) | function createMap() {
function stringify (line 2186) | function stringify(value) {
function setupModuleLoader (line 2223) | function setupModuleLoader(window) {
function shallowCopy (line 2633) | function shallowCopy(src, dst) {
function serializeObject (line 2655) | function serializeObject(obj, maxDepth) {
function toDebugString (line 2678) | function toDebugString(obj, maxDepth) {
function publishExternalAPI (line 2816) | function publishExternalAPI(angular) {
function jqNextId (line 3099) | function jqNextId() { return ++jqId; }
function cssKebabToCamel (line 3112) | function cssKebabToCamel(name) {
function fnCamelCaseReplace (line 3116) | function fnCamelCaseReplace(all, letter) {
function kebabToCamel (line 3124) | function kebabToCamel(name) {
function jqLiteIsTextNode (line 3149) | function jqLiteIsTextNode(html) {
function jqLiteAcceptsData (line 3153) | function jqLiteAcceptsData(node) {
function jqLiteHasData (line 3160) | function jqLiteHasData(node) {
function jqLiteBuildFragment (line 3167) | function jqLiteBuildFragment(html, context) {
function jqLiteParseHTML (line 3204) | function jqLiteParseHTML(html, context) {
function jqLiteWrapNode (line 3219) | function jqLiteWrapNode(node, wrapper) {
function JQLite (line 3237) | function JQLite(element) {
function jqLiteClone (line 3264) | function jqLiteClone(element) {
function jqLiteDealoc (line 3268) | function jqLiteDealoc(element, onlyDescendants) {
function isEmptyObject (line 3276) | function isEmptyObject(obj) {
function removeIfEmptyData (line 3285) | function removeIfEmptyData(element) {
function jqLiteOff (line 3298) | function jqLiteOff(element, type, fn, unsupported) {
function jqLiteRemoveData (line 3338) | function jqLiteRemoveData(element, name) {
function jqLiteExpandoStore (line 3354) | function jqLiteExpandoStore(element, createIfNecessary) {
function jqLiteData (line 3367) | function jqLiteData(element, key, value) {
function jqLiteHasClass (line 3396) | function jqLiteHasClass(element, selector) {
function jqLiteRemoveClass (line 3402) | function jqLiteRemoveClass(element, cssClasses) {
function jqLiteAddClass (line 3419) | function jqLiteAddClass(element, cssClasses) {
function jqLiteAddNodes (line 3439) | function jqLiteAddNodes(root, elements) {
function jqLiteController (line 3465) | function jqLiteController(element, name) {
function jqLiteInheritedData (line 3469) | function jqLiteInheritedData(element, name, value) {
function jqLiteEmpty (line 3489) | function jqLiteEmpty(element) {
function jqLiteRemove (line 3496) | function jqLiteRemove(element, keepData) {
function jqLiteDocumentLoaded (line 3503) | function jqLiteDocumentLoaded(action, win) {
function jqLiteReady (line 3516) | function jqLiteReady(fn) {
function getBooleanAttrName (line 3580) | function getBooleanAttrName(element, name) {
function getAliasedAttrName (line 3588) | function getAliasedAttrName(name) {
function getText (line 3686) | function getText(element, value) {
function createEventHandler (line 3771) | function createEventHandler(element, events) {
function defaultHandlerWrapper (line 3823) | function defaultHandlerWrapper(element, event, handler) {
function specialMouseHandlerWrapper (line 3827) | function specialMouseHandlerWrapper(target, event, handler) {
function $$jqLiteProvider (line 4078) | function $$jqLiteProvider() {
function hashKey (line 4109) | function hashKey(obj, nextUidFn) {
function NgMapShim (line 4134) | function NgMapShim() {
function stringifyFn (line 4270) | function stringifyFn(fn) {
function extractArgs (line 4274) | function extractArgs(fn) {
function anonFn (line 4280) | function anonFn(fn) {
function annotate (line 4290) | function annotate(fn, strictDi, name) {
function createInjector (line 4903) | function createInjector(modulesToLoad, strictDi) {
function $AnchorScrollProvider (line 5183) | function $AnchorScrollProvider() {
function mergeClasses (line 5451) | function mergeClasses(a,b) {
function extractElementNode (line 5460) | function extractElementNode(element) {
function splitClasses (line 5469) | function splitClasses(classes) {
function prepareAnimateOptions (line 5494) | function prepareAnimateOptions(options) {
function updateData (line 5545) | function updateData(data, classes, value) {
function handleCSSClassChanges (line 5560) | function handleCSSClassChanges() {
function addRemoveClassesPostDigest (line 5593) | function addRemoveClassesPostDigest(element, add, remove) {
function domInsert (line 5759) | function domInsert(element, parentElement, afterElement) {
function waitForTick (line 6246) | function waitForTick(fn) {
function next (line 6285) | function next() {
function onProgress (line 6309) | function onProgress(response) {
function AnimateRunner (line 6317) | function AnimateRunner(host) {
function run (line 6475) | function run() {
function applyAnimationContents (line 6486) | function applyAnimationContents() {
function getHash (line 6506) | function getHash(url) {
function trimEmptyHash (line 6511) | function trimEmptyHash(url) {
function Browser (line 6536) | function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
function $BrowserProvider (line 6844) | function $BrowserProvider() {
function $CacheFactoryProvider (line 6933) | function $CacheFactoryProvider() {
function $TemplateCacheProvider (line 7252) | function $TemplateCacheProvider() {
function UNINITIALIZED_VALUE (line 8623) | function UNINITIALIZED_VALUE() {}
function $CompileProvider (line 8634) | function $CompileProvider($provide, $$sanitizeUriProvider) {
function SimpleChange (line 11452) | function SimpleChange(previous, current) {
function directiveNormalize (line 11466) | function directiveNormalize(name) {
function nodesetLinkingFn (line 11519) | function nodesetLinkingFn(
function directiveLinkingFn (line 11526) | function directiveLinkingFn(
function tokenDifference (line 11534) | function tokenDifference(str1, str2) {
function removeComments (line 11550) | function removeComments(jqNodes) {
function identifierForController (line 11572) | function identifierForController(controller, ident) {
function $ControllerProvider (line 11593) | function $ControllerProvider() {
function $DocumentProvider (line 11766) | function $DocumentProvider() {
function $$IsDocumentHiddenProvider (line 11778) | function $$IsDocumentHiddenProvider() {
function $ExceptionHandlerProvider (line 11843) | function $ExceptionHandlerProvider() {
function serializeValue (line 11884) | function serializeValue(v) {
function $HttpParamSerializerProvider (line 11893) | function $HttpParamSerializerProvider() {
function $HttpParamSerializerJQLikeProvider (line 11931) | function $HttpParamSerializerJQLikeProvider() {
function defaultHttpResponseTransform (line 12007) | function defaultHttpResponseTransform(data, headers) {
function isJsonLike (line 12033) | function isJsonLike(str) {
function parseHeaders (line 12044) | function parseHeaders(headers) {
function headersGetter (line 12080) | function headersGetter(headers) {
function transformData (line 12110) | function transformData(data, headers, status, fns) {
function isSuccess (line 12123) | function isSuccess(status) {
function $HttpProvider (line 12136) | function $HttpProvider() {
function $xhrFactoryProvider (line 13431) | function $xhrFactoryProvider() {
function $HttpBackendProvider (line 13457) | function $HttpBackendProvider() {
function createHttpBackend (line 13463) | function createHttpBackend($browser, createXhr, $browserDefer, callbacks...
function $InterpolateProvider (line 13698) | function $InterpolateProvider() {
function $IntervalProvider (line 14054) | function $IntervalProvider() {
function $$IntervalFactoryProvider (line 14232) | function $$IntervalFactoryProvider() {
function createCallback (line 14292) | function createCallback(callbackId) {
function encodePath (line 14383) | function encodePath(path) {
function decodePath (line 14395) | function decodePath(path, html5Mode) {
function normalizePath (line 14410) | function normalizePath(pathValue, searchValue, hashValue) {
function parseAbsoluteUrl (line 14418) | function parseAbsoluteUrl(absoluteUrl, locationObj) {
function parseAppUrl (line 14427) | function parseAppUrl(url, locationObj, html5Mode) {
function startsWith (line 14449) | function startsWith(str, search) {
function stripBaseUrl (line 14460) | function stripBaseUrl(base, url) {
function stripHash (line 14466) | function stripHash(url) {
function stripFile (line 14471) | function stripFile(url) {
function serverBase (line 14476) | function serverBase(url) {
function LocationHtml5Url (line 14490) | function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
function LocationHashbangUrl (line 14562) | function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
function LocationHashbangInHtml5Url (line 14666) | function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
function locationGetter (line 15042) | function locationGetter(property) {
function locationGetterSetter (line 15049) | function locationGetterSetter(property, preprocess) {
function $LocationProvider (line 15097) | function $LocationProvider() {
function $LogProvider (line 15457) | function $LogProvider() {
function getStringValue (line 15603) | function getStringValue(name) {
function ifDefined (line 16180) | function ifDefined(v, d) {
function plusFn (line 16184) | function plusFn(l, r) {
function isStateless (line 16190) | function isStateless($filter, filterName) {
function isPure (line 16199) | function isPure(node, parentIsPure) {
function findConstantAndWatchExpressions (line 16224) | function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
function getInputs (line 16337) | function getInputs(body) {
function isAssignable (line 16345) | function isAssignable(ast) {
function assignableAST (line 16349) | function assignableAST(ast) {
function isLiteral (line 16355) | function isLiteral(ast) {
function isConstant (line 16363) | function isConstant(ast) {
function ASTCompiler (line 16367) | function ASTCompiler($filter) {
function ASTInterpreter (line 16833) | function ASTInterpreter($filter) {
function Parser (line 17207) | function Parser(lexer, $filter, options) {
function getValueOf (line 17240) | function getValueOf(value) {
function $ParseProvider (line 17296) | function $ParseProvider() {
function $QProvider (line 17813) | function $QProvider() {
function $$QProvider (line 17845) | function $$QProvider() {
function qFactory (line 17873) | function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
function isStateExceptionHandled (line 18265) | function isStateExceptionHandled(state) {
function markQStateExceptionHandled (line 18268) | function markQStateExceptionHandled(state) {
function markQExceptionHandled (line 18271) | function markQExceptionHandled(q) {
function $$RAFProvider (line 18282) | function $$RAFProvider() { //rAF
function $RootScopeProvider (line 18381) | function $RootScopeProvider() {
function $$SanitizeUriProvider (line 19831) | function $$SanitizeUriProvider() {
function snakeToCamel (line 19950) | function snakeToCamel(name) {
function adjustMatcher (line 19955) | function adjustMatcher(matcher) {
function adjustMatchers (line 19983) | function adjustMatchers(matchers) {
function $SceDelegateProvider (line 20084) | function $SceDelegateProvider() {
function $SceProvider (line 20680) | function $SceProvider() {
function $SnifferProvider (line 21106) | function $SnifferProvider() {
function $$TaskTrackerFactoryProvider (line 21187) | function $$TaskTrackerFactoryProvider() {
function TaskTracker (line 21191) | function TaskTracker(log) {
function $TemplateRequestProvider (line 21306) | function $TemplateRequestProvider() {
function $$TestabilityProvider (line 21416) | function $$TestabilityProvider() {
function $TimeoutProvider (line 21542) | function $TimeoutProvider() {
function urlResolve (line 21712) | function urlResolve(url) {
function urlIsSameOrigin (line 21755) | function urlIsSameOrigin(requestUrl) {
function urlIsSameOriginAsBaseUrl (line 21769) | function urlIsSameOriginAsBaseUrl(requestUrl) {
function urlIsAllowedOriginFactory (line 21782) | function urlIsAllowedOriginFactory(whitelistedOriginUrls) {
function urlsAreSameOrigin (line 21811) | function urlsAreSameOrigin(url1, url2) {
function getBaseUrl (line 21823) | function getBaseUrl() {
function $WindowProvider (line 21882) | function $WindowProvider() {
function $$CookieReader (line 21895) | function $$CookieReader($document) {
function $$CookieReaderProvider (line 21946) | function $$CookieReaderProvider() {
function $FilterProvider (line 22057) | function $FilterProvider($provide) {
function filterFilter (line 22257) | function filterFilter() {
function createPredicateFn (line 22294) | function createPredicateFn(expression, comparator, anyPropertyKey, match...
function deepCompare (line 22331) | function deepCompare(actual, expected, comparator, anyPropertyKey, match...
function getTypeForFilter (line 22383) | function getTypeForFilter(val) {
function currencyFilter (line 22444) | function currencyFilter($locale) {
function numberFilter (line 22521) | function numberFilter($locale) {
function parse (line 22546) | function parse(numStr) {
function roundNumber (line 22601) | function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
function formatNumber (line 22676) | function formatNumber(number, pattern, groupSep, decimalSep, fractionSiz...
function padNumber (line 22742) | function padNumber(num, digits, trim, negWrap) {
function dateGetter (line 22761) | function dateGetter(name, size, offset, trim, negWrap) {
function dateStrGetter (line 22773) | function dateStrGetter(name, shortForm, standAlone) {
function timeZoneGetter (line 22783) | function timeZoneGetter(date, formats, offset) {
function getFirstThursdayOfYear (line 22793) | function getFirstThursdayOfYear(year) {
function getThursdayThisWeek (line 22801) | function getThursdayThisWeek(datetime) {
function weekGetter (line 22807) | function weekGetter(size) {
function ampmGetter (line 22819) | function ampmGetter(date, formats) {
function eraGetter (line 22823) | function eraGetter(date, formats) {
function longEraGetter (line 22827) | function longEraGetter(date, formats) {
function dateFilter (line 22965) | function dateFilter($locale) {
function jsonFilter (line 23072) | function jsonFilter() {
function limitToFilter (line 23221) | function limitToFilter() {
function sliceFn (line 23248) | function sliceFn(input, begin, end) {
function orderByFilter (line 23814) | function orderByFilter($parse) {
function ngDirective (line 23960) | function ngDirective(directive) {
function defaultLinkFn (line 24356) | function defaultLinkFn(scope, element, attr) {
function nullFormRenameControl (line 24466) | function nullFormRenameControl(control, name) {
function FormController (line 24520) | function FormController($element, $attrs, $scope, $animate, $interpolate) {
function getSetter (line 25058) | function getSetter(expression) {
function setupValidity (line 25074) | function setupValidity(instance) {
function addSetValidityMethod (line 25078) | function addSetValidityMethod(context) {
function isObjectEmpty (line 25165) | function isObjectEmpty(obj) {
function stringBasedInputType (line 26453) | function stringBasedInputType(ctrl) {
function textInputType (line 26459) | function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function baseInputType (line 26464) | function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function weekParser (line 26584) | function weekParser(isoWeek, existingDate) {
function createDateParser (line 26616) | function createDateParser(regexp, mapping) {
function createDateInputType (line 26674) | function createDateInputType(type, regexp, parseDate, format) {
function badInputChecker (line 26795) | function badInputChecker(scope, element, attr, ctrl, parserName) {
function numberFormatterParser (line 26811) | function numberFormatterParser(ctrl) {
function parseNumberAttrVal (line 26831) | function parseNumberAttrVal(val) {
function isNumberInteger (line 26838) | function isNumberInteger(num) {
function countDecimals (line 26846) | function countDecimals(num) {
function isValidForStep (line 26866) | function isValidForStep(viewValue, stepBase, step) {
function numberInputType (line 26897) | function numberInputType(scope, element, attr, ctrl, $sniffer, $browser,...
function rangeInputType (line 26962) | function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function urlInputType (line 27108) | function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function emailInputType (line 27120) | function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function radioInputType (line 27132) | function radioInputType(scope, element, attr, ctrl) {
function parseConstantExpr (line 27163) | function parseConstantExpr($parse, context, name, expression, fallback) {
function checkboxInputType (line 27176) | function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browse...
function updateElementValue (line 27519) | function updateElementValue(element, attr, value) {
function classDirective (line 27838) | function classDirective(name, selector) {
function createEventDirective (line 28939) | function createEventDirective($parse, $rootScope, $exceptionHandler, dir...
function NgModelController (line 30282) | function NgModelController($scope, $exceptionHandler, $attr, $element, $...
function processParseErrors (line 30645) | function processParseErrors() {
function processSyncValidators (line 30667) | function processSyncValidators() {
function processAsyncValidators (line 30683) | function processAsyncValidators() {
function setValidity (line 30709) | function setValidity(name, isValid) {
function validationDone (line 30715) | function validationDone(allValid) {
function writeToModelIfNeeded (line 30801) | function writeToModelIfNeeded() {
function setupModelWatcher (line 31103) | function setupModelWatcher(ctrl) {
function setTouched (line 31393) | function setTouched() {
function ModelOptions (line 31422) | function ModelOptions(options) {
function NgModelOptionsController (line 31958) | function NgModelOptionsController($attrs, $scope) {
function defaults (line 31983) | function defaults(dst, src) {
function parseOptionsExpression (line 32274) | function parseOptionsExpression(optionsExp, selectElement, scope) {
function ngOptionsPostLink (line 32437) | function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
function updateElementText (line 32972) | function updateElementText(newText) {
function ngTranscludeCloneAttachFn (line 34857) | function ngTranscludeCloneAttachFn(clone, transcludedScope) {
function useFallbackContent (line 34868) | function useFallbackContent() {
function notWhitespace (line 34876) | function notWhitespace(nodes) {
function setOptionSelectedStatus (line 34941) | function setOptionSelectedStatus(optionEl, value) {
function scheduleRender (line 35291) | function scheduleRender() {
function scheduleViewValueUpdate (line 35301) | function scheduleViewValueUpdate(renderAfter) {
function selectPreLink (line 35689) | function selectPreLink(scope, element, attr, ctrls) {
function selectPostLink (line 35774) | function selectPostLink(scope, element, attrs, ctrls) {
function parsePatternAttr (line 36245) | function parsePatternAttr(regex, patternExp, elm) {
function parseLength (line 36261) | function parseLength(val) {
function getDecimals (line 36282) | function getDecimals(n) {
function getVF (line 36288) | function getVF(n, opt_precision) {
FILE: static/third_party/bootstrap/bootstrap.js
function _defineProperties (line 14) | function _defineProperties(target, props) {
function _createClass (line 24) | function _createClass(Constructor, protoProps, staticProps) {
function _defineProperty (line 30) | function _defineProperty(obj, key, value) {
function _objectSpread (line 45) | function _objectSpread(target) {
function _inheritsLoose (line 64) | function _inheritsLoose(subClass, superClass) {
function toType (line 86) | function toType(obj) {
function getSpecialTransitionEndEvent (line 90) | function getSpecialTransitionEndEvent() {
function transitionEndEmulator (line 104) | function transitionEndEmulator(duration) {
function setTransitionEndSupport (line 119) | function setTransitionEndSupport() {
function Alert (line 260) | function Alert(element) {
function Button (line 428) | function Button(element) {
function Carousel (line 635) | function Carousel(element, config) {
function Collapse (line 1195) | function Collapse(element, config) {
function microtaskDebounce (line 1539) | function microtaskDebounce(fn) {
function taskDebounce (line 1553) | function taskDebounce(fn) {
function isFunction (line 1586) | function isFunction(functionToCheck) {
function getStyleComputedProperty (line 1598) | function getStyleComputedProperty(element, property) {
function getParentNode (line 1615) | function getParentNode(element) {
function getScrollParent (line 1629) | function getScrollParent(element) {
function isIE (line 1667) | function isIE(version) {
function getOffsetParent (line 1684) | function getOffsetParent(element) {
function isOffsetContainer (line 1713) | function isOffsetContainer(element) {
function getRoot (line 1729) | function getRoot(node) {
function findCommonOffsetParent (line 1745) | function findCommonOffsetParent(element1, element2) {
function getScroll (line 1789) | function getScroll(element) {
function includeScroll (line 1813) | function includeScroll(rect, element) {
function getBordersSize (line 1836) | function getBordersSize(styles, axis) {
function getSize (line 1843) | function getSize(axis, body, html, computedStyle) {
function getWindowSizes (line 1847) | function getWindowSizes(document) {
function defineProperties (line 1865) | function defineProperties(target, props) {
function getClientRect (line 1922) | function getClientRect(offsets) {
function getBoundingClientRect (line 1936) | function getBoundingClientRect(element) {
function getOffsetRectRelativeToArbitraryNode (line 1985) | function getOffsetRectRelativeToArbitraryNode(children, parent) {
function getViewportOffsetRectRelativeToArtbitraryNode (line 2037) | function getViewportOffsetRectRelativeToArtbitraryNode(element) {
function isFixed (line 2066) | function isFixed(element) {
function getFixedPositionOffsetParent (line 2089) | function getFixedPositionOffsetParent(element) {
function getBoundaries (line 2112) | function getBoundaries(popper, reference, padding, boundariesElement) {
function getArea (line 2166) | function getArea(_ref) {
function computeAutoPlacement (line 2182) | function computeAutoPlacement(placement, refRect, popper, reference, bou...
function getReferenceOffsets (line 2243) | function getReferenceOffsets(state, popper, reference) {
function getOuterSizes (line 2257) | function getOuterSizes(element) {
function getOppositePlacement (line 2276) | function getOppositePlacement(placement) {
function getPopperOffsets (line 2293) | function getPopperOffsets(popper, referenceOffsets, placement) {
function find (line 2331) | function find(arr, check) {
function findIndex (line 2350) | function findIndex(arr, prop, value) {
function runModifiers (line 2375) | function runModifiers(modifiers, data, ends) {
function update (line 2405) | function update() {
function isModifierEnabled (line 2457) | function isModifierEnabled(modifiers, modifierName) {
function getSupportedPropertyName (line 2472) | function getSupportedPropertyName(property) {
function destroy (line 2491) | function destroy() {
function getWindow (line 2521) | function getWindow(element) {
function attachToScrollParents (line 2526) | function attachToScrollParents(scrollParent, event, callback, scrollPare...
function setupEventListeners (line 2543) | function setupEventListeners(reference, options, state, updateBound) {
function enableEventListeners (line 2563) | function enableEventListeners() {
function removeEventListeners (line 2575) | function removeEventListeners(reference, state) {
function disableEventListeners (line 2599) | function disableEventListeners() {
function isNumeric (line 2613) | function isNumeric(n) {
function setStyles (line 2625) | function setStyles(element, styles) {
function setAttributes (line 2644) | function setAttributes(element, attributes) {
function applyStyle (line 2664) | function applyStyle(data) {
function applyStyleOnLoad (line 2693) | function applyStyleOnLoad(reference, popper, options, modifierOptions, s...
function getRoundedOffsets (line 2730) | function getRoundedOffsets(data, shouldRound) {
function computeStyle (line 2769) | function computeStyle(data, options) {
function isModifierRequired (line 2870) | function isModifierRequired(modifiers, requestingName, requestedName) {
function arrow (line 2895) | function arrow(data, options) {
function getOppositeVariation (line 2977) | function getOppositeVariation(variation) {
function clockwise (line 3032) | function clockwise(placement) {
function flip (line 3053) | function flip(data, options) {
function keepTogether (line 3143) | function keepTogether(data) {
function toValue (line 3177) | function toValue(str, measurement, popperOffsets, referenceOffsets) {
function parseOffset (line 3229) | function parseOffset(offset, popperOffsets, referenceOffsets, basePlacem...
function offset (line 3305) | function offset(data, _ref) {
function preventOverflow (line 3346) | function preventOverflow(data, options) {
function shift (line 3417) | function shift(data) {
function hide (line 3450) | function hide(data) {
function inner (line 3488) | function inner(data) {
function Popper (line 3939) | function Popper(reference, popper) {
function Dropdown (line 4169) | function Dropdown(element, config) {
function Modal (line 4674) | function Modal(element, config) {
function allowedAttribute (line 5247) | function allowedAttribute(attr, allowedAttributeList) {
function sanitizeHtml (line 5271) | function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
function Tooltip (line 5408) | function Tooltip(element, config) {
function Popover (line 6086) | function Popover() {
function ScrollSpy (line 6273) | function ScrollSpy(element, config) {
function Tab (line 6568) | function Tab(element) {
function Toast (line 6805) | function Toast(element, config) {
FILE: static/third_party/chart/Chart.js
function tmpl (line 501) | function tmpl(str, data){
FILE: static/third_party/jquery/jquery.js
function DOMEval (line 97) | function DOMEval( code, doc, node ) {
function toType (line 115) | function toType( obj ) {
function isArrayLike (line 483) | function isArrayLike( obj ) {
function Sizzle (line 715) | function Sizzle( selector, context, results, seed ) {
function createCache (line 854) | function createCache() {
function markFunction (line 872) | function markFunction( fn ) {
function assert (line 881) | function assert( fn ) {
function addHandle (line 903) | function addHandle( attrs, handler ) {
function siblingCheck (line 918) | function siblingCheck( a, b ) {
function createInputPseudo (line 944) | function createInputPseudo( type ) {
function createButtonPseudo (line 955) | function createButtonPseudo( type ) {
function createDisabledPseudo (line 966) | function createDisabledPseudo( disabled ) {
function createPositionalPseudo (line 1022) | function createPositionalPseudo( fn ) {
function testContext (line 1045) | function testContext( context ) {
function setFilters (line 2127) | function setFilters() {}
function toSelector (line 2198) | function toSelector( tokens ) {
function addCombinator (line 2208) | function addCombinator( matcher, combinator, base ) {
function elementMatcher (line 2272) | function elementMatcher( matchers ) {
function multipleContexts (line 2286) | function multipleContexts( selector, contexts, results ) {
function condense (line 2295) | function condense( unmatched, map, filter, context, xml ) {
function setMatcher (line 2316) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
function matcherFromTokens (line 2409) | function matcherFromTokens( tokens ) {
function matcherFromGroupMatchers (line 2467) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
function nodeName (line 2803) | function nodeName( elem, name ) {
function winnow (line 2813) | function winnow( elements, qualifier, not ) {
function sibling (line 3108) | function sibling( cur, dir ) {
function createOptions (line 3195) | function createOptions( options ) {
function Identity (line 3420) | function Identity( v ) {
function Thrower (line 3423) | function Thrower( ex ) {
function adoptValue (line 3427) | function adoptValue( value, resolve, reject, noValue ) {
function resolve (line 3520) | function resolve( depth, deferred, handler, special ) {
function completed (line 3885) | function completed() {
function fcamelCase (line 3980) | function fcamelCase( all, letter ) {
function camelCase (line 3987) | function camelCase( string ) {
function Data (line 4004) | function Data() {
function getData (line 4173) | function getData( data ) {
function dataAttr (line 4198) | function dataAttr( elem, key, data ) {
function adjustCSS (line 4511) | function adjustCSS( elem, prop, valueParts, tween ) {
function getDefaultDisplay (line 4578) | function getDefaultDisplay( elem ) {
function showHide (line 4601) | function showHide( elements, show ) {
function getAll (line 4702) | function getAll( context, tag ) {
function setGlobalEval (line 4727) | function setGlobalEval( elems, refElements ) {
function buildFragment (line 4743) | function buildFragment( elems, context, scripts, selection, ignored ) {
function returnTrue (line 4866) | function returnTrue() {
function returnFalse (line 4870) | function returnFalse() {
function safeActiveElement (line 4876) | function safeActiveElement() {
function on (line 4882) | function on( elem, types, selector, data, fn, one ) {
function manipulationTarget (line 5610) | function manipulationTarget( elem, content ) {
function disableScript (line 5621) | function disableScript( elem ) {
function restoreScript (line 5625) | function restoreScript( elem ) {
function cloneCopyEvent (line 5635) | function cloneCopyEvent( src, dest ) {
function fixInput (line 5670) | function fixInput( src, dest ) {
function domManip (line 5683) | function domManip( collection, args, callback, ignored ) {
function remove (line 5773) | function remove( elem, selector, keepData ) {
function computeStyleTests (line 6066) | function computeStyleTests() {
function roundPixelMeasures (line 6108) | function roundPixelMeasures( measure ) {
function curCSS (line 6153) | function curCSS( elem, name, computed ) {
function addGetHookIf (line 6206) | function addGetHookIf( conditionFn, hookFn ) {
function vendorPropName (line 6243) | function vendorPropName( name ) {
function finalPropName (line 6264) | function finalPropName( name ) {
function setPositiveNumber (line 6272) | function setPositiveNumber( elem, value, subtract ) {
function boxModelAdjustment (line 6284) | function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, ...
function getWidthOrHeight (line 6349) | function getWidthOrHeight( elem, dimension, extra ) {
function Tween (line 6682) | function Tween( elem, options, prop, end, easing ) {
function schedule (line 6805) | function schedule() {
function createFxNow (line 6818) | function createFxNow() {
function genFx (line 6826) | function genFx( type, includeWidth ) {
function createTween (line 6846) | function createTween( value, prop, animation ) {
function defaultPrefilter (line 6860) | function defaultPrefilter( elem, props, opts ) {
function propFilter (line 7032) | function propFilter( props, specialEasing ) {
function Animation (line 7069) | function Animation( elem, properties, options ) {
function stripAndCollapse (line 7784) | function stripAndCollapse( value ) {
function getClass (line 7790) | function getClass( elem ) {
function classesToArray (line 7794) | function classesToArray( value ) {
function buildParams (line 8416) | function buildParams( prefix, obj, traditional, add ) {
function addToPrefiltersOrTransports (line 8566) | function addToPrefiltersOrTransports( structure ) {
function inspectPrefiltersOrTransports (line 8600) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
function ajaxExtend (line 8629) | function ajaxExtend( target, src ) {
function ajaxHandleResponses (line 8649) | function ajaxHandleResponses( s, jqXHR, responses ) {
function ajaxConvert (line 8707) | function ajaxConvert( s, response, jqXHR, isSuccess ) {
function done (line 9220) | function done( status, nativeStatusText, responses, headers ) {
FILE: static/third_party/moment/moment.js
function utils_hooks__hooks (line 15) | function utils_hooks__hooks () {
function setHookCallback (line 21) | function setHookCallback (callback) {
function isArray (line 25) | function isArray(input) {
function isDate (line 29) | function isDate(input) {
function map (line 33) | function map(arr, fn) {
function hasOwnProp (line 41) | function hasOwnProp(a, b) {
function extend (line 45) | function extend(a, b) {
function create_utc__createUTC (line 63) | function create_utc__createUTC (input, format, locale, strict) {
function defaultParsingFlags (line 67) | function defaultParsingFlags() {
function getParsingFlags (line 85) | function getParsingFlags(m) {
function valid__isValid (line 110) | function valid__isValid(m) {
function valid__createInvalid (line 136) | function valid__createInvalid (flags) {
function isUndefined (line 148) | function isUndefined(input) {
function copyConfig (line 156) | function copyConfig(to, from) {
function Moment (line 206) | function Moment(config) {
function isMoment (line 218) | function isMoment (obj) {
function absFloor (line 222) | function absFloor (number) {
function toInt (line 230) | function toInt(argumentForCoercion) {
function compareArrays (line 242) | function compareArrays(array1, array2, dontConvert) {
function warn (line 256) | function warn(msg) {
function deprecate (line 263) | function deprecate(msg, fn) {
function deprecateSimple (line 280) | function deprecateSimple(name, msg) {
function isFunction (line 293) | function isFunction(input) {
function isObject (line 297) | function isObject(input) {
function locale_set__set (line 301) | function locale_set__set (config) {
function mergeConfigs (line 317) | function mergeConfigs(parentConfig, childConfig) {
function Locale (line 335) | function Locale(config) {
function normalizeLocale (line 361) | function normalizeLocale(key) {
function chooseLocale (line 368) | function chooseLocale(names) {
function loadLocale (line 392) | function loadLocale(name) {
function locale_locales__getSetGlobalLocale (line 411) | function locale_locales__getSetGlobalLocale (key, values) {
function defineLocale (line 430) | function defineLocale (name, config) {
function updateLocale (line 461) | function updateLocale(name, config) {
function locale_locales__getLocale (line 487) | function locale_locales__getLocale (key) {
function locale_locales__listLocales (line 510) | function locale_locales__listLocales() {
function addUnitAlias (line 516) | function addUnitAlias (unit, shorthand) {
function normalizeUnits (line 521) | function normalizeUnits(units) {
function normalizeObjectUnits (line 525) | function normalizeObjectUnits(inputObject) {
function makeGetSet (line 542) | function makeGetSet (unit, keepTime) {
function get_set__get (line 554) | function get_set__get (mom, unit) {
function get_set__set (line 559) | function get_set__set (mom, unit, value) {
function getSet (line 567) | function getSet (units, value) {
function zeroFill (line 582) | function zeroFill(number, targetLength, forceSign) {
function addFormatToken (line 602) | function addFormatToken (token, padded, ordinal, callback) {
function removeFormattingTokens (line 624) | function removeFormattingTokens(input) {
function makeFormatFunction (line 631) | function makeFormatFunction(format) {
function formatMoment (line 652) | function formatMoment(m, format) {
function expandFormat (line 663) | function expandFormat(format, locale) {
function addRegexToken (line 707) | function addRegexToken (token, regex, strictRegex) {
function getParseRegexForToken (line 713) | function getParseRegexForToken (token, config) {
function unescapeFormat (line 722) | function unescapeFormat(s) {
function regexEscape (line 728) | function regexEscape(s) {
function addParseToken (line 734) | function addParseToken (token, callback) {
function addWeekParseToken (line 749) | function addWeekParseToken (token, callback) {
function addTimeToArrayFromToken (line 756) | function addTimeToArrayFromToken(token, input, config) {
function daysInMonth (line 789) | function daysInMonth(year, month) {
function localeMonths (line 840) | function localeMonths (m, format) {
function localeMonthsShort (line 846) | function localeMonthsShort (m, format) {
function units_month__handleStrictParse (line 851) | function units_month__handleStrictParse(monthName, format, strict) {
function localeMonthsParse (line 892) | function localeMonthsParse (monthName, format, strict) {
function setMonth (line 932) | function setMonth (mom, value) {
function getSetMonth (line 957) | function getSetMonth (value) {
function getDaysInMonth (line 967) | function getDaysInMonth () {
function monthsShortRegex (line 972) | function monthsShortRegex (isStrict) {
function monthsRegex (line 989) | function monthsRegex (isStrict) {
function computeMonthsParse (line 1005) | function computeMonthsParse () {
function checkOverflow (line 1037) | function checkOverflow (m) {
function configFromISO (line 1105) | function configFromISO(config) {
function configFromString (line 1158) | function configFromString(config) {
function createDate (line 1183) | function createDate (y, m, d, h, M, s, ms) {
function createUTCDate (line 1195) | function createUTCDate (y) {
function daysInYear (line 1245) | function daysInYear(year) {
function isLeapYear (line 1249) | function isLeapYear(year) {
function getIsLeapYear (line 1263) | function getIsLeapYear () {
function firstWeekOffset (line 1268) | function firstWeekOffset(year, dow, doy) {
function dayOfYearFromWeeks (line 1278) | function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
function weekOfYear (line 1301) | function weekOfYear(mom, dow, doy) {
function weeksInYear (line 1323) | function weeksInYear(year, dow, doy) {
function defaults (line 1330) | function defaults(a, b, c) {
function currentDateArray (line 1340) | function currentDateArray(config) {
function configFromArray (line 1353) | function configFromArray (config) {
function dayOfYearFromWeekInfo (line 1415) | function dayOfYearFromWeekInfo(config) {
function configFromStringAndFormat (line 1472) | function configFromStringAndFormat(config) {
function meridiemFixWrap (line 1541) | function meridiemFixWrap (locale, hour, meridiem) {
function configFromStringAndArray (line 1567) | function configFromStringAndArray(config) {
function configFromObject (line 1611) | function configFromObject(config) {
function createFromConfig (line 1624) | function createFromConfig (config) {
function prepareConfig (line 1635) | function prepareConfig (config) {
function configFromInput (line 1668) | function configFromInput(config) {
function createLocalOrUTC (line 1691) | function createLocalOrUTC (input, format, locale, strict, isUTC) {
function local__createLocal (line 1710) | function local__createLocal (input, format, locale, strict) {
function pickBy (line 1743) | function pickBy(fn, moments) {
function min (line 1761) | function min () {
function max (line 1767) | function max () {
function Duration (line 1777) | function Duration (duration) {
function isDuration (line 1812) | function isDuration (obj) {
function offset (line 1818) | function offset (token, separator) {
function offsetFromString (line 1849) | function offsetFromString(matcher, string) {
function cloneWithOffset (line 1859) | function cloneWithOffset(input, model) {
function getDateOffset (line 1873) | function getDateOffset (m) {
function getSetOffset (line 1897) | function getSetOffset (input, keepLocalTime) {
function getSetZone (line 1932) | function getSetZone (input, keepLocalTime) {
function setOffsetToUTC (line 1946) | function setOffsetToUTC (keepLocalTime) {
function setOffsetToLocal (line 1950) | function setOffsetToLocal (keepLocalTime) {
function setOffsetToParsedOffset (line 1962) | function setOffsetToParsedOffset () {
function hasAlignedHourOffset (line 1971) | function hasAlignedHourOffset (input) {
function isDaylightSavingTime (line 1980) | function isDaylightSavingTime () {
function isDaylightSavingTimeShifted (line 1987) | function isDaylightSavingTimeShifted () {
function isLocal (line 2008) | function isLocal () {
function isUtcOffset (line 2012) | function isUtcOffset () {
function isUtc (line 2016) | function isUtc () {
function create__createDuration (line 2028) | function create__createDuration (input, key) {
function parseIso (line 2091) | function parseIso (inp, sign) {
function positiveMomentsDifference (line 2100) | function positiveMomentsDifference(base, other) {
function momentsDifference (line 2114) | function momentsDifference(base, other) {
function absRound (line 2132) | function absRound (number) {
function createAdder (line 2141) | function createAdder(direction, name) {
function add_subtract__addSubtract (line 2157) | function add_subtract__addSubtract (mom, duration, isAdding, updateOffse...
function moment_calendar__calendar (line 2186) | function moment_calendar__calendar (time, formats) {
function clone (line 2204) | function clone () {
function isAfter (line 2208) | function isAfter (input, units) {
function isBefore (line 2221) | function isBefore (input, units) {
function isBetween (line 2234) | function isBetween (from, to, units, inclusivity) {
function isSame (line 2240) | function isSame (input, units) {
function isSameOrAfter (line 2255) | function isSameOrAfter (input, units) {
function isSameOrBefore (line 2259) | function isSameOrBefore (input, units) {
function diff (line 2263) | function diff (input, units, asFloat) {
function monthDiff (line 2301) | function monthDiff (a, b) {
function toString (line 2325) | function toString () {
function moment_format__toISOString (line 2329) | function moment_format__toISOString () {
function format (line 2343) | function format (inputString) {
function from (line 2351) | function from (time, withoutSuffix) {
function fromNow (line 2361) | function fromNow (withoutSuffix) {
function to (line 2365) | function to (time, withoutSuffix) {
function toNow (line 2375) | function toNow (withoutSuffix) {
function locale (line 2382) | function locale (key) {
function localeData (line 2407) | function localeData () {
function startOf (line 2411) | function startOf (units) {
function endOf (line 2455) | function endOf (units) {
function to_type__valueOf (line 2469) | function to_type__valueOf () {
function unix (line 2473) | function unix () {
function toDate (line 2477) | function toDate () {
function toArray (line 2481) | function toArray () {
function toObject (line 2486) | function toObject () {
function toJSON (line 2499) | function toJSON () {
function moment_valid__isValid (line 2504) | function moment_valid__isValid () {
function parsingFlags (line 2508) | function parsingFlags () {
function invalidAt (line 2512) | function invalidAt () {
function creationData (line 2516) | function creationData() {
function addWeekYearFormatToken (line 2536) | function addWeekYearFormatToken (token, getter) {
function getSetWeekYear (line 2571) | function getSetWeekYear (input) {
function getSetISOWeekYear (line 2580) | function getSetISOWeekYear (input) {
function getISOWeeksInYear (line 2585) | function getISOWeeksInYear () {
function getWeeksInYear (line 2589) | function getWeeksInYear () {
function getSetWeekYearHelper (line 2594) | function getSetWeekYearHelper(input, week, weekday, dow, doy) {
function setWeekAll (line 2607) | function setWeekAll(weekYear, week, weekday, dow, doy) {
function getSetQuarter (line 2634) | function getSetQuarter (input) {
function localeWeek (line 2663) | function localeWeek (mom) {
function localeFirstDayOfWeek (line 2672) | function localeFirstDayOfWeek () {
function localeFirstDayOfYear (line 2676) | function localeFirstDayOfYear () {
function getSetWeek (line 2682) | function getSetWeek (input) {
function getSetISOWeek (line 2687) | function getSetISOWeek (input) {
function parseWeekday (line 2773) | function parseWeekday(input, locale) {
function localeWeekdays (line 2793) | function localeWeekdays (m, format) {
function localeWeekdaysShort (line 2799) | function localeWeekdaysShort (m) {
function localeWeekdaysMin (line 2804) | function localeWeekdaysMin (m) {
function day_of_week__handleStrictParse (line 2808) | function day_of_week__handleStrictParse(weekdayName, format, strict) {
function localeWeekdaysParse (line 2872) | function localeWeekdaysParse (weekdayName, format, strict) {
function getSetDayOfWeek (line 2914) | function getSetDayOfWeek (input) {
function getSetLocaleDayOfWeek (line 2927) | function getSetLocaleDayOfWeek (input) {
function getSetISODayOfWeek (line 2935) | function getSetISODayOfWeek (input) {
function weekdaysRegex (line 2946) | function weekdaysRegex (isStrict) {
function weekdaysShortRegex (line 2963) | function weekdaysShortRegex (isStrict) {
function weekdaysMinRegex (line 2980) | function weekdaysMinRegex (isStrict) {
function computeWeekdaysParse (line 2997) | function computeWeekdaysParse () {
function getSetDayOfYear (line 3058) | function getSetDayOfYear (input) {
function hFormat (line 3065) | function hFormat() {
function kFormat (line 3069) | function kFormat() {
function meridiem (line 3095) | function meridiem (token, lowercase) {
function matchMeridiem (line 3110) | function matchMeridiem (isStrict, locale) {
function localeIsPM (line 3164) | function localeIsPM (input) {
function localeMeridiem (line 3171) | function localeMeridiem (hours, minutes, isLower) {
function parseMs (line 3270) | function parseMs(input, array) {
function getZoneAbbr (line 3288) | function getZoneAbbr () {
function getZoneName (line 3292) | function getZoneName () {
function moment__createUnix (line 3401) | function moment__createUnix (input) {
function moment__createInZone (line 3405) | function moment__createInZone () {
function locale_calendar__calendar (line 3418) | function locale_calendar__calendar (key, mom, now) {
function longDateFormat (line 3432) | function longDateFormat (key) {
function invalidDate (line 3449) | function invalidDate () {
function ordinal (line 3456) | function ordinal (number) {
function preParsePostFormat (line 3460) | function preParsePostFormat (string) {
function relative__relativeTime (line 3480) | function relative__relativeTime (number, withoutSuffix, string, isFuture) {
function pastFuture (line 3487) | function pastFuture (diff, output) {
function lists__get (line 3548) | function lists__get (format, index, field, setter) {
function listMonthsImpl (line 3554) | function listMonthsImpl (format, index, field) {
function listWeekdaysImpl (line 3582) | function listWeekdaysImpl (localeSorted, format, index, field) {
function lists__listMonths (line 3618) | function lists__listMonths (format, index) {
function lists__listMonthsShort (line 3622) | function lists__listMonthsShort (format, index) {
function lists__listWeekdays (line 3626) | function lists__listWeekdays (localeSorted, format, index) {
function lists__listWeekdaysShort (line 3630) | function lists__listWeekdaysShort (localeSorted, format, index) {
function lists__listWeekdaysMin (line 3634) | function lists__listWeekdaysMin (localeSorted, format, index) {
function duration_abs__abs (line 3656) | function duration_abs__abs () {
function duration_add_subtract__addSubtract (line 3673) | function duration_add_subtract__addSubtract (duration, input, value, dir...
function duration_add_subtract__add (line 3684) | function duration_add_subtract__add (input, value) {
function duration_add_subtract__subtract (line 3689) | function duration_add_subtract__subtract (input, value) {
function absCeil (line 3693) | function absCeil (number) {
function bubble (line 3701) | function bubble () {
function daysToMonths (line 3748) | function daysToMonths (days) {
function monthsToDays (line 3754) | function monthsToDays (months) {
function as (line 3759) | function as (units) {
function duration_as__valueOf (line 3787) | function duration_as__valueOf () {
function makeAs (line 3796) | function makeAs (alias) {
function duration_get__get (line 3811) | function duration_get__get (units) {
function makeGetter (line 3816) | function makeGetter(name) {
function weeks (line 3830) | function weeks () {
function substituteTimeAgo (line 3844) | function substituteTimeAgo(string, number, withoutSuffix, isFuture, loca...
function duration_humanize__relativeTime (line 3848) | function duration_humanize__relativeTime (posNegDuration, withoutSuffix,...
function duration_humanize__getSetRelativeTimeThreshold (line 3875) | function duration_humanize__getSetRelativeTimeThreshold (threshold, limi...
function humanize (line 3886) | function humanize (withSuffix) {
function iso_string__toISOString (line 3899) | function iso_string__toISOString() {
FILE: static/third_party/pagedown/Markdown.Converter.js
function identity (line 56) | function identity(x) { return x; }
function returnFalse (line 57) | function returnFalse(x) { return false; }
function HookCollection (line 59) | function HookCollection() { }
function SaveHash (line 99) | function SaveHash() { }
function _StripLinkDefinitions (line 308) | function _StripLinkDefinitions(text) {
function _HashHTMLBlocks (line 359) | function _HashHTMLBlocks(text) {
function hashBlock (line 480) | function hashBlock(text) {
function hashMatch (line 486) | function hashMatch(wholeMatch, m1) {
function _RunBlockGamut (line 492) | function _RunBlockGamut(text, doNotUnhash) {
function _RunSpanGamut (line 524) | function _RunSpanGamut(text) {
function _EscapeSpecialCharsWithinTagAttributes (line 559) | function _EscapeSpecialCharsWithinTagAttributes(text) {
function _DoAnchors (line 581) | function _DoAnchors(text) {
function writeAnchorTag (line 679) | function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
function _DoImages (line 724) | function _DoImages(text) {
function attributeEncode (line 787) | function attributeEncode(text) {
function writeImageTag (line 793) | function writeImageTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
function _DoHeaders (line 838) | function _DoHeaders(text) {
function _DoLists (line 884) | function _DoLists(text, isInsideParagraphlessListItem) {
function _ProcessListItems (line 969) | function _ProcessListItems(list_str, list_type, isInsideParagraphlessLis...
function _DoCodeBlocks (line 1062) | function _DoCodeBlocks(text) {
function _DoCodeSpans (line 1105) | function _DoCodeSpans(text) {
function _EncodeCode (line 1159) | function _EncodeCode(text) {
function _DoItalicsAndBoldStrict (line 1189) | function _DoItalicsAndBoldStrict(text) {
function _DoItalicsAndBold_AllowIntrawordWithAsterisk (line 1228) | function _DoItalicsAndBold_AllowIntrawordWithAsterisk(text) {
function _DoBlockQuotes (line 1325) | function _DoBlockQuotes(text) {
function _FormParagraphs (line 1373) | function _FormParagraphs(text, doNotUnhash) {
function _EncodeAmpsAndAngles (line 1426) | function _EncodeAmpsAndAngles(text) {
function _EncodeBackslashEscapes (line 1439) | function _EncodeBackslashEscapes(text) {
function handleTrailingParens (line 1465) | function handleTrailingParens(wholeMatch, lookbehind, protocol, link) {
function _DoAutoLinks (line 1501) | function _DoAutoLinks(text) {
function _UnescapeSpecialChars (line 1546) | function _UnescapeSpecialChars(text) {
function _Outdent (line 1559) | function _Outdent(text) {
function _Detab (line 1575) | function _Detab(text) {
function attributeSafeUrl (line 1598) | function attributeSafeUrl(url) {
function escapeCharacters (line 1604) | function escapeCharacters(text, charsToEscape, afterBackslash) {
function escapeCharacters_callback (line 1620) | function escapeCharacters_callback(wholeMatch, m1) {
FILE: static/third_party/pagedown/Markdown.Editor.js
function Chunks (line 154) | function Chunks() { }
function PanelCollection (line 300) | function PanelCollection(postfix) {
function UndoManager (line 451) | function UndoManager(callback, panels) {
function TextareaState (line 685) | function TextareaState(panels, isInitialState) {
function PreviewManager (line 823) | function PreviewManager(converter, panels, previewRefreshCallback) {
function UIManager (line 1217) | function UIManager(postfix, panels, undoManager, previewManager, command...
function CommandManager (line 1529) | function CommandManager(pluginHooks, getString) {
function properlyEncoded (line 1689) | function properlyEncoded(linkdef) {
FILE: static/third_party/pagedown/Markdown.Sanitizer.js
function sanitizeHtml (line 18) | function sanitizeHtml(html) {
function sanitizeTag (line 30) | function sanitizeTag(tag) {
function balanceTags (line 45) | function balanceTags(html) {
FILE: tests.py
function main (line 19) | def main(argv):
Condensed preview — 135 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,243K chars).
[
{
"path": ".codecov.yml",
"chars": 414,
"preview": "codecov:\n notify:\n require_ci_to_pass: true\ncomment:\n behavior: default\n layout: header, diff\n require_changes: f"
},
{
"path": ".coveragerc",
"chars": 89,
"preview": "[run]\nsource = scoreboard\nomit =\n scoreboard/tests/*\n main.py\n setup.py\nbranch = True\n"
},
{
"path": ".editorconfig",
"chars": 274,
"preview": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\nend_of_line = lf\ninde"
},
{
"path": ".gcloudignore",
"chars": 195,
"preview": ".gcloudignore\n.git\nscoreboard/tests\n*.md\n*.pyc\nhtmlcov/\ndoc/\n.hooks/\nLICENSE\nAUTHORS\nMakefile\nDockerfile\nconfig.example."
},
{
"path": ".gitignore",
"chars": 228,
"preview": "# Compiled, swap files\n*.pyc\n*.swp\n.virtualenv\n__pycache__\n\n# Runtime data\n/config.py\n*.bak\n*.db\n/attachments\n/attachmen"
},
{
"path": ".hooks/pre-commit.sh",
"chars": 584,
"preview": "#!/bin/bash\n\nif [ \"${SKIP_TESTS}\" != \"\" ] ; then\n exit 0\nfi\n\n# stash code not to be committed\ngit stash -q --keep-ind"
},
{
"path": ".travis.yml",
"chars": 264,
"preview": "language: python\nsudo: false\npython:\n - \"2.7\"\n - \"3.6\"\n - \"3.7\"\n\ninstall:\n - pip install -r requirements.txt\n - pip"
},
{
"path": "AUTHORS",
"chars": 209,
"preview": "Current maintainer: David Tomaschik <dwt@google.com>\n\nCore Team:\n\nAndrew Griffiths <agriffiths@google.com>\nDavid Tomasch"
},
{
"path": "CONTRIBUTING.md",
"chars": 1450,
"preview": "Want to contribute? Great! First, read this page (including the small print at the end).\n\n### Before you contribute\nBefo"
},
{
"path": "Dockerfile",
"chars": 753,
"preview": "FROM debian:buster\n\nRUN apt-get update && apt-get install -y \\\n nginx \\\n python3 \\\n python3-dev \\\n python3-p"
},
{
"path": "LICENSE",
"chars": 11358,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "Makefile",
"chars": 867,
"preview": "# Makefile to minimize JS using UglifyJS (https://github.com/mishoo/UglifyJS2)\n# or 'cat' to just assemble into one file"
},
{
"path": "README.md",
"chars": 4712,
"preview": "## CTF Scoreboard ##\n\nThis is a basic CTF Scoreboard, with support for teams or individual\ncompetitors, and a handful of"
},
{
"path": "app.yaml",
"chars": 508,
"preview": "runtime: python37\ninstance_class: F4\nautomatic_scaling:\n max_instances: 50\n max_idle_instances: 10\n\nhandlers:\n- url: /"
},
{
"path": "config.example.py",
"chars": 1104,
"preview": "# Copyright 2016 Google Inc. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "doc/developing/README.md",
"chars": 2050,
"preview": "# Development Setup\n\nYou'll want to have `python3` and `pip` installed. I also recommend\n`virtualenv` and `virtualenvwr"
},
{
"path": "doc/developing/requirements.txt",
"chars": 77,
"preview": "# Additional requirements for development\ncoverage\nflake8\nflask-testing\nmock\n"
},
{
"path": "doc/docker/supervisord.conf",
"chars": 363,
"preview": "[supervisord]\nnodaemon=true\n\n[program:uwsgi]\ncommand=/usr/bin/uwsgi --ini /opt/scoreboard/doc/docker/uwsgi.ini\nstdout_lo"
},
{
"path": "doc/docker/uwsgi.ini",
"chars": 249,
"preview": "# Sample uWSGI config file\n[uwsgi]\nchdir = /opt/scoreboard\nsocket = 127.0.0.1:9000\nprocesses = 4\nthreads = 2\nmaster = tr"
},
{
"path": "doc/nginx.conf",
"chars": 246,
"preview": "server {\n listen 80 default_server;\n root /opt/scoreboard/static; # Make sure code is not in document root!\n\n locati"
},
{
"path": "doc/uwsgi.ini",
"chars": 277,
"preview": "# Sample uWSGI config file\n[uwsgi]\nchdir = /opt/scoreboard\nsocket = 127.0.0.1:9000\nprocesses = 4\nthreads = 2\nmaster = tr"
},
{
"path": "main.py",
"chars": 1396,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "requirements.txt",
"chars": 242,
"preview": "Flask\nFlask-RESTful\nFlask-SQLAlchemy\nFlask-Scss\nJinja2\nMarkupSafe\nPyMySQL\nSQLAlchemy<1.4.0\nWerkzeug<1.0.0\naniso8601\nargp"
},
{
"path": "scoreboard/__init__.py",
"chars": 595,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/attachments/__init__.py",
"chars": 1803,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/attachments/file.py",
"chars": 2402,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/attachments/gcs.py",
"chars": 2481,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/attachments/testing.py",
"chars": 1436,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/auth/__init__.py",
"chars": 1187,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/auth/local.py",
"chars": 1414,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/cache.py",
"chars": 4693,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/config_defaults.py",
"chars": 1499,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/context.py",
"chars": 4254,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/controllers.py",
"chars": 7233,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/csrfutil.py",
"chars": 3356,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/errors.py",
"chars": 1577,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/logger.py",
"chars": 1168,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/mail.py",
"chars": 4180,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/main.py",
"chars": 4860,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/models.py",
"chars": 25940,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/rest.py",
"chars": 35177,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/__init__.py",
"chars": 595,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/base.py",
"chars": 8885,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/cache_test.py",
"chars": 6129,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/controllers_test.py",
"chars": 2145,
"preview": "# Copyright 2019 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/csrfutil_test.py",
"chars": 6764,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/data.py",
"chars": 3800,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/models_test.py",
"chars": 7023,
"preview": "# Copyright 2019 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/rest_test.py",
"chars": 41372,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/utils_test.py",
"chars": 2294,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/tests/validators_test.py",
"chars": 4838,
"preview": "# Copyright 2017 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/utils.py",
"chars": 8546,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/__init__.py",
"chars": 1771,
"preview": "# Copyright 2017 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/base.py",
"chars": 1162,
"preview": "# Copyright 2017 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/nonce.py",
"chars": 4636,
"preview": "# Copyright 2019 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/per_team.py",
"chars": 1376,
"preview": "# Copyright 2017 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/regex.py",
"chars": 1318,
"preview": "# Copyright 2018 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/validators/static_pbkdf2.py",
"chars": 1683,
"preview": "# Copyright 2017 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/views.py",
"chars": 2965,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "scoreboard/wsgi.py",
"chars": 873,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
},
{
"path": "static/css/.keep",
"chars": 0,
"preview": ""
},
{
"path": "static/js/Chart.Step.js",
"chars": 29879,
"preview": "/**\r\n * Copyright 2016 Google LLC. All Rights Reserved.\r\n * \r\n * Licensed under the Apache License, Version 2.0 (the \"Li"
},
{
"path": "static/js/app.js",
"chars": 4269,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/admin/challenges.js",
"chars": 19455,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/controllers/admin/news.js",
"chars": 1755,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/admin/page.js",
"chars": 2166,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/admin/teams.js",
"chars": 5448,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/controllers/admin/tools.js",
"chars": 1745,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/controllers/challenges.js",
"chars": 3586,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/controllers/global.js",
"chars": 2243,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/page.js",
"chars": 1365,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/registration.js",
"chars": 7621,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/controllers/scoreboard.js",
"chars": 2061,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/controllers/teams.js",
"chars": 1911,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/directives.js",
"chars": 16917,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/filters.js",
"chars": 1667,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/services/admin.js",
"chars": 1191,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/services/challenges.js",
"chars": 5423,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/services/global.js",
"chars": 9154,
"preview": "/**\n * Copyright 2018 Google LLC. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "static/js/services/page.js",
"chars": 1981,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/services/session.js",
"chars": 3564,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/services/teams.js",
"chars": 1050,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/services/upload.js",
"chars": 2280,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/js/services/users.js",
"chars": 1304,
"preview": "/**\n * Copyright 2016 Google LLC. All Rights Reserved.\n * \n * Licensed under the Apache License, Version 2.0 (the \"Licen"
},
{
"path": "static/partials/admin/attachments.html",
"chars": 1949,
"preview": "<h2>Attachments</h2>\n<div class='well' ng-repeat='a in attachments'>\n <form ng-submit='updateAttachment(a)' name='admin"
},
{
"path": "static/partials/admin/challenge.html",
"chars": 5195,
"preview": "<h2><span ng-bind='action'></span> Challenge</h2>\n<form ng-submit='saveChallenge()' name='challengeForm'>\n <div class='"
},
{
"path": "static/partials/admin/challenges.html",
"chars": 1433,
"preview": "<table class='table table-striped challenge-table text-light'>\n <thead>\n <tr><th class='col-md-4'>Name</th><th class"
},
{
"path": "static/partials/admin/news.html",
"chars": 1103,
"preview": "<h2>Submit News</h2>\n<form ng-submit='submitNews()' name='newsForm'>\n <div class='form-group'>\n <label for='ne"
},
{
"path": "static/partials/admin/page.html",
"chars": 588,
"preview": "<h2 ng-bind='action'></h2>\n<form ng-submit='save()' name='pageForm'>\n <div class='form-group'>\n <label for='pa"
},
{
"path": "static/partials/admin/pages.html",
"chars": 2590,
"preview": "<h2>Pages</h2>\n<div class='well'>\n <table class='table text-light' id='tag-table'>\n <thead>\n <tr>\n <th"
},
{
"path": "static/partials/admin/restore.html",
"chars": 1153,
"preview": "<h2>Backup/Restore Challenges</h2>\n<fieldset><legend>Backup</legend>\n <a href='/api/backup' target='_self' class='btn b"
},
{
"path": "static/partials/admin/tags.html",
"chars": 1395,
"preview": "<h2>Tags</h2>\n<div class='well' ng-repeat='t in tags'>\n <form ng-submit='updateTag(t)' name='adminTagForm[$index]'>\n "
},
{
"path": "static/partials/admin/teams.html",
"chars": 4534,
"preview": "<!-- All Teams -->\n<table class='table table-striped text-light' ng-hide='!!team'>\n <thead>\n <tr>\n <th class='c"
},
{
"path": "static/partials/admin/tools.html",
"chars": 453,
"preview": "<h2>Admin Tools</h2>\n<p><input class='btn btn-info' type='submit' ng-click='recalculateScores()'\n value='Recalculate "
},
{
"path": "static/partials/admin/users.html",
"chars": 1776,
"preview": "<table class='table table-striped text-light' ng-hide='!!user'>\n <thead>\n <tr><th>Nick</th><th>E-Mail</th>\n <th"
},
{
"path": "static/partials/challenge_grid.html",
"chars": 1769,
"preview": "<h3>Challenges</h3>\n<div class='tag-selector' ng-show='!!allTags'>\n <div class=\"info-box\">\n <span class='info-text'>"
},
{
"path": "static/partials/components/challenge.html",
"chars": 2535,
"preview": "<div class='chall-panel challenge card'>\n <div class='card-header position-relative' id='{{chall.cid}}'>\n <h3 class="
},
{
"path": "static/partials/components/countdown.html",
"chars": 451,
"preview": "<span class='ng-hide countdown' ng-show='display'>\n <span class='msg' ng-show='!!message' ng-bind='message'></span>\n "
},
{
"path": "static/partials/login.html",
"chars": 933,
"preview": "<div class='col-md-6 col-md-offset-3'>\n <form ng-submit='login()' name='loginForm'>\n <div class='form-group'>\n "
},
{
"path": "static/partials/page.html",
"chars": 388,
"preview": "<h2 ng-show='page.title' ng-bind='page.title'></h2>\n<div class='page markdown' id='page-contents' ng-bind-html='page.con"
},
{
"path": "static/partials/profile.html",
"chars": 3593,
"preview": "<h2>{{user.nick}}</h2>\n<div class='row user-field'>\n <div class='col-md-5 col-md-offset-2'>\n <form ng-submit='update"
},
{
"path": "static/partials/pwreset.html",
"chars": 1066,
"preview": "<div class='col-md-6 col-md-offset-3'>\n <form ng-submit='pwreset()' name='pwresetForm'>\n <div class='form-group'>\n "
},
{
"path": "static/partials/register.html",
"chars": 3334,
"preview": "<div class='col-md-6 col-md-offset-3'>\n <form ng-submit='register()' name='registerForm'>\n <div class='form-group'\n "
},
{
"path": "static/partials/scoreboard.html",
"chars": 762,
"preview": "<h2>Scoreboard</h2>\n<div score-chart chart-data='scoreHistory' with-legend\n start-date='{{config.game_start}}' end-date"
},
{
"path": "static/partials/team.html",
"chars": 840,
"preview": "<h2>{{team.name}}</h2>\n<div class='row'>\n <div class='tag-chart col-md-6' donut-chart chart-data='tagData'></div>\n <di"
},
{
"path": "static/scss/scoreboard-colors.scss",
"chars": 1948,
"preview": "\n$body-bkg: #2f343a;\n$body-text: #ffffff;\n$nav-bkg: #27326d;\n$sidebar-bkg: #cbcbcb;\n$link: #8899b7;\n$news-bkg: #4e5d6c;\n"
},
{
"path": "static/scss/scoreboard-mobile.scss",
"chars": 459,
"preview": "@media screen and (max-width: 767px) {\n .mobile {\n display: initial;\n }\n\n #news-box {\n display: none;\n }\n\n bo"
},
{
"path": "static/scss/scoreboard.scss",
"chars": 10842,
"preview": "/** Colors */\n$solved-banner-bgcolor: #ff0000;\n\nhtml {\n overflow-y: scroll;\n\n &,body {\n height: 100%;\n }\n\n"
},
{
"path": "static/third_party/angular/LICENSE",
"chars": 1100,
"preview": "The MIT License\n\nCopyright (c) 2010-2014 Google, Inc. http://angularjs.org\n\nPermission is hereby granted, free of charge"
},
{
"path": "static/third_party/angular/angular-csp.css",
"chars": 343,
"preview": "/* Include this file in your html if you are using the CSP mode. */\n\n@charset \"UTF-8\";\n\n[ng\\:cloak],\n[ng-cloak],\n[data-n"
},
{
"path": "static/third_party/angular/angular-resource.js",
"chars": 38056,
"preview": "/**\n * @license AngularJS v1.7.7\n * (c) 2010-2018 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
},
{
"path": "static/third_party/angular/angular-route.js",
"chars": 46917,
"preview": "/**\n * @license AngularJS v1.7.7\n * (c) 2010-2018 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
},
{
"path": "static/third_party/angular/angular-sanitize.js",
"chars": 34264,
"preview": "/**\n * @license AngularJS v1.7.7\n * (c) 2010-2018 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
},
{
"path": "static/third_party/angular/angular.js",
"chars": 1371531,
"preview": "/**\n * @license AngularJS v1.7.7\n * (c) 2010-2018 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
},
{
"path": "static/third_party/bootstrap/LICENSE",
"chars": 1084,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2011-2014 Twitter, Inc\n\nPermission is hereby granted, free of charge, to any person"
},
{
"path": "static/third_party/bootstrap/bootstrap.css",
"chars": 192348,
"preview": "/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors\n * Copyright 2011-2019 "
},
{
"path": "static/third_party/bootstrap/bootstrap.js",
"chars": 222909,
"preview": "/*!\n * Bootstrap v4.3.1 (https://getbootstrap.com/)\n * Copyright 2011-2019 The Bootstrap Authors (https://github.com/t"
},
{
"path": "static/third_party/bootstrap-theme/bootstrap-theme.css",
"chars": 14936,
"preview": "/*!\n * Bootstrap v3.1.1 (http://getbootstrap.com)\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://gi"
},
{
"path": "static/third_party/chart/Chart.Scatter.js",
"chars": 31980,
"preview": "(function (root, factory) {\r\n\tif (typeof define === 'function' && define.amd) {\r\n\t\t// AMD. Register as an anonymous modu"
},
{
"path": "static/third_party/chart/Chart.js",
"chars": 118110,
"preview": "/*!\n * Chart.js\n * http://chartjs.org/\n * Version: 1.0.2\n *\n * Copyright 2015 Nick Downie\n * Released under the MIT lice"
},
{
"path": "static/third_party/chart/LICENSE.md",
"chars": 1060,
"preview": "Copyright (c) 2013-2016 Nick Downie\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of thi"
},
{
"path": "static/third_party/chart/Scatter.LICENSE.md",
"chars": 1075,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 dima117\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "static/third_party/jquery/LICENSE.txt",
"chars": 1617,
"preview": "Copyright 2005, 2014 jQuery Foundation and other contributors,\nhttps://jquery.org/\n\nThis software consists of voluntary "
},
{
"path": "static/third_party/jquery/jquery.js",
"chars": 271751,
"preview": "/*!\n * jQuery JavaScript Library v3.3.1\n * https://jquery.com/\n *\n * Includes Sizzle.js\n * https://sizzlejs.com/\n *\n * C"
},
{
"path": "static/third_party/moment/LICENSE",
"chars": 1097,
"preview": "Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors\n\nPermission is hereby granted, free of charge, "
},
{
"path": "static/third_party/moment/moment.js",
"chars": 134906,
"preview": "//! moment.js\n//! version : 2.13.0\n//! authors : Tim Wood, Iskren Chernev, Moment.js contributors\n//! license : MIT\n//! "
},
{
"path": "static/third_party/pagedown/LICENSE.txt",
"chars": 1484,
"preview": "A javascript port of Markdown, as used on Stack Overflow\r\nand the rest of Stack Exchange network.\r\n\r\nLargely based on sh"
},
{
"path": "static/third_party/pagedown/Markdown.Converter.js",
"chars": 75566,
"preview": "\"use strict\";\r\nvar Markdown;\r\n\r\nif (typeof exports === \"object\" && typeof require === \"function\") // we're in a CommonJS"
},
{
"path": "static/third_party/pagedown/Markdown.Editor.js",
"chars": 84367,
"preview": "// needs Markdown.Converter.js at the moment\r\n\r\n(function () {\r\n\r\n var util = {},\r\n position = {},\r\n u"
},
{
"path": "static/third_party/pagedown/Markdown.Sanitizer.js",
"chars": 3907,
"preview": "(function () {\n var output, Converter;\n if (typeof exports === \"object\" && typeof require === \"function\") { // we'"
},
{
"path": "templates/base.html",
"chars": 7672,
"preview": "{# NOTE: This is rendered both by Jinja2 then Angular. Most of the template\nshould be in {% raw %} blocks. DFIU! -#}\n{"
},
{
"path": "templates/error.html",
"chars": 479,
"preview": "{% extends \"base.html\" %}\n{% block body %}\n{% if exc %}\n {% if exc.code == 404 %}\n <div class='alert alert-danger'>{{r"
},
{
"path": "templates/index.html",
"chars": 26,
"preview": "{% extends \"base.html\" %}\n"
},
{
"path": "templates/pwreset.eml",
"chars": 269,
"preview": "Dear {{user.nick}},\n\nSomeone at {{ip}} requested a password reset for {{user.email}} at\n{{config.TITLE}}. If you didn't"
},
{
"path": "tests.py",
"chars": 865,
"preview": "# Copyright 2016 Google LLC. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# "
}
]
About this extraction
This page contains the full source code of the google/ctfscoreboard GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 135 files (2.9 MB), approximately 776.2k tokens, and a symbol index with 1321 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.