├── 20231222-Containers.odp ├── README.md ├── compose-1 ├── 99-pipewire-default.conf ├── Dockerfile ├── docker-compose.yml └── pulse-client.conf ├── hackergame.patch ├── hackergame ├── .dockerignore ├── .env.example ├── .github │ ├── dependabot.yml │ └── workflows │ │ ├── build.yml │ │ └── smoketest.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── challenges │ └── example │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── docker-compose.yml │ │ └── example.py ├── conf │ ├── local_settings.py.example │ ├── logrotate │ │ └── uwsgi │ ├── nginx-sites │ │ ├── hackergame │ │ ├── hackergame-docker │ │ └── hgtest │ ├── pgbouncer.ini │ ├── settings │ │ ├── base.py │ │ ├── dev.py │ │ ├── docker.py │ │ ├── hackergame.py │ │ └── hgtest.py │ ├── systemd │ │ └── hackergame@.service │ ├── uwsgi-apps │ │ ├── hackergame-docker.ini │ │ ├── hackergame.ini │ │ └── hgtest.ini │ └── uwsgi.ini ├── docker-compose.yml ├── docker │ └── start.sh ├── frontend │ ├── README.md │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── apps.py │ ├── auth_providers │ │ ├── README.md │ │ ├── base.py │ │ ├── cas.py │ │ ├── debug.py │ │ ├── external.py │ │ ├── fdu.py │ │ ├── gdou.py │ │ ├── gdut.py │ │ ├── gzhu.py │ │ ├── hit.py │ │ ├── jlu.py │ │ ├── neu.py │ │ ├── nuaa.py │ │ ├── nudt.py │ │ ├── nyist.py │ │ ├── ouc.py │ │ ├── shu.py │ │ ├── sms.py │ │ ├── sustech.py │ │ ├── sysu.py │ │ ├── tongji.py │ │ ├── ustc.py │ │ ├── xidian.py │ │ ├── xmut.py │ │ └── zju.py │ ├── context_processors.py │ ├── management │ │ └── commands │ │ │ ├── fake_data.py │ │ │ ├── import_data.py │ │ │ ├── regen_all.py │ │ │ └── setup.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20191010_0025.py │ │ ├── 0003_page_js.py │ │ ├── 0004_ustceligible_ustcsnos.py │ │ ├── 0005_qa.py │ │ ├── 0006_credits.py │ │ ├── 0007_accountlog_specialprofileusedrecord_and_more.py │ │ ├── 0008_accountlog_unique_account_log_for_each_type.py │ │ ├── 0009_alter_accountlog_account.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ ├── axios.min.js │ │ ├── chart.js │ │ ├── chart.umd.min.js │ │ ├── chartjs-adapter-moment.min.js │ │ ├── code.css │ │ ├── color.js │ │ ├── date.js │ │ ├── favicon.svg │ │ ├── fontawesome │ │ │ ├── css │ │ │ │ └── all.min.css │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.eot │ │ │ │ ├── fa-brands-400.svg │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.eot │ │ │ │ ├── fa-regular-400.svg │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-regular-400.woff │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ ├── fa-solid-900.eot │ │ │ │ ├── fa-solid-900.svg │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff │ │ │ │ └── fa-solid-900.woff2 │ │ ├── grids-responsive.min.css │ │ ├── katex │ │ │ ├── auto-render.min.js │ │ │ ├── fonts │ │ │ │ ├── KaTeX_AMS-Regular.ttf │ │ │ │ ├── KaTeX_AMS-Regular.woff │ │ │ │ ├── KaTeX_AMS-Regular.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ │ │ ├── KaTeX_Fraktur-Bold.woff │ │ │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ │ │ ├── KaTeX_Fraktur-Regular.woff │ │ │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ │ │ ├── KaTeX_Main-Bold.ttf │ │ │ │ ├── KaTeX_Main-Bold.woff │ │ │ │ ├── KaTeX_Main-Bold.woff2 │ │ │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ │ │ ├── KaTeX_Main-BoldItalic.woff │ │ │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Main-Italic.ttf │ │ │ │ ├── KaTeX_Main-Italic.woff │ │ │ │ ├── KaTeX_Main-Italic.woff2 │ │ │ │ ├── KaTeX_Main-Regular.ttf │ │ │ │ ├── KaTeX_Main-Regular.woff │ │ │ │ ├── KaTeX_Main-Regular.woff2 │ │ │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ │ │ ├── KaTeX_Math-BoldItalic.woff │ │ │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Math-Italic.ttf │ │ │ │ ├── KaTeX_Math-Italic.woff │ │ │ │ ├── KaTeX_Math-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ │ │ ├── KaTeX_SansSerif-Bold.woff │ │ │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ │ │ ├── KaTeX_SansSerif-Italic.woff │ │ │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ │ │ ├── KaTeX_SansSerif-Regular.woff │ │ │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ │ │ ├── KaTeX_Script-Regular.ttf │ │ │ │ ├── KaTeX_Script-Regular.woff │ │ │ │ ├── KaTeX_Script-Regular.woff2 │ │ │ │ ├── KaTeX_Size1-Regular.ttf │ │ │ │ ├── KaTeX_Size1-Regular.woff │ │ │ │ ├── KaTeX_Size1-Regular.woff2 │ │ │ │ ├── KaTeX_Size2-Regular.ttf │ │ │ │ ├── KaTeX_Size2-Regular.woff │ │ │ │ ├── KaTeX_Size2-Regular.woff2 │ │ │ │ ├── KaTeX_Size3-Regular.ttf │ │ │ │ ├── KaTeX_Size3-Regular.woff │ │ │ │ ├── KaTeX_Size3-Regular.woff2 │ │ │ │ ├── KaTeX_Size4-Regular.ttf │ │ │ │ ├── KaTeX_Size4-Regular.woff │ │ │ │ ├── KaTeX_Size4-Regular.woff2 │ │ │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ │ │ ├── KaTeX_Typewriter-Regular.woff │ │ │ │ └── KaTeX_Typewriter-Regular.woff2 │ │ │ ├── katex.min.css │ │ │ └── katex.min.js │ │ ├── main.css │ │ ├── menus.js │ │ ├── moment.min.js │ │ ├── pure.min.css │ │ ├── seedrandom.js │ │ └── vue.min.js │ ├── templates │ │ ├── admin │ │ │ └── index.html │ │ ├── admin_announcement.html │ │ ├── admin_base.html │ │ ├── admin_challenge.html │ │ ├── admin_submission.html │ │ ├── admin_terms.html │ │ ├── admin_trigger.html │ │ ├── admin_user.html │ │ ├── announcements.html │ │ ├── base.html │ │ ├── board.html │ │ ├── credits.html │ │ ├── first.html │ │ ├── hub.html │ │ ├── login.html │ │ ├── login_base.html │ │ ├── login_debug.html │ │ ├── login_email.html │ │ ├── login_info.html │ │ ├── login_sms.html │ │ ├── profile.html │ │ ├── qa.html │ │ ├── score.html │ │ ├── terms.html │ │ ├── user.html │ │ └── ustcprofile.html │ ├── tests │ │ ├── __init__.py │ │ ├── auth_test.py │ │ └── command_test.py │ ├── urls.py │ ├── utils.py │ ├── views.py │ └── wsgi.py ├── manage.py ├── requirements-manual.txt ├── requirements.txt └── server │ ├── README.md │ ├── __init__.py │ ├── announcement │ ├── __init__.py │ ├── apps.py │ ├── interface.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── tests.py │ ├── challenge │ ├── __init__.py │ ├── apps.py │ ├── expr_flags.py │ ├── interface.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20191011_2116.py │ │ ├── 0003_rename_url_challenge_url_orig_and_more.py │ │ └── __init__.py │ ├── models.py │ └── tests.py │ ├── context.py │ ├── exceptions.py │ ├── submission │ ├── __init__.py │ ├── apps.py │ ├── interface.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20191005_1331.py │ │ ├── 0003_submission_group.py │ │ ├── 0004_auto_20191011_2116.py │ │ ├── 0005_auto_20191012_2325.py │ │ ├── 0006_auto_20191015_0202.py │ │ ├── 0007_auto_20201022_1721.py │ │ ├── 0008_remove_flagviolation_violation_flag_and_more.py │ │ └── __init__.py │ └── models.py │ ├── terms │ ├── __init__.py │ ├── apps.py │ ├── interface.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── tests.py │ ├── trigger │ ├── __init__.py │ ├── apps.py │ ├── interface.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20201012_2213.py │ │ ├── 0003_trigger_can_update_profile.py │ │ └── __init__.py │ ├── models.py │ └── tests.py │ └── user │ ├── __init__.py │ ├── apps.py │ ├── interface.py │ ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20191010_1458.py │ ├── 0003_user_qq.py │ ├── 0004_auto_20191011_1700.py │ ├── 0005_auto_20191011_1842.py │ ├── 0006_auto_20201019_2248.py │ ├── 0007_user_aff.py │ ├── 0008_userlog.py │ ├── 0009_auto_20211010_1930.py │ ├── 0010_alter_user_options.py │ ├── 0011_add_major_and_campus.py │ ├── 0012_add_groups.py │ ├── 0013_user_suspicious_user_suspicious_reason_and_more.py │ ├── 0014_user_suspicious_ddl_userlog_suspicious_ddl.py │ ├── 0015_update_groups_2023.py │ ├── 0016_add_nyist.py │ └── __init__.py │ ├── models.py │ └── tests.py └── hmcl ├── Dockerfile └── docker-compose.yml /20231222-Containers.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/20231222-Containers.odp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Containers 2 | 3 | 4 | 5 | ## License 6 | 7 | - 代码:MIT 8 | - Slides: CC-BY-NC-SA 4.0 9 | 10 | ## 附件 11 | 12 | - compose 前面的部分大概不需要附件(都是直接跑一/几条命令) 13 | - compose-1: 支持 X11 + GPU + Audio 的容器的 docker-compose 配置 14 | - hackergame: 从 取的一份完整的无 git 历史代码,加上容器化(compose)修改之后的整个代码库 15 | - 主要是时间来不及了,所以出此下策( 16 | - 大概修改不会进入主线,因为目前没有需要 17 | - 运行需要重命名并修改 `.env.example` -> `.env` 18 | - 一部分文件还是需要按照 README 改 19 | - 思考这样的问题:各个容器之间互联用了 TCP socket(而不是 UNIX socket),考虑是什么? 20 | - 因为时间仓促有一些 bug(比如说数据库忘了加 volume),最新版本参考 。 21 | - hmcl: Minecraft! 22 | -------------------------------------------------------------------------------- /compose-1/99-pipewire-default.conf: -------------------------------------------------------------------------------- 1 | pcm.!default { 2 | type pipewire 3 | playback_node "-1" 4 | capture_node "-1" 5 | hint { 6 | show on 7 | description "Default ALSA Output (currently PipeWire Media Server)" 8 | } 9 | } 10 | 11 | ctl.!default { 12 | type pipewire 13 | } 14 | -------------------------------------------------------------------------------- /compose-1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ustclug/ubuntu:22.04 2 | 3 | RUN apt update && apt install -y x11-apps mesa-utils \ 4 | pipewire-audio-client-libraries alsa-utils pulseaudio-utils 5 | COPY 99-pipewire-default.conf /etc/alsa/conf.d/99-pipewire-default.conf 6 | COPY pulse-client.conf /etc/pulse/client.conf 7 | -------------------------------------------------------------------------------- /compose-1/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | desktop: 4 | build: . 5 | environment: 6 | - DISPLAY=$DISPLAY 7 | - XAUTHORITY=$XAUTHORITY 8 | volumes: 9 | - /tmp/.X11-unix:/tmp/.X11-unix 10 | - $XAUTHORITY:$XAUTHORITY 11 | - /dev/dri:/dev/dri 12 | - /run/user/1000/pipewire-0:/run/pipewire/pipewire-0 13 | - /run/user/1000/pulse/native:/run/pulse/native 14 | -------------------------------------------------------------------------------- /compose-1/pulse-client.conf: -------------------------------------------------------------------------------- 1 | # Connect to the host system's PulseAudio server using the bind-mounted UNIX socket 2 | default-server = unix:/run/pulse/native 3 | 4 | # Prevent a PulseAudio server from attempting to spawn in the container 5 | autospawn = no 6 | daemon-binary = /bin/true 7 | 8 | # Prevent the use of shared memory when communicating with the PulseAudio server 9 | enable-shm = false 10 | -------------------------------------------------------------------------------- /hackergame.patch: -------------------------------------------------------------------------------- 1 | Only in hackergame/: challenges 2 | Only in hackergame/conf: local_settings.py 3 | Only in hackergame/conf/nginx-sites: hackergame-docker 4 | diff '--color=auto' -r /home/taoky/Downloads/hackergame-master/conf/settings/docker.py hackergame/conf/settings/docker.py 5 | 1a2 6 | > import os 7 | 2a4,19 8 | > # Official domain and localhost for local test 9 | > ALLOWED_HOSTS = ["hack.lug.ustc.edu.cn", '.localhost', '127.0.0.1', '[::1]'] 10 | > # For local test 11 | > DEBUG = os.environ.get('DEBUG', 'False') == 'True' 12 | > DATABASES = { 13 | > 'default': { 14 | > 'ENGINE': 'django.db.backends.postgresql', 15 | > 'NAME': 'hackergame', 16 | > 'USER': 'hackergame', 17 | > 'CONN_MAX_AGE': 0, 18 | > 'ATOMIC_REQUESTS': True, 19 | > 'HOST': 'hackergame-pgbouncer', 20 | > 'PORT': 5432, 21 | > 'PASSWORD': os.environ.get('DB_PASSWORD', ''), 22 | > }, 23 | > } 24 | 6c23 25 | < 'LOCATION': 'memcached:11211', 26 | --- 27 | > 'LOCATION': 'hackergame-memcached:11211', 28 | diff '--color=auto' -r /home/taoky/Downloads/hackergame-master/conf/uwsgi-apps/hackergame-docker.ini hackergame/conf/uwsgi-apps/hackergame-docker.ini 29 | 2c2 30 | < socket=unix:///run/uwsgi/app/hackergame-docker/socket 31 | --- 32 | > socket=:2018 33 | 6d5 34 | < env=DJANGO_SETTINGS_MODULE=conf.settings.hackergame 35 | 13,15c12,14 36 | < home=/usr/local 37 | < uid=www-data 38 | < gid=www-data 39 | --- 40 | > #home=/usr/local 41 | > #uid=www-data 42 | > #gid=www-data 43 | Only in hackergame/: docker 44 | diff '--color=auto' -r /home/taoky/Downloads/hackergame-master/docker-compose.yml hackergame/docker-compose.yml 45 | 7c7 46 | < image: ghcr.io/ustclug/hackergame:latest 47 | --- 48 | > build: . 49 | 10a11,13 50 | > - DB_PASSWORD=${DB_PASSWORD} 51 | > # 调试用 52 | > - DEBUG=True 53 | 13,16d15 54 | < # 容器外使用 /run/uwsgi/app/hackergame-docker/socket 提供服务 55 | < # 需要提前将 /run/uwsgi/app/hackergame-docker/ 的 owner 修改为 www-data 56 | < # 参考 conf/nginx-sites/hackergame 的配置修改 nginx 配置 57 | < - /run/uwsgi/app/hackergame-docker/:/run/uwsgi/app/hackergame-docker/ 58 | 18,20c17,18 59 | < - /var/opt/hackergame/:/var/opt/hackergame/ 60 | < # 数据库,需要在容器外配置好 postgresql 和 pgbouncer 61 | < - /var/run/postgresql/:/var/run/postgresql/ 62 | --- 63 | > - hackergame-static:/var/opt/hackergame/ 64 | > # 很不幸,你可能还需要 bind 完整的题目目录进来(不然不方便导入) 65 | 22a21 66 | > - pgbouncer 67 | 24c23 68 | < container_name: memcached 69 | --- 70 | > container_name: hackergame-memcached 71 | 26a26,62 72 | > postgresql: 73 | > container_name: hackergame-postgresql 74 | > image: postgres:15 75 | > restart: always 76 | > environment: 77 | > - POSTGRES_USER=hackergame 78 | > - POSTGRES_PASSWORD=${DB_PASSWORD} 79 | > - POSTGRES_DB=hackergame 80 | > pgbouncer: 81 | > container_name: hackergame-pgbouncer 82 | > image: edoburu/pgbouncer:latest 83 | > restart: always 84 | > environment: 85 | > - DB_USER=hackergame 86 | > - DB_PASSWORD=${DB_PASSWORD} 87 | > - DB_HOST=postgresql 88 | > - POOL_MODE=transaction 89 | > # 坑: pg14+ 默认使用 scram-sha-256, 而 pgbouncer 默认是 md5 90 | > - AUTH_TYPE=scram-sha-256 91 | > depends_on: 92 | > - postgresql 93 | > nginx: 94 | > container_name: hackergame-nginx 95 | > image: nginx:1.25 96 | > restart: always 97 | > volumes: 98 | > - ./conf/nginx-sites/hackergame-docker:/etc/nginx/conf.d/default.conf:ro 99 | > - hackergame-static:/var/opt/hackergame/:ro 100 | > - nginx-log:/var/log/nginx/:rw 101 | > ports: 102 | > - 12345:80 103 | > depends_on: 104 | > - hackergame 105 | > 106 | > volumes: 107 | > hackergame-static: 108 | > nginx-log: 109 | diff '--color=auto' -r /home/taoky/Downloads/hackergame-master/Dockerfile hackergame/Dockerfile 110 | 15,17c15,16 111 | < CMD ["/usr/local/bin/uwsgi", "--master", "--ini", "conf/uwsgi.ini", \ 112 | < "--ini", "conf/uwsgi-apps/hackergame-docker.ini", \ 113 | < "--set-placeholder", "appname=hackergame-docker"] 114 | --- 115 | > EXPOSE 2018 116 | > CMD ["docker/start.sh"] 117 | Only in hackergame/: .env 118 | Only in hackergame/: .env.example 119 | -------------------------------------------------------------------------------- /hackergame/.dockerignore: -------------------------------------------------------------------------------- 1 | # Dockerignore-specific 2 | .git 3 | .github 4 | .gitignore 5 | docker-compose.yml 6 | README.md 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | # PyCharm 114 | .idea/ 115 | -------------------------------------------------------------------------------- /hackergame/.env.example: -------------------------------------------------------------------------------- 1 | DB_PASSWORD=example -------------------------------------------------------------------------------- /hackergame/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "github-actions" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "monthly" 16 | -------------------------------------------------------------------------------- /hackergame/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | IMAGE: 'ghcr.io/ustclug/hackergame:latest' 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Build Docker image 18 | run: | 19 | docker pull "$IMAGE" 20 | docker build -t "$IMAGE" --cache-from "$IMAGE" . 21 | - name: Push Docker image 22 | run: | 23 | docker login ghcr.io -u ${{ github.actor }} --password-stdin <<< "${{ secrets.GITHUB_TOKEN }}" 24 | docker push "$IMAGE" 25 | -------------------------------------------------------------------------------- /hackergame/.github/workflows/smoketest.yml: -------------------------------------------------------------------------------- 1 | name: smoke test (dev) 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | DJANGO_SETTINGS_MODULE: conf.settings.dev 12 | strategy: 13 | max-parallel: 3 14 | matrix: 15 | # Debian 12, 11 16 | # Debian 10 uses Python 3.7, which is not supported by Django 4.2 LTS 17 | python-version: [3.11, 3.9] 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Configuration 27 | run: | 28 | python3 -m venv .venv 29 | source .venv/bin/activate 30 | pip install --upgrade pip 31 | pip install -r requirements.txt 32 | 33 | cp conf/local_settings.py.example conf/local_settings.py 34 | PRIVATE_KEY=$(openssl ecparam -name secp256k1 -genkey -noout) 35 | 36 | sed -i "s!openssl ecparam -name secp256k1 -genkey -noout!${PRIVATE_KEY//$'\n'/\\n}!g" conf/local_settings.py 37 | CERTIFICATE=$(openssl req -x509 -key conf/local_settings.py -days 30 -subj "/CN=notforproduction") 38 | sed -i "s!openssl req -x509 -key conf/local_settings.py -days 30!${CERTIFICATE//$'\n'/\\n}!g" conf/local_settings.py 39 | 40 | # set SECRET_KEY 41 | sed -i "s/SECRET_KEY = ''/SECRET_KEY = 'test'/g" conf/local_settings.py 42 | 43 | mkdir var 44 | - name: Run smoke test 45 | run: | 46 | source .venv/bin/activate 47 | ./manage.py migrate 48 | ./manage.py setup 49 | ./manage.py fake_data 50 | ./manage.py test 51 | ./manage.py runserver & 52 | 53 | # wait for few seconds 54 | sleep 2 55 | 56 | # try curling 57 | response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/) 58 | if [ "$response" != "200" ]; then 59 | echo "Failed to curl http://localhost:8000/" 60 | exit 1 61 | fi 62 | -------------------------------------------------------------------------------- /hackergame/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm 107 | .idea/ 108 | -------------------------------------------------------------------------------- /hackergame/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | RUN apt-get update && \ 4 | apt-get clean && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | ENV PYTHONUNBUFFERED=1 8 | WORKDIR /opt/hackergame 9 | COPY requirements.txt /opt/hackergame/ 10 | RUN pip3 install --upgrade -r requirements.txt 11 | # Bind project inside instead of copying it 12 | # to avoid copying credentials inside container 13 | # COPY ./ /opt/hackergame/ 14 | 15 | EXPOSE 2018 16 | CMD ["docker/start.sh"] 17 | -------------------------------------------------------------------------------- /hackergame/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hypercube and others 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hackergame/challenges/example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | COPY example.py / 3 | CMD ["/usr/local/bin/python3", "-u", "/example.py"] 4 | -------------------------------------------------------------------------------- /hackergame/challenges/example/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | enabled: true 3 | name: 示例题目 4 | category: general 5 | url: http://127.0.0.1:10001/?token={token} 6 | prompt: flag{...} 7 | index: 0 8 | flags: 9 | - name: flag1 10 | score: 100 11 | type: expr 12 | flag: f"flag{{this_is_an_example_{sha256('example1'+token)[:10]}}}" 13 | - name: flag2 14 | score: 200 15 | type: expr 16 | flag: f"flag{{this_is_the_second_flag_{sha256('example2'+token)[:10]}}}" 17 | --- 18 | 19 | 除了网页终端,你也可以通过 `nc 127.0.0.1 10000` 来连接 20 | -------------------------------------------------------------------------------- /hackergame/challenges/example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | challenge: 4 | build: . 5 | entrypoint: ["/bin/true"] 6 | front: 7 | extends: 8 | file: ../dynamic_flag/docker-compose.yml 9 | service: front 10 | depends_on: 11 | - challenge 12 | web: 13 | extends: 14 | file: ../web_netcat/docker-compose.yml 15 | service: web 16 | -------------------------------------------------------------------------------- /hackergame/challenges/example/example.py: -------------------------------------------------------------------------------- 1 | print("Your first flag:", open("flag1").read()) 2 | print("Answer the question to get your second flag") 3 | if input("1+1=").strip() == "2": 4 | print(open("flag2").read()) 5 | else: 6 | print("Wrong!") 7 | -------------------------------------------------------------------------------- /hackergame/conf/local_settings.py.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY = '' 2 | PRIVATE_KEY = ''' 3 | openssl ecparam -name secp256k1 -genkey -noout 4 | ''' 5 | CERTIFICATE = ''' 6 | openssl req -x509 -key conf/local_settings.py -days 30 7 | ''' 8 | GOOGLE_APP_SECRET = '' 9 | MICROSOFT_APP_SECRET = '' 10 | SMS_ACCESS_KEY_SECRET = '' 11 | SMTP_HOSTNAME = '' 12 | SMTP_USERNAME = '' 13 | SMTP_PASSWORD = '' 14 | EXTERNAL_LOGINS = { 15 | 'example': { 16 | 'url': 'https://lug.example.edu.cn/api/v1/sendmail', 17 | 'key': '1234567', 18 | 'use_smtp': False, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hackergame/conf/logrotate/uwsgi: -------------------------------------------------------------------------------- 1 | "/var/log/uwsgi/app/*.log" { 2 | copytruncate 3 | daily 4 | rotate -1 5 | compress 6 | delaycompress 7 | missingok 8 | notifempty 9 | } -------------------------------------------------------------------------------- /hackergame/conf/nginx-sites/hackergame: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name hack.lug.ustc.edu.cn; 4 | 5 | charset utf-8; 6 | sendfile on; 7 | tcp_nopush on; 8 | tcp_nodelay on; 9 | server_tokens off; 10 | log_not_found off; 11 | 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | access_log /var/log/nginx/hackergame.log; 16 | error_log /var/log/nginx/hackergame.error.log; 17 | 18 | gzip on; 19 | gzip_vary on; 20 | gzip_proxied any; 21 | gzip_comp_level 6; 22 | gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; 23 | 24 | location /media { 25 | root /var/opt/hackergame; 26 | add_header Content-Type application/octet-stream; 27 | expires -1; 28 | } 29 | location /static { 30 | root /var/opt/hackergame; 31 | expires 1h; 32 | } 33 | location / { 34 | uwsgi_pass unix:///run/uwsgi/app/hackergame/socket; 35 | client_max_body_size 500M; 36 | include /etc/nginx/uwsgi_params; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hackergame/conf/nginx-sites/hackergame-docker: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | #server_name hack.lug.ustc.edu.cn; 4 | server_name _; 5 | 6 | charset utf-8; 7 | sendfile on; 8 | tcp_nopush on; 9 | tcp_nodelay on; 10 | server_tokens off; 11 | log_not_found off; 12 | 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | 16 | access_log /var/log/nginx/hackergame.log; 17 | error_log /var/log/nginx/hackergame.error.log; 18 | 19 | gzip on; 20 | gzip_vary on; 21 | gzip_proxied any; 22 | gzip_comp_level 6; 23 | gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; 24 | 25 | location /media { 26 | root /var/opt/hackergame; 27 | add_header Content-Type application/octet-stream; 28 | expires -1; 29 | } 30 | location /static { 31 | root /var/opt/hackergame; 32 | expires 1h; 33 | } 34 | location / { 35 | uwsgi_pass hackergame:2018; 36 | client_max_body_size 500M; 37 | include /etc/nginx/uwsgi_params; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /hackergame/conf/nginx-sites/hgtest: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name hgtest.lug.ustc.edu.cn; 4 | 5 | charset utf-8; 6 | sendfile on; 7 | tcp_nopush on; 8 | tcp_nodelay on; 9 | server_tokens off; 10 | log_not_found off; 11 | 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | access_log /var/log/nginx/hgtest.log; 16 | error_log /var/log/nginx/hgtest.error.log; 17 | 18 | gzip on; 19 | gzip_vary on; 20 | gzip_proxied any; 21 | gzip_comp_level 6; 22 | gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; 23 | 24 | location /media { 25 | root /var/opt/hgtest; 26 | add_header Content-Type application/octet-stream; 27 | expires -1; 28 | } 29 | location /static { 30 | root /var/opt/hgtest; 31 | expires 1h; 32 | } 33 | location / { 34 | uwsgi_pass unix:///run/uwsgi/app/hgtest/socket; 35 | client_max_body_size 500M; 36 | include /etc/nginx/uwsgi_params; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hackergame/conf/settings/base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ..local_settings import * 3 | 4 | 5 | ROOT_URLCONF = 'frontend.urls' 6 | WSGI_APPLICATION = 'frontend.wsgi.application' 7 | INSTALLED_APPS = [ 8 | 'frontend', 9 | 'server.announcement', 10 | 'server.challenge', 11 | 'server.submission', 12 | 'server.terms', 13 | 'server.trigger', 14 | 'server.user', 15 | 'allauth', 16 | 'allauth.account', 17 | 'allauth.socialaccount', 18 | 'allauth.socialaccount.providers.google', 19 | 'allauth.socialaccount.providers.microsoft', 20 | 'django.contrib.admin', 21 | 'django.contrib.auth', 22 | 'django.contrib.contenttypes', 23 | 'django.contrib.sessions', 24 | 'django.contrib.messages', 25 | 'django.contrib.sites', 26 | 'django.contrib.staticfiles', 27 | ] 28 | MIDDLEWARE = [ 29 | 'django.middleware.security.SecurityMiddleware', 30 | 'django.contrib.sessions.middleware.SessionMiddleware', 31 | 'django.middleware.common.CommonMiddleware', 32 | 'django.middleware.csrf.CsrfViewMiddleware', 33 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 34 | 'django.contrib.messages.middleware.MessageMiddleware', 35 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 36 | 'frontend.middleware.UserRequestMiddleware', 37 | 'allauth.account.middleware.AccountMiddleware', 38 | ] 39 | 40 | # Auth 41 | AUTHENTICATION_BACKENDS = [ 42 | 'django.contrib.auth.backends.ModelBackend', 43 | 'allauth.account.auth_backends.AuthenticationBackend', 44 | ] 45 | 46 | # Database 47 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 48 | 49 | # Media 50 | MEDIA_URL = '/media/' 51 | 52 | # Site 53 | SITE_ID = 1 54 | 55 | # Static 56 | STATIC_URL = '/static/' 57 | 58 | # Template 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'frontend.context_processors.frontend', 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.i18n', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | # Email 77 | EMAIL_SUBJECT_PREFIX = '[Hackergame] ' 78 | ADMINS = [('Hypercube', 'hypercube@0x01.me')] 79 | DEFAULT_FROM_EMAIL_NAME = 'Hackergame' 80 | DEFAULT_FROM_EMAIL_EMAIL = 'hackergame@ustclug.org' 81 | DEFAULT_FROM_EMAIL = f'{DEFAULT_FROM_EMAIL_NAME} <{DEFAULT_FROM_EMAIL_EMAIL}>' 82 | SERVER_EMAIL = 'hackergame@ustclug.org' 83 | EMAIL_TIMEOUT = 15 84 | 85 | # I18N and L10N 86 | USE_I18N = True 87 | USE_L10N = True 88 | USE_TZ = True 89 | TIME_ZONE = 'Asia/Shanghai' 90 | LANGUAGE_CODE = 'zh-Hans' 91 | 92 | # allauth 93 | LOGIN_REDIRECT_URL = 'hub' 94 | SOCIALACCOUNT_QUERY_EMAIL = True 95 | SOCIALACCOUNT_ADAPTER = 'frontend.adapters.SocialAccountAdapter' 96 | ACCOUNT_EMAIL_VERIFICATION = 'none' 97 | SOCIALACCOUNT_LOGIN_ON_GET = True 98 | SOCIALACCOUNT_PROVIDERS = { 99 | 'microsoft': { 100 | 'TENANT': "consumers" 101 | } 102 | } -------------------------------------------------------------------------------- /hackergame/conf/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | ALLOWED_HOSTS = ['*'] 5 | MEDIA_ROOT = 'var/media' 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': 'var/db.sqlite3', 11 | 'ATOMIC_REQUESTS': True, 12 | }, 13 | } 14 | 15 | LOGGING = { 16 | 'version': 1, 17 | 'disable_existing_loggers': False, 18 | 'formatters': { 19 | 'request': { 20 | 'format': '%(asctime)s %(ip)s %(userid)s %(levelname)s %(message)s', 21 | }, 22 | }, 23 | 'filters': { 24 | 'add_user_info': { 25 | '()': 'frontend.utils.UserInfoFilter' 26 | }, 27 | }, 28 | 'loggers': { 29 | 'django.server': { 30 | 'handlers': ['databaselog'], 31 | 'propagate': True, 32 | }, 33 | 'django.db.backends': { 34 | 'level': 'DEBUG', 35 | 'handlers': ['databaselog'], 36 | }, 37 | 'django.request': { 38 | 'filters': ['add_user_info'], 39 | 'handlers': ['request'], 40 | 'propagate': False, 41 | }, 42 | }, 43 | 'handlers': { 44 | 'databaselog': { 45 | 'level': 'DEBUG', 46 | 'class': 'logging.FileHandler', 47 | 'filename': 'var/database.log', 48 | 'mode': 'w', 49 | }, 50 | 'request': { 51 | 'level': 'INFO', 52 | 'class': 'logging.StreamHandler', 53 | 'formatter': 'request', 54 | }, 55 | }, 56 | } 57 | 58 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 59 | 60 | GOOGLE_APP_ID = '2574063612-e44k22j4071rjntvm9rpkgipv1u85svm' \ 61 | '.apps.googleusercontent.com' 62 | MICROSOFT_APP_ID = '6a243fe9-a603-4c6e-b6bd-5af20b7f460e' 63 | SMS_ACCESS_KEY_ID = 'LTAI4FmgeKHNWB7WbTwTP7d9' 64 | -------------------------------------------------------------------------------- /hackergame/conf/settings/docker.py: -------------------------------------------------------------------------------- 1 | from .hackergame import * 2 | import os 3 | 4 | # Official domain and localhost for local test 5 | ALLOWED_HOSTS = ["hack.lug.ustc.edu.cn", '.localhost', '127.0.0.1', '[::1]'] 6 | # For local test 7 | DEBUG = os.environ.get('DEBUG', 'False') == 'True' 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.postgresql', 11 | 'NAME': 'hackergame', 12 | 'USER': 'hackergame', 13 | 'CONN_MAX_AGE': 0, 14 | 'ATOMIC_REQUESTS': True, 15 | 'HOST': 'hackergame-pgbouncer', 16 | 'PORT': 5432, 17 | 'PASSWORD': os.environ.get('DB_PASSWORD', ''), 18 | }, 19 | } 20 | CACHES = { 21 | 'default': { 22 | 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 23 | 'LOCATION': 'hackergame-memcached:11211', 24 | 'TIMEOUT': 3600, 25 | 'KEY_PREFIX': 'hackergame', 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /hackergame/conf/settings/hackergame.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | ALLOWED_HOSTS = ['hack.lug.ustc.edu.cn'] 5 | MEDIA_ROOT = '/var/opt/hackergame/media' 6 | STATIC_ROOT = '/var/opt/hackergame/static' 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.postgresql', 11 | 'NAME': 'hackergame', 12 | 'USER': 'hackergame', 13 | 'CONN_MAX_AGE': 0, 14 | 'ATOMIC_REQUESTS': True, 15 | 'HOST': '/var/run/postgresql', 16 | 'PORT': 6432, 17 | }, 18 | } 19 | CACHES = { 20 | 'default': { 21 | 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 22 | 'LOCATION': '127.0.0.1:11211', 23 | 'TIMEOUT': 3600, 24 | 'KEY_PREFIX': 'hackergame', 25 | }, 26 | } 27 | SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' 28 | SESSION_COOKIE_AGE = 365 * 86400 29 | LOGGING = { 30 | 'version': 1, 31 | 'disable_existing_loggers': False, 32 | 'formatters': { 33 | 'django.server': { 34 | '()': 'django.utils.log.ServerFormatter', 35 | 'format': '[{server_time}] {message}', 36 | 'style': '{', 37 | }, 38 | 'request': { 39 | 'format': '%(asctime)s %(ip)s %(userid)s %(levelname)s %(message)s', 40 | }, 41 | }, 42 | 'filters': { 43 | 'add_user_info': { 44 | '()': 'frontend.utils.UserInfoFilter' 45 | }, 46 | }, 47 | 'handlers': { 48 | 'django.server': { 49 | 'level': 'INFO', 50 | 'class': 'logging.StreamHandler', 51 | 'formatter': 'django.server', 52 | }, 53 | 'mail_admins': { 54 | 'level': 'ERROR', 55 | 'class': 'frontend.utils.ThrottledAdminEmailHandler', 56 | }, 57 | 'request': { 58 | 'class': 'logging.StreamHandler', 59 | 'formatter': 'request', 60 | }, 61 | }, 62 | 'loggers': { 63 | 'django': { 64 | 'handlers': ['mail_admins', 'django.server'], 65 | 'level': 'INFO', 66 | }, 67 | 'django.server': { 68 | 'handlers': ['django.server'], 69 | 'level': 'INFO', 70 | 'propagate': False, 71 | }, 72 | 'django.request': { 73 | 'filters': ['add_user_info'], 74 | 'handlers': ['request'], 75 | 'propagate': False, 76 | }, 77 | }, 78 | } 79 | 80 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 81 | EMAIL_HOST = SMTP_HOSTNAME 82 | EMAIL_PORT = 587 83 | EMAIL_HOST_USER = SMTP_USERNAME 84 | EMAIL_HOST_PASSWORD = SMTP_PASSWORD 85 | EMAIL_USE_TLS = True 86 | 87 | GOOGLE_APP_ID = '2574063612-kstsrirbttbimgk2da2ju1mmbh8t0ogk' \ 88 | '.apps.googleusercontent.com' 89 | MICROSOFT_APP_ID = '6474be41-5098-4bbe-80dc-95d7ae9660a5' 90 | SMS_ACCESS_KEY_ID = 'LTAI4FmgeKHNWB7WbTwTP7d9' 91 | -------------------------------------------------------------------------------- /hackergame/conf/settings/hgtest.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | ALLOWED_HOSTS = ['hgtest.lug.ustc.edu.cn'] 5 | MEDIA_ROOT = '/var/opt/hgtest/media' 6 | STATIC_ROOT = '/var/opt/hgtest/static' 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.postgresql', 11 | 'NAME': 'hgtest', 12 | 'USER': 'hgtest', 13 | 'CONN_MAX_AGE': 0, 14 | 'ATOMIC_REQUESTS': True, 15 | 'HOST': '/var/run/postgresql', 16 | 'PORT': 6432, 17 | }, 18 | } 19 | CACHES = { 20 | 'default': { 21 | 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 22 | 'LOCATION': '127.0.0.1:11211', 23 | 'TIMEOUT': 3600, 24 | 'KEY_PREFIX': 'hgtest', 25 | }, 26 | } 27 | SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' 28 | SESSION_COOKIE_AGE = 365 * 86400 29 | LOGGING = { 30 | 'version': 1, 31 | 'disable_existing_loggers': False, 32 | 'formatters': { 33 | 'django.server': { 34 | '()': 'django.utils.log.ServerFormatter', 35 | 'format': '[{server_time}] {message}', 36 | 'style': '{', 37 | }, 38 | 'request': { 39 | 'format': '%(asctime)s %(ip)s %(userid)s %(levelname)s %(message)s', 40 | } 41 | }, 42 | 'filters': { 43 | 'add_user_info': { 44 | '()': 'frontend.utils.UserInfoFilter' 45 | }, 46 | }, 47 | 'handlers': { 48 | 'django.server': { 49 | 'level': 'INFO', 50 | 'class': 'logging.StreamHandler', 51 | 'formatter': 'django.server', 52 | }, 53 | 'mail_admins': { 54 | 'level': 'ERROR', 55 | 'class': 'frontend.utils.ThrottledAdminEmailHandler', 56 | }, 57 | 'request': { 58 | 'class': 'logging.StreamHandler', 59 | 'formatter': 'request', 60 | }, 61 | }, 62 | 'loggers': { 63 | 'django': { 64 | 'handlers': ['mail_admins', 'django.server'], 65 | 'level': 'INFO', 66 | }, 67 | 'django.server': { 68 | 'handlers': ['django.server'], 69 | 'level': 'INFO', 70 | 'propagate': False, 71 | }, 72 | 'django.request': { 73 | 'filters': ['add_user_info'], 74 | 'handlers': ['request'], 75 | 'propagate': False, 76 | }, 77 | }, 78 | } 79 | 80 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 81 | EMAIL_HOST = SMTP_HOSTNAME 82 | EMAIL_PORT = 587 83 | EMAIL_HOST_USER = SMTP_USERNAME 84 | EMAIL_HOST_PASSWORD = SMTP_PASSWORD 85 | EMAIL_USE_TLS = True 86 | EMAIL_SUBJECT_PREFIX = '[hgtest] ' 87 | 88 | GOOGLE_APP_ID = '2574063612-p2ss2hgg9rr7c67n9d0e4g3l6j9gk8v2' \ 89 | '.apps.googleusercontent.com' 90 | MICROSOFT_APP_ID = '6a243fe9-a603-4c6e-b6bd-5af20b7f460e' 91 | SMS_ACCESS_KEY_ID = 'LTAI4FmgeKHNWB7WbTwTP7d9' 92 | -------------------------------------------------------------------------------- /hackergame/conf/systemd/hackergame@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # available options: 3 | # hackergame@hackergame.service, or hackergame@hgtest.service 4 | Description=Hackergame Platform (%i) 5 | After=syslog.target 6 | 7 | [Service] 8 | ExecStart=/opt/%i/.venv/bin/uwsgi --ini /opt/%i/conf/uwsgi-apps/%i.ini --ini /opt/%i/conf/uwsgi.ini --set-placeholder appname=%i --daemonize /var/log/uwsgi/app/%i.log 9 | ExecReload=/bin/kill -HUP $MAINPID 10 | LogsDirectory=uwsgi/app 11 | RuntimeDirectory=uwsgi/app/%i 12 | Restart=always 13 | KillSignal=SIGQUIT 14 | Type=forking 15 | User=www-data 16 | Group=www-data 17 | 18 | [Install] 19 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /hackergame/conf/uwsgi-apps/hackergame-docker.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=:2018 3 | chdir=/opt/hackergame 4 | #plugin=python3,gevent_python3 5 | module=frontend.wsgi:application 6 | env=PSYCOPG_WAIT_FUNC=wait_selector 7 | master=true 8 | #processes=16 9 | gevent=1024 10 | gevent-monkey-patch=true 11 | vacuum=true 12 | #home=/usr/local 13 | #uid=www-data 14 | #gid=www-data 15 | stats=/run/uwsgi/app/hackergame-docker/stats.socket 16 | harakiri=60 17 | -------------------------------------------------------------------------------- /hackergame/conf/uwsgi-apps/hackergame.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=unix:///run/uwsgi/app/hackergame/socket 3 | chdir=/opt/hackergame 4 | #plugin=python3,gevent_python3 5 | module=frontend.wsgi:application 6 | env=DJANGO_SETTINGS_MODULE=conf.settings.hackergame 7 | env=PSYCOPG_WAIT_FUNC=wait_selector 8 | master=true 9 | #processes=16 10 | gevent=1024 11 | gevent-monkey-patch=true 12 | vacuum=true 13 | home=.venv 14 | stats=/run/uwsgi/app/hackergame/stats.socket 15 | harakiri=60 16 | -------------------------------------------------------------------------------- /hackergame/conf/uwsgi-apps/hgtest.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=unix:///run/uwsgi/app/hgtest/socket 3 | chdir=/opt/hgtest 4 | #plugin=python3,gevent_python3 5 | module=frontend.wsgi:application 6 | env=DJANGO_SETTINGS_MODULE=conf.settings.hgtest 7 | env=PSYCOPG_WAIT_FUNC=wait_selector 8 | master=true 9 | #processes=16 10 | gevent=1024 11 | gevent-monkey-patch=true 12 | vacuum=true 13 | home=.venv 14 | stats=/run/uwsgi/app/hgtest/stats.socket 15 | harakiri=60 16 | -------------------------------------------------------------------------------- /hackergame/conf/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | workers = 8 4 | listen = 1024 5 | no-orphans = true 6 | pidfile = /run/uwsgi/app/%(appname)/pid 7 | socket = /run/uwsgi/app/%(appname)/socket 8 | chown-socket = www-data 9 | chmod-socket = 660 10 | log-date = true 11 | uid = www-data 12 | gid = www-data 13 | -------------------------------------------------------------------------------- /hackergame/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | 3 | services: 4 | hackergame: 5 | container_name: &name hackergame 6 | hostname: *name 7 | build: . 8 | restart: always 9 | environment: 10 | - DJANGO_SETTINGS_MODULE=conf.settings.docker 11 | - DB_PASSWORD=${DB_PASSWORD} 12 | # 调试用 13 | - DEBUG=True 14 | volumes: 15 | - .:/opt/hackergame/:ro 16 | # 存储静态网页与题目文件 17 | - hackergame-static:/var/opt/hackergame/ 18 | # 很不幸,你可能还需要 bind 完整的题目目录进来(不然不方便导入) 19 | depends_on: 20 | - memcached 21 | - pgbouncer 22 | memcached: 23 | container_name: hackergame-memcached 24 | image: memcached 25 | restart: always 26 | postgresql: 27 | container_name: hackergame-postgresql 28 | image: postgres:15 29 | restart: always 30 | environment: 31 | - POSTGRES_USER=hackergame 32 | - POSTGRES_PASSWORD=${DB_PASSWORD} 33 | - POSTGRES_DB=hackergame 34 | pgbouncer: 35 | container_name: hackergame-pgbouncer 36 | image: edoburu/pgbouncer:latest 37 | restart: always 38 | environment: 39 | - DB_USER=hackergame 40 | - DB_PASSWORD=${DB_PASSWORD} 41 | - DB_HOST=postgresql 42 | - POOL_MODE=transaction 43 | # 坑: pg14+ 默认使用 scram-sha-256, 而 pgbouncer 默认是 md5 44 | - AUTH_TYPE=scram-sha-256 45 | depends_on: 46 | - postgresql 47 | nginx: 48 | container_name: hackergame-nginx 49 | image: nginx:1.25 50 | restart: always 51 | volumes: 52 | - ./conf/nginx-sites/hackergame-docker:/etc/nginx/conf.d/default.conf:ro 53 | - hackergame-static:/var/opt/hackergame/:ro 54 | - nginx-log:/var/log/nginx/:rw 55 | ports: 56 | - 12345:80 57 | depends_on: 58 | - hackergame 59 | 60 | volumes: 61 | hackergame-static: 62 | nginx-log: 63 | -------------------------------------------------------------------------------- /hackergame/docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Set permission for /run/uwsgi/app/hackergame-docker/ 4 | mkdir -p /run/uwsgi/app/hackergame-docker/ 5 | chown www-data:www-data /run/uwsgi/app/hackergame-docker/ 6 | 7 | echo "Note that /opt/hackergame/ shall be readable by uwsgi (www-data in container)." 8 | echo "You could set it to be readable by everyone: chmod -R a+rX hackergame/" 9 | 10 | echo "If this is your first time to run this container, you should run:" 11 | echo " docker exec -it hackergame ./manage.py migrate" 12 | echo " docker exec -it hackergame ./manage.py collectstatic" 13 | 14 | # Start uwsgi 15 | exec /usr/local/bin/uwsgi --master --ini conf/uwsgi.ini \ 16 | --ini conf/uwsgi-apps/hackergame-docker.ini \ 17 | --set-placeholder appname=hackergame-docker 18 | -------------------------------------------------------------------------------- /hackergame/frontend/README.md: -------------------------------------------------------------------------------- 1 | 这是一个特别的 Django app,它只负责和网页界面相关的逻辑,不管理业务逻辑。 2 | -------------------------------------------------------------------------------- /hackergame/frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/__init__.py -------------------------------------------------------------------------------- /hackergame/frontend/adapters.py: -------------------------------------------------------------------------------- 1 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 2 | from server.user.interface import User 3 | from server.context import Context 4 | 5 | 6 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 7 | def save_user(self, request, sociallogin, form=None): 8 | super().save_user(request, sociallogin, form) 9 | user = sociallogin.user 10 | User.create( 11 | Context(elevated=True), 12 | group='other', 13 | user=user, 14 | email=user.email, 15 | ) 16 | return user 17 | -------------------------------------------------------------------------------- /hackergame/frontend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django.contrib.auth.admin import UserAdmin as BaseDjangoUserAdmin 4 | from django.contrib.auth.models import User as DjangoUser 5 | 6 | from server.announcement.models import Announcement 7 | from server.challenge.models import Challenge 8 | from server.submission.models import Submission 9 | from server.terms.models import Terms 10 | from server.trigger.models import Trigger 11 | from server.user.models import User 12 | from .models import Page, Account, Code, AccountLog, SpecialProfileUsedRecord, Qa, Credits 13 | 14 | admin.site.register([Page, Account, Code, Qa, Credits, SpecialProfileUsedRecord]) 15 | 16 | 17 | class PermissionListFilter(admin.SimpleListFilter): 18 | title = '权限' 19 | parameter_name = 'permission' 20 | 21 | def lookups(self, request, model_admin): 22 | return [ 23 | ("has_permission", "有非空「用户权限」的用户"), 24 | ] 25 | 26 | def queryset(self, request, queryset): 27 | if self.value() == "has_permission": 28 | return queryset.filter(user_permissions__isnull=False) 29 | 30 | 31 | class DjangoUserAdmin(BaseDjangoUserAdmin): 32 | def __init__(self, model, admin_site) -> None: 33 | super().__init__(model, admin_site) 34 | self.list_filter += (PermissionListFilter, ) 35 | 36 | 37 | admin.site.unregister(DjangoUser) 38 | admin.site.register(DjangoUser, DjangoUserAdmin) 39 | 40 | # XXX: Hack here 41 | # I also replaced template `admin/index.html`, so that these entries 42 | # show up on the admin page, while low-level models still kept 43 | # inaccessible. 44 | @admin.register(Announcement, Challenge, Submission, Terms, Trigger, User) 45 | class FakeAdmin(admin.ModelAdmin): 46 | def get_model_perms(self, request): 47 | return {None: True} 48 | 49 | def has_add_permission(self, request): 50 | return False 51 | 52 | def has_change_permission(self, request, obj=None): 53 | return False 54 | 55 | def has_delete_permission(self, request, obj=None): 56 | return False 57 | 58 | def has_view_permission(self, request, obj=None): 59 | return False 60 | 61 | 62 | @admin.register(AccountLog) 63 | class AccountLogAdmin(admin.ModelAdmin): 64 | search_fields = ["account__pk", "contents"] 65 | -------------------------------------------------------------------------------- /hackergame/frontend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FrontendConfig(AppConfig): 5 | name = 'frontend' 6 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/README.md: -------------------------------------------------------------------------------- 1 | # 登录方式 2 | 3 | ## 对协办单位的登录方式要求 4 | 5 | 新的设计为协办单位至少提供一种用户认证的接入方式: 6 | 7 | - Yale CAS(推荐) 8 | - OAuth2(推荐) 9 | - 基于 HTTP POST JSON 的邮件接口(见 [external.py](./external.py)) 10 | 11 | 尽管代码仍然支持,我们计划尽量避免使用直接 SMTP 发信的方式,因为往年的经验表明邮件的到达率问题非常大。 12 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/cas.py: -------------------------------------------------------------------------------- 1 | from urllib.error import URLError 2 | from urllib.parse import urlencode 3 | from urllib.request import urlopen 4 | from xml.etree import ElementTree 5 | 6 | from django.http import HttpRequest 7 | from django.contrib import messages 8 | from django.shortcuts import redirect 9 | 10 | from typing import Optional, Any 11 | 12 | from .base import BaseLoginView 13 | 14 | 15 | class CASBaseLoginView(BaseLoginView): 16 | cas_name: str 17 | cas_login_url: str 18 | cas_service_validate_url: str 19 | 20 | YALE_CAS_URL = "{http://www.yale.edu/tp/cas}" 21 | 22 | def login_attrs(self) -> dict[str, Any]: 23 | raise NotImplementedError("CAS 登录需要实现 login_attrs()") 24 | 25 | def get(self, request: HttpRequest): 26 | self.service = request.build_absolute_uri(request.path) 27 | self.ticket = request.GET.get("ticket") 28 | if not self.ticket: 29 | return redirect( 30 | self.cas_login_url + "?" + urlencode({"service": self.service}) 31 | ) 32 | if self.check_ticket(): 33 | self.login(**self.login_attrs()) 34 | return redirect("hub") 35 | 36 | def check_ticket(self) -> Optional[ElementTree.Element]: 37 | try: 38 | with urlopen( 39 | self.cas_service_validate_url 40 | + "?" 41 | + urlencode({"service": self.service, "ticket": self.ticket}), 42 | timeout=15, 43 | ) as req: 44 | tree = ElementTree.fromstring(req.read())[0] 45 | except URLError: 46 | messages.error(self.request, f"连接{self.cas_name}出错") 47 | return None 48 | cas = "{http://www.yale.edu/tp/cas}" 49 | if tree.tag != cas + "authenticationSuccess": 50 | messages.error(self.request, "登录失败") 51 | return None 52 | return tree 53 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/debug.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import redirect 3 | from django.urls import path 4 | 5 | from .base import BaseLoginView 6 | 7 | 8 | class LoginView(BaseLoginView): 9 | template_name = 'login_debug.html' 10 | 11 | def post(self, request): 12 | self.provider = request.POST['provider'] 13 | self.group = { 14 | 'debug': 'other', 15 | 'ustc': 'ustc', 16 | 'sms': 'other', 17 | }[self.provider] 18 | self.identity = request.POST['identity'] 19 | self.login() 20 | return redirect('hub') 21 | 22 | 23 | urlpatterns = [ 24 | path('debug/login/', LoginView.as_view(), name='debug_login'), 25 | ] if settings.DEBUG else [] 26 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/external.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from django.conf import settings 4 | from django.core.mail import EmailMessage 5 | from django.shortcuts import redirect 6 | 7 | from .base import BaseGetCodeView, BaseLoginView, ExternalProviderError 8 | 9 | 10 | class ExternalLoginView(BaseLoginView): 11 | template_name = 'login_email.html' 12 | 13 | def post(self, request): 14 | if self.check_code(): 15 | self.login(email=self.identity) 16 | return redirect('hub') 17 | 18 | def normalize_identity(self): 19 | return self.identity.casefold() 20 | 21 | 22 | class ExternalGetCodeView(BaseGetCodeView): 23 | """ 24 | 使用该类预期 external provider 实现了以下接口: 25 | 26 | POST <提供的 url> 27 | HTTP 头部: 28 | Authorization: Bearer <提供的 key> 29 | 正文 JSON 参数: 30 | to: 接收验证码的地址 31 | subject: 邮件标题 32 | body: 邮件正文 33 | ip: 请求发送邮件的客户端 IP 地址 34 | 返回的 HTTP Code: 35 | 200: 响应正常,邮件可能发送成功或失败 36 | 其他: 视为邮件发送失败(可能是服务配置错误导致),response 的正文内容不会返回给用户 37 | 返回的 HTTP 响应正文 JSON 参数: 38 | success: 布尔值,表示邮件是否发送成功 39 | msg: 字符串,表示邮件发送失败(检查不通过,或出现网络问题等时的错误信息) 40 | 41 | 需要保证在合理的时间内完成发信,否则可能会触发超时。 42 | 43 | external provider 可能需要额外实现邮件地址检查(如果有更加复杂,无法被正则涵盖的检查逻辑), 44 | 以及限流(如果有更加复杂的限流逻辑)。 45 | """ 46 | provider: str 47 | 48 | def send(self, identity, code): 49 | use_smtp = settings.EXTERNAL_LOGINS[self.provider]['use_smtp'] 50 | url = settings.EXTERNAL_LOGINS[self.provider].get('url', None) 51 | key = settings.EXTERNAL_LOGINS[self.provider].get('key', None) 52 | 53 | if settings.DEBUG or use_smtp: 54 | EmailMessage( 55 | subject=f'Hackergame 登录校验码:{code}', 56 | body=f'{code}\n请使用该校验码登录 Hackergame\n', 57 | to=[identity], 58 | ).send() 59 | else: 60 | response = requests.post( 61 | url=url, 62 | headers={'Authorization': 'Bearer ' + key}, 63 | json={ 64 | 'to': identity, 65 | 'subject': f'Hackergame 登录校验码:{code}', 66 | 'body': f'{code}\n请使用该校验码登录 Hackergame\n', 67 | 'ip': self.request.META['REMOTE_ADDR'], 68 | }, 69 | timeout=15, 70 | ) 71 | status_code = response.status_code 72 | if status_code != 200: 73 | raise ExternalProviderError("校验码邮件因未知原因发送失败。") 74 | response_json = response.json() 75 | if not response_json['success']: 76 | raise ExternalProviderError("校验码邮件发送失败:" + response_json['msg']) 77 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/fdu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import UserRegexAndDomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '复旦大学'} 11 | provider = 'fdu' 12 | group = 'fdu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'fdu' 17 | duration = timedelta(hours=1) 18 | validate_identity = UserRegexAndDomainEmailValidator(['fudan.edu.cn', 'm.fudan.edu.cn'], r'^\d{11}$') 19 | 20 | 21 | urlpatterns = [ 22 | path('fdu/login/', LoginView.as_view()), 23 | path('fdu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/gdou.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '广东海洋大学'} 11 | provider = 'gdou' 12 | group = 'gdou' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'gdou' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator(['stu.gdou.edu.cn']) 19 | 20 | 21 | urlpatterns = [ 22 | path('gdou/login/', LoginView.as_view()), 23 | path('gdou/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/gdut.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '广东工业大学'} 11 | provider = 'gdut' 12 | group = 'gdut' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'gdut' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator(['mail2.gdut.edu.cn']) 19 | 20 | 21 | urlpatterns = [ 22 | path('gdut/login/', LoginView.as_view()), 23 | path('gdut/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/gzhu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '广州大学'} 11 | provider = 'gzhu' 12 | group = 'gzhu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'gzhu' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator(['e.gzhu.edu.cn']) 19 | 20 | 21 | urlpatterns = [ 22 | path('gzhu/login/', LoginView.as_view()), 23 | path('gzhu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/hit.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import RegexDomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '哈尔滨工业大学'} 11 | provider = 'hit' 12 | group = 'hit' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'hit' 17 | duration = timedelta(hours=1) 18 | validate_identity = RegexDomainEmailValidator(r'^(\w+\.)?hit(wh|sz|)\.edu\.cn$') 19 | 20 | 21 | urlpatterns = [ 22 | path('hit/login/', LoginView.as_view()), 23 | path('hit/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/jlu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '吉林大学'} 11 | provider = 'jlu' 12 | group = 'jlu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'jlu' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('mails.jlu.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('jlu/login/', LoginView.as_view()), 23 | path('jlu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/neu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '东北大学'} 11 | provider = 'neu' 12 | group = 'neu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'neu' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('stu.neu.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('neu/login/', LoginView.as_view()), 23 | path('neu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/nuaa.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '南京航空航天大学'} 11 | provider = 'nuaa' 12 | group = 'nuaa' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'nuaa' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('nuaa.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('nuaa/login/', LoginView.as_view()), 23 | path('nuaa/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/nudt.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import AllowlistEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '国防科技大学'} 11 | provider = 'nudt' 12 | group = 'nudt' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'nudt' 17 | duration = timedelta(hours=1) 18 | # validate_identity = AllowlistEmailValidator([]) 19 | 20 | 21 | urlpatterns = [ 22 | path('nudt/login/', LoginView.as_view()), 23 | path('nudt/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/nyist.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '南阳理工学院'} 11 | provider = 'nyist' 12 | group = 'nyist' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'nyist' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('nyist.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('nyist/login/', LoginView.as_view()), 23 | path('nyist/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/ouc.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '中国海洋大学'} 11 | provider = 'ouc' 12 | group = 'ouc' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'ouc' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('stu.ouc.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('ouc/login/', LoginView.as_view()), 23 | path('ouc/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/shu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import UserRegexAndDomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '上海大学'} 11 | provider = 'shu' 12 | group = 'shu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'shu' 17 | duration = timedelta(hours=1) 18 | validate_identity = UserRegexAndDomainEmailValidator('shu.edu.cn', r'^(?:[a-z]|_|-|\d){3,50}$') 19 | 20 | 21 | urlpatterns = [ 22 | path('shu/login/', LoginView.as_view()), 23 | path('shu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/sms.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | from django.core.validators import RegexValidator 5 | from django.shortcuts import redirect 6 | from django.urls import path 7 | 8 | import aliyunsdkcore.acs_exception.exceptions 9 | import aliyunsdkcore.client 10 | import aliyunsdkcore.request 11 | 12 | from .base import BaseLoginView, BaseGetCodeView 13 | 14 | client = aliyunsdkcore.client.AcsClient( 15 | settings.SMS_ACCESS_KEY_ID, 16 | settings.SMS_ACCESS_KEY_SECRET, 17 | 'default', 18 | ) 19 | 20 | 21 | class LoginView(BaseLoginView): 22 | template_name = 'login_sms.html' 23 | provider = 'sms' 24 | group = 'other' 25 | 26 | def post(self, request): 27 | if self.check_code(): 28 | self.login(tel=self.identity) 29 | return redirect('hub') 30 | 31 | 32 | class GetCodeView(BaseGetCodeView): 33 | provider = 'sms' 34 | validate_identity = RegexValidator(r'^1[0-9]{10}$', '手机号码格式错误') 35 | 36 | def send(self, identity, code): 37 | request = aliyunsdkcore.request.CommonRequest() 38 | request.set_accept_format('json') 39 | request.set_domain('dysmsapi.aliyuncs.com') 40 | request.set_method('POST') 41 | request.set_protocol_type('https') 42 | request.set_version('2017-05-25') 43 | request.set_action_name('SendSms') 44 | request.add_query_param('RegionId', 'default') 45 | request.add_query_param('PhoneNumbers', identity) 46 | request.add_query_param('SignName', 'Hackergame') 47 | request.add_query_param('TemplateCode', 'SMS_168560438') 48 | request.add_query_param('TemplateParam', json.dumps({'code': code})) 49 | response = json.loads(client.do_action_with_exception(request)) 50 | if response['Code'] != 'OK': 51 | raise ValueError(response['Code']) 52 | 53 | 54 | urlpatterns = [ 55 | path('sms/login/', LoginView.as_view()), 56 | path('sms/get_code/', GetCodeView.as_view()), 57 | ] 58 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/sustech.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | from django.urls import path 4 | 5 | from typing import Any, Optional 6 | 7 | from ..models import AccountLog 8 | from .cas import CASBaseLoginView 9 | 10 | 11 | class LoginView(CASBaseLoginView): 12 | provider = 'sustech' 13 | group = 'sustech' 14 | service: str 15 | ticket: str 16 | sno: str 17 | 18 | cas_name = 'CRA SSO / SUSTech CAS' 19 | cas_login_url = 'https://sso.cra.ac.cn/realms/cra-service-realm/protocol/cas/login' 20 | cas_service_validate_url = 'https://sso.cra.ac.cn/realms/cra-service-realm/protocol/cas/serviceValidate' 21 | 22 | def login_attrs(self) -> dict[str, Any]: 23 | return { 24 | "sno": self.identity, 25 | "email": self.email, 26 | "name": self.name, 27 | } 28 | 29 | def check_ticket(self) -> Optional[ElementTree.Element]: 30 | tree = super().check_ticket() 31 | if not tree: 32 | return None 33 | self.identity = tree.find(self.YALE_CAS_URL + 'user').text.strip() 34 | self.email = tree.find(self.YALE_CAS_URL + 'attributes').find(self.YALE_CAS_URL + 'mail').text.strip() 35 | self.name = tree.find(self.YALE_CAS_URL + 'attributes').find(self.YALE_CAS_URL + 'cn').text.strip() 36 | return tree 37 | 38 | def on_get_account(self, account): 39 | def to_set(s): 40 | return set(s.split(',')) if s else set() 41 | def from_set(vs): 42 | return ','.join(sorted(vs)) 43 | custom_attrs: list[tuple[str, str]] = [ 44 | ('邮箱', self.email), 45 | ('姓名', self.name) 46 | ] 47 | for display_name, self_value in custom_attrs: 48 | try: 49 | o = AccountLog.objects.get(account=account, content_type=display_name) 50 | new_value = from_set(to_set(o.contents) | {self_value}) 51 | if new_value != o.contents: 52 | o.contents = new_value 53 | o.save() 54 | except AccountLog.DoesNotExist: 55 | AccountLog.objects.create(account=account, contents=f"{self_value}", content_type=display_name) 56 | return account 57 | 58 | 59 | urlpatterns = [ 60 | path('sustech/login/', LoginView.as_view()), 61 | ] 62 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/sysu.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '中山大学'} 11 | provider = 'sysu' 12 | group = 'sysu' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'sysu' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('mail2.sysu.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('sysu/login/', LoginView.as_view()), 23 | path('sysu/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/tongji.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import UserRegexAndDomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '同济大学'} 11 | provider = 'tongji' 12 | group = 'tongji' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'tongji' 17 | duration = timedelta(hours=1) 18 | validate_identity = UserRegexAndDomainEmailValidator('tongji.edu.cn', r'^\d{7}$') 19 | 20 | 21 | urlpatterns = [ 22 | path('tongji/login/', LoginView.as_view()), 23 | path('tongji/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/ustc.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | from django.urls import path 4 | 5 | from typing import Optional, Any 6 | 7 | from ..models import AccountLog 8 | from .cas import CASBaseLoginView 9 | 10 | 11 | class LoginView(CASBaseLoginView): 12 | provider = 'ustc' 13 | group = 'other' # XXX: 先加入 other,确认在读后移动至 ustc 14 | service: str 15 | ticket: str 16 | sno: str 17 | 18 | cas_name = '统一身份认证平台' 19 | cas_login_url = 'https://passport.ustc.edu.cn/login' 20 | cas_service_validate_url = 'https://passport.ustc.edu.cn/serviceValidate' 21 | 22 | def login_attrs(self) -> dict[str, Any]: 23 | return { 24 | "sno": self.sno, 25 | } 26 | 27 | def check_ticket(self) -> Optional[ElementTree.Element]: 28 | tree = super().check_ticket() 29 | if not tree: 30 | return None 31 | self.identity = tree.find('attributes').find(self.YALE_CAS_URL + 'gid').text.strip() 32 | self.sno = tree.find(self.YALE_CAS_URL + 'user').text.strip() 33 | return tree 34 | 35 | def on_get_account(self, account): 36 | def to_set(s): 37 | return set(s.split(',')) if s else set() 38 | def from_set(vs): 39 | return ','.join(sorted(vs)) 40 | try: 41 | o = AccountLog.objects.get(account=account, content_type='学号') 42 | new_snos = from_set(to_set(o.contents) | {self.sno}) 43 | if new_snos != o.contents: 44 | o.contents = new_snos 45 | o.save() 46 | except AccountLog.DoesNotExist: 47 | AccountLog.objects.create(account=account, contents=f"{self.sno}", content_type='学号') 48 | return account 49 | 50 | 51 | urlpatterns = [ 52 | path('ustc/login/', LoginView.as_view()), 53 | ] 54 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/xidian.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '西安电子科技大学'} 11 | provider = 'xidian' 12 | group = 'xidian' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'xidian' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator('stu.xidian.edu.cn') 19 | 20 | 21 | urlpatterns = [ 22 | path('xidian/login/', LoginView.as_view()), 23 | path('xidian/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/xmut.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.urls import path 4 | 5 | from .base import DomainEmailValidator 6 | from .external import ExternalLoginView, ExternalGetCodeView 7 | 8 | 9 | class LoginView(ExternalLoginView): 10 | template_context = {'provider_name': '厦门理工学院'} 11 | provider = 'xmut' 12 | group = 'xmut' 13 | 14 | 15 | class GetCodeView(ExternalGetCodeView): 16 | provider = 'xmut' 17 | duration = timedelta(hours=1) 18 | validate_identity = DomainEmailValidator(['s.xmut.edu.cn']) 19 | 20 | 21 | urlpatterns = [ 22 | path('xmut/login/', LoginView.as_view()), 23 | path('xmut/get_code/', GetCodeView.as_view()), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/auth_providers/zju.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | import json 4 | from typing import Any 5 | from Crypto.Cipher import AES 6 | from base64 import b64decode 7 | from urllib.parse import urlencode 8 | 9 | from django.conf import settings 10 | from django.contrib import messages 11 | from django.shortcuts import redirect 12 | from django.template.response import TemplateResponse 13 | from django.urls import path 14 | from django.utils.decorators import method_decorator 15 | from django.views.decorators.csrf import csrf_exempt 16 | 17 | from .base import BaseLoginView 18 | 19 | 20 | class LoginView(BaseLoginView): 21 | template_name = "login_info.html" 22 | template_context = { 23 | "provider_name": "浙江大学统一身份认证", 24 | "info": "此次登录需要您处在一个能够访问浙江大学内网的环境;如果您不在校内,请尝试使用 RVPN 等工具访问内网;确认能访问内网后,请单击登录按钮。", 25 | } 26 | provider = "zju" 27 | group = "zju" 28 | 29 | def __init__(self, **kwargs: Any) -> None: 30 | super().__init__(**kwargs) 31 | # 避免 local_settings 未配置时出错 32 | provider = settings.EXTERNAL_LOGINS.get(self.provider) 33 | if not provider: 34 | raise RuntimeError(f"未配置{self.template_context['provider_name']}登录") 35 | self.state_key = provider["state_key"].encode() 36 | self.cipher_key = b64decode(provider["cipher_key"]) 37 | self.provider_url = provider["provider_url"] 38 | 39 | # XXX: POST 请求到外部,不宜外传 csrf token,且反 CSRF 的功能已被 nonce 实现 40 | @method_decorator(csrf_exempt) 41 | def dispatch(self, *args, **kwargs): 42 | return super(LoginView, self).dispatch(*args, **kwargs) 43 | 44 | def normalize_identity(self): 45 | return self.identity.casefold() 46 | 47 | def check_cipher(self) -> bool: 48 | try: 49 | ciphertext = self.cipher 50 | nonce = self.request.session.get("auth_nonce_zju") 51 | assert isinstance(ciphertext, str) and isinstance(nonce, str) 52 | self.request.session.pop("auth_nonce_zju") 53 | # ciphertext: 54 | # 16 bytes: nonce 55 | # 16 bytes till end - 16 bytes: payload 56 | # 16 bytes from end: MAC 57 | ciphertext = b64decode(ciphertext) 58 | cipher = AES.new(self.cipher_key, AES.MODE_GCM, ciphertext[:16]) 59 | cipher.update(nonce.encode()) 60 | payload = json.loads( 61 | cipher.decrypt_and_verify(ciphertext[16:-16], ciphertext[-16:]) 62 | ) 63 | sno = payload["sno"] 64 | assert isinstance(sno, str) 65 | sno = sno.strip() 66 | # XXX: 实际上的长度是 8,但作为可信内容放宽限制来避免一些特殊情况 67 | if not ( 68 | all(char in string.digits for char in sno) 69 | and 5 <= len(sno) <= 26 70 | ): 71 | messages.error(self.request, "学号非法") 72 | return False 73 | self.sno = sno 74 | self.name = payload["name"] 75 | # XXX: 能够以此学号登录应当与拥有此邮箱等价 76 | self.identity = sno + "@zju.edu.cn" 77 | return True 78 | except Exception: 79 | messages.error(self.request, "登录失败") 80 | return False 81 | 82 | def get(self, request): 83 | self.cipher = request.GET.get("cipher") 84 | if self.cipher: 85 | if self.check_cipher(): 86 | self.login(email=self.identity, sno=self.sno, name=self.name) 87 | return redirect("hub") 88 | template_context = self.template_context.copy() 89 | nonce = "".join( 90 | secrets.choice(string.ascii_letters + string.digits) for _ in range(32) 91 | ) 92 | request.session['auth_nonce_zju'] = nonce 93 | 94 | # 在登录前显示提示信息(而非直接跳转) 95 | redirect_uri = request.build_absolute_uri("/accounts/zju/login/") 96 | template_context["url"] = ( 97 | self.provider_url 98 | + "?" 99 | + urlencode({"redirect_uri": redirect_uri, "nonce": nonce}) 100 | ) 101 | return TemplateResponse(request, self.template_name, template_context) 102 | 103 | 104 | urlpatterns = [ 105 | path("zju/login/", LoginView.as_view()), 106 | ] 107 | -------------------------------------------------------------------------------- /hackergame/frontend/context_processors.py: -------------------------------------------------------------------------------- 1 | from server.user.interface import User 2 | from server.context import Context 3 | 4 | from django.conf import settings 5 | 6 | from .models import Page 7 | 8 | 9 | def frontend(request): 10 | return { 11 | 'page': Page.get(), 12 | 'user_': ( 13 | User.get(Context.from_request(request), request.user.pk) 14 | if request.user.is_authenticated else None 15 | ), 16 | 'groups': User.groups, 17 | 'debug': settings.DEBUG, 18 | 'no_board_groups': User.no_board_groups, 19 | } 20 | -------------------------------------------------------------------------------- /hackergame/frontend/management/commands/regen_all.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from server.challenge.interface import Challenge 4 | from server.submission.interface import Submission 5 | from server.context import Context 6 | 7 | 8 | class Command(BaseCommand): 9 | help = '重算所有数据库缓存' 10 | 11 | def handle(self, *args, **options): 12 | context = Context(elevated=True) 13 | Challenge.regen_all(context) 14 | Submission.regen_all(context) 15 | -------------------------------------------------------------------------------- /hackergame/frontend/management/commands/setup.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.models import Site 3 | from django.core.management.base import BaseCommand 4 | 5 | from allauth.socialaccount.models import SocialApp 6 | 7 | 8 | class Command(BaseCommand): 9 | help = '用配置文件更新数据库' 10 | 11 | def handle(self, *args, **options): 12 | site = Site.objects.get_current() 13 | app = SocialApp.objects.create( 14 | provider='google', 15 | name='Google', 16 | client_id=settings.GOOGLE_APP_ID, 17 | secret=settings.GOOGLE_APP_SECRET, 18 | key='', 19 | ) 20 | app.sites.add(site) 21 | app = SocialApp.objects.create( 22 | provider='microsoft', 23 | name='Microsoft', 24 | client_id=settings.MICROSOFT_APP_ID, 25 | secret=settings.MICROSOFT_APP_SECRET, 26 | key='' 27 | ) 28 | app.sites.add(site) 29 | -------------------------------------------------------------------------------- /hackergame/frontend/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.utils.deprecation import MiddlewareMixin 3 | 4 | 5 | request_logger = logging.getLogger('django.request') 6 | 7 | 8 | class UserRequestMiddleware(MiddlewareMixin): 9 | def __init__(self, get_response=None): 10 | super().__init__(get_response) 11 | 12 | def process_response(self, request, response): 13 | request_logger.info(msg=f"{request.get_full_path()}", extra={"request": request}) 14 | return response 15 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-09-26 09:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Page', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.TextField(default='Hackergame')), 19 | ('description', models.TextField(blank=True)), 20 | ('keywords', models.TextField(blank=True, default='Hackergame,CTF')), 21 | ('content', models.TextField(blank=True, default='

Hackergame

', help_text='会被放入 div 的 HTML')), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0002_auto_20191010_0025.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-09 16:25 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('frontend', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Account', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('provider', models.TextField()), 21 | ('identity', models.TextField()), 22 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Code', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('provider', models.TextField()), 30 | ('identity', models.TextField()), 31 | ('code', models.TextField(db_index=True)), 32 | ('expiration', models.DateTimeField()), 33 | ], 34 | ), 35 | migrations.AlterUniqueTogether( 36 | name='account', 37 | unique_together={('provider', 'identity')}, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0003_page_js.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-14 14:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('frontend', '0002_auto_20191010_0025'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='page', 15 | name='js', 16 | field=models.TextField(blank=True, help_text='会被放入 script 的 JS'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0004_ustceligible_ustcsnos.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-11 13:51 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('auth', '0012_alter_user_first_name_max_length'), 11 | ('frontend', '0003_page_js'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='UstcEligible', 17 | fields=[ 18 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='auth.user')), 19 | ('eligible', models.BooleanField()), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='UstcSnos', 24 | fields=[ 25 | ('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='frontend.account')), 26 | ('snos', models.TextField()), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0005_qa.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-23 14:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('frontend', '0004_ustceligible_ustcsnos'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Qa', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('content', models.TextField(blank=True, default='

问与答

', help_text='会被放入 div 的 HTML')), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0006_credits.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-29 07:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('frontend', '0005_qa'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Credits', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('content', models.TextField(blank=True, help_text='会被放入 div 的 HTML')), 18 | ('js', models.TextField(blank=True, help_text='会被放入 script 的 JS')), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0007_accountlog_specialprofileusedrecord_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-12 17:46 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("auth", "0012_alter_user_first_name_max_length"), 11 | ("frontend", "0006_credits"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="AccountLog", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("contents", models.TextField()), 28 | ("content_type", models.CharField(default="学号", max_length=32)), 29 | ( 30 | "account", 31 | models.OneToOneField( 32 | on_delete=django.db.models.deletion.CASCADE, 33 | to="frontend.account", 34 | ), 35 | ), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name="SpecialProfileUsedRecord", 40 | fields=[ 41 | ( 42 | "user", 43 | models.OneToOneField( 44 | on_delete=django.db.models.deletion.CASCADE, 45 | primary_key=True, 46 | serialize=False, 47 | to=settings.AUTH_USER_MODEL, 48 | ), 49 | ), 50 | ], 51 | ), 52 | migrations.RemoveField( 53 | model_name="ustcsnos", 54 | name="account", 55 | ), 56 | migrations.DeleteModel( 57 | name="UstcEligible", 58 | ), 59 | migrations.DeleteModel( 60 | name="UstcSnos", 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0008_accountlog_unique_account_log_for_each_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-12 18:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("frontend", "0007_accountlog_specialprofileusedrecord_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddConstraint( 13 | model_name="accountlog", 14 | constraint=models.UniqueConstraint( 15 | fields=("account", "content_type"), 16 | name="unique_account_log_for_each_type", 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/0009_alter_accountlog_account.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-14 16:35 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("frontend", "0008_accountlog_unique_account_log_for_each_type"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="accountlog", 15 | name="account", 16 | field=models.ForeignKey( 17 | on_delete=django.db.models.deletion.CASCADE, to="frontend.account" 18 | ), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hackergame/frontend/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/frontend/models.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from random import randrange 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.db import models 6 | from django.utils.timezone import now 7 | 8 | 9 | class Page(models.Model): 10 | title = models.TextField(default='Hackergame') 11 | description = models.TextField(blank=True) 12 | keywords = models.TextField(blank=True, default='Hackergame,CTF') 13 | content = models.TextField(blank=True, default='

Hackergame

', 14 | help_text='会被放入 div 的 HTML') 15 | js = models.TextField(blank=True, 16 | help_text='会被放入 script 的 JS') 17 | 18 | def __str__(self): 19 | return self.title 20 | 21 | @classmethod 22 | def get(cls): 23 | return cls.objects.get_or_create()[0] 24 | 25 | 26 | class Account(models.Model): 27 | provider = models.TextField() 28 | identity = models.TextField() 29 | user = models.ForeignKey(get_user_model(), models.CASCADE, null=True) 30 | 31 | def __str__(self): 32 | return f'User {self.user.pk if self.user else "(null)"} ({self.provider}:{self.identity})' 33 | 34 | class Meta: 35 | unique_together = ('provider', 'identity') 36 | 37 | 38 | class Code(models.Model): 39 | provider = models.TextField() 40 | identity = models.TextField() 41 | code = models.TextField(db_index=True) 42 | expiration = models.DateTimeField() 43 | 44 | class TooMany(Exception): 45 | pass 46 | 47 | @classmethod 48 | def generate(cls, provider, identity, duration=timedelta(minutes=10), 49 | limit=3): 50 | if cls.objects.filter( 51 | provider=provider, 52 | identity=identity, 53 | expiration__gt=now(), 54 | ).count() >= limit: 55 | raise cls.TooMany 56 | return cls.objects.create( 57 | provider=provider, 58 | identity=identity, 59 | code=str(randrange(100000, 1000000)), 60 | expiration=now() + duration, 61 | ).code 62 | 63 | @classmethod 64 | def authenticate(cls, provider, identity, code): 65 | try: 66 | cls.objects.get( 67 | provider=provider, 68 | identity=identity, 69 | code=code, 70 | expiration__gt=now(), 71 | ).delete() 72 | return True 73 | except cls.DoesNotExist: 74 | return False 75 | 76 | 77 | # 记录特殊登录方式(例如 USTC CAS)的用户,其登录方式返回的「可靠」信息 78 | class AccountLog(models.Model): 79 | account = models.ForeignKey(Account, models.CASCADE, db_index=True) 80 | contents = models.TextField() 81 | content_type = models.CharField(max_length=32, default='学号') 82 | 83 | def __str__(self): 84 | return f"Account {self.account.pk} ({self.content_type} {self.contents})" 85 | 86 | class Meta: 87 | constraints = [ 88 | models.UniqueConstraint(fields=['account', 'content_type'], 89 | name='unique_account_log_for_each_type'), 90 | ] 91 | 92 | 93 | # 记录需要在首次登录后显示换组页面并且已经换组的用户 94 | # 目前只有 USTC 有这个需求 95 | class SpecialProfileUsedRecord(models.Model): 96 | user = models.OneToOneField(get_user_model(), models.CASCADE, primary_key=True) 97 | 98 | def __str__(self) -> str: 99 | return f"A used record of User id {self.user.pk}" 100 | 101 | 102 | class Qa(models.Model): 103 | content = models.TextField(blank=True, default='

问与答

', 104 | help_text='会被放入 div 的 HTML') 105 | 106 | @classmethod 107 | def get(cls): 108 | return cls.objects.get_or_create()[0] 109 | 110 | 111 | class Credits(models.Model): 112 | content = models.TextField(blank=True, 113 | help_text='会被放入 div 的 HTML') 114 | js = models.TextField(blank=True, 115 | help_text='会被放入 script 的 JS') 116 | 117 | @classmethod 118 | def get(cls): 119 | return cls.objects.get_or_create()[0] 120 | -------------------------------------------------------------------------------- /hackergame/frontend/static/chart.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function drawchart() { 4 | axios.all([ 5 | axios.all(app.objs.slice(0, 10).map(i => ( 6 | axios.post('/admin/submission/', { 7 | method: 'get_user_history', 8 | args: {user: i.user}, 9 | }) 10 | ))), 11 | axios.post('/admin/trigger/', {method: 'get_all'}), 12 | ]).then(([user_reqs, {data: {value: triggers}}]) => { 13 | let starttime = triggers.find(i => i.can_submit); 14 | if (!starttime || new Date(starttime.time) > new Date()) { 15 | document.getElementById('charttext').innerHTML = '比赛尚未开始'; 16 | return; 17 | } else { 18 | starttime = new Date(starttime.time); 19 | } 20 | let last_starttime = [...triggers].reverse().find(i => i.can_submit); 21 | let endtime = [...triggers].reverse().find(i => !i.can_submit); 22 | if (!endtime || new Date(endtime.time) > new Date() || new Date(endtime.time) < new Date(last_starttime.time)) { 23 | endtime = new Date(); 24 | } else { 25 | endtime = new Date(endtime.time); 26 | } 27 | let data = user_reqs.map(({data: {value: history}}, i) => { 28 | let points = history.map(i => ({x: new Date(i.time), y: i.score})); 29 | points.unshift({x: starttime, y: 0}); 30 | points.push({x: endtime, y: points[points.length-1].y}); 31 | let username = app.users[app.objs[i].user].display_name; 32 | let color = ["#C0392B", "#2ECC71", "#3498DB", "#F1C40F", "#8E44AD", "#797D7F", "#117864", "#E67E22", "#F1948A", "#1F618D"][i]; 33 | return { 34 | label: username, 35 | data: points, 36 | stepped: true, 37 | fill: false, 38 | backgroundColor: color, 39 | borderColor: color, 40 | borderWidth: 2, 41 | radius: 2, 42 | hoverRadius: 3, 43 | }; 44 | }); 45 | 46 | document.getElementById('charttext').innerHTML = ''; 47 | 48 | new Chart(document.getElementById('chart').getContext('2d'),{ 49 | type: 'line', 50 | data: { 51 | datasets: data, 52 | }, 53 | options: { 54 | hover: { 55 | mode: 'x', 56 | }, 57 | responsive: false, 58 | scales: { 59 | x: { 60 | type: 'time', 61 | ticks: { 62 | minRotation: 50, 63 | }, 64 | time: { 65 | unit: 'hour', 66 | displayFormats: { 67 | hour: "MM-DD HH:mm", 68 | }, 69 | tooltipFormat: "YYYY-MM-DD HH:mm:ss", 70 | }, 71 | }, 72 | }, 73 | }, 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /hackergame/frontend/static/chartjs-adapter-moment.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chartjs-adapter-moment v1.0.1 3 | * https://www.chartjs.org 4 | * (c) 2022 chartjs-adapter-moment Contributors 5 | * Released under the MIT license 6 | */ 7 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("moment"),require("chart.js")):"function"==typeof define&&define.amd?define(["moment","chart.js"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).moment,e.Chart)}(this,(function(e,t){"use strict";function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var f=n(e);const a={datetime:"MMM D, YYYY, h:mm:ss a",millisecond:"h:mm:ss.SSS a",second:"h:mm:ss a",minute:"h:mm a",hour:"hA",day:"MMM D",week:"ll",month:"MMM YYYY",quarter:"[Q]Q - YYYY",year:"YYYY"};t._adapters._date.override("function"==typeof f.default?{_id:"moment",formats:function(){return a},parse:function(e,t){return"string"==typeof e&&"string"==typeof t?e=f.default(e,t):e instanceof f.default||(e=f.default(e)),e.isValid()?e.valueOf():null},format:function(e,t){return f.default(e).format(t)},add:function(e,t,n){return f.default(e).add(t,n).valueOf()},diff:function(e,t,n){return f.default(e).diff(f.default(t),n)},startOf:function(e,t,n){return e=f.default(e),"isoWeek"===t?(n=Math.trunc(Math.min(Math.max(0,n),6)),e.isoWeekday(n).startOf("day").valueOf()):e.startOf(t).valueOf()},endOf:function(e,t){return f.default(e).endOf(t).valueOf()}}:{})})); 8 | -------------------------------------------------------------------------------- /hackergame/frontend/static/color.js: -------------------------------------------------------------------------------- 1 | /* 2 | Numbers come from: 3 | https://github.com/hsluv/hsluv-c/blob/498de4d9ce7a33933e9252fd3c87b75244215005/src/hsluv.c 4 | I validated them against those generated by 5 | https://github.com/colour-science/colour 6 | Only keep 17 significant digits, because if an IEEE 754 double-precision 7 | number is converted to a decimal string with at least 17 significant 8 | digits, and then converted back to double-precision representation, the 9 | final result must match the original number. 10 | 11 | Algorithms come from: 12 | https://peteroupc.github.io/colorgen.html 13 | http://brucelindbloom.com/ 14 | */ 15 | 16 | class Transform { 17 | constructor(...matrix) { 18 | return (...args) => ( 19 | matrix.map(i => ( 20 | i.map((j, k) => j * args[k]).reduce((a, b) => a+b, 0) 21 | )) 22 | ); 23 | } 24 | } 25 | 26 | class sRGB { 27 | constructor(R, G, B) { 28 | this.R = R; 29 | this.G = G; 30 | this.B = B; 31 | } 32 | static compand(v) { 33 | return (v <= 0.0031308) ? ( 34 | 12.92 * v 35 | ) : ( 36 | 1.055 * v ** (1 / 2.4) - 0.055 37 | ); 38 | } 39 | static inverse_compand(v) { 40 | return (v > 0.04045) ? ( 41 | ((v + 0.055) / 1.055) ** 2.4 42 | ) : ( 43 | v / 12.92 44 | ); 45 | } 46 | valid() { 47 | return Object.values(this).every(i => i >= 0 && i <= 1); 48 | } 49 | hex() { 50 | return '#' + Object.values(this).map(i => ( 51 | Math.round(i * 255).toString(16).padStart(2, '0') 52 | )).join(''); 53 | } 54 | CIEXYZ() { 55 | let t = new Transform( 56 | [.41239079926595948, .35758433938387796, .18048078840183429], 57 | [.21263900587151036, .71516867876775593, .072192315360733715], 58 | [.019330818715591851, .11919477979462599, .95053215224966058], 59 | ); 60 | return new CIEXYZ(...t(...Object.values(this).map(sRGB.inverse_compand))); 61 | } 62 | CIELUV() { 63 | return this.CIEXYZ().CIELUV(); 64 | } 65 | } 66 | 67 | class CIEXYZ { 68 | constructor(X, Y, Z) { 69 | this.X = X; 70 | this.Y = Y; 71 | this.Z = Z; 72 | } 73 | sRGB() { 74 | let t = new Transform( 75 | [3.2409699419045213, -1.5373831775700935, -.49861076029300328], 76 | [-.96924363628087983, 1.8759675015077207, .041555057407175612], 77 | [.055630079696993608, -.20397695888897656, 1.0569715142428786], 78 | ); 79 | return new sRGB(...t(...Object.values(this)).map(sRGB.compand)); 80 | } 81 | CIELUV() { 82 | let [u_ref, v_ref] = [.19783000664283681, .46831999493879100]; 83 | let [epsilon, kappa] = [216 / 24389, 24389 / 27]; 84 | let L = (this.Y <= epsilon) ? ( 85 | kappa * this.Y 86 | ) : ( 87 | 116 * this.Y ** (1 / 3) - 16 88 | ); 89 | if (L === 0) { 90 | return new CIELUV(0, 0, 0); 91 | } 92 | let divider = this.X + 15 * this.Y + 3 * this.Z; 93 | let u = 13 * L * (4 * this.X / divider - u_ref); 94 | let v = 13 * L * (9 * this.Y / divider - v_ref); 95 | return new CIELUV(L, u, v); 96 | } 97 | } 98 | 99 | class CIELUV { 100 | constructor(L, u, v) { 101 | this.L = L; 102 | this.u = u; 103 | this.v = v; 104 | } 105 | CIEXYZ() { 106 | let [u_ref, v_ref] = [.19783000664283681, .46831999493879100]; 107 | let [epsilon, kappa] = [216 / 24389, 24389 / 27]; 108 | if (this.L === 0) { 109 | return new CIEXYZ(0, 0, 0); 110 | } 111 | let Y = (this.L > kappa * epsilon) ? ( 112 | ((this.L + 16) / 116) ** 3 113 | ) : ( 114 | this.L / kappa 115 | ); 116 | let u_ = this.u + 13 * this.L * u_ref; 117 | let v_ = this.v + 13 * this.L * v_ref; 118 | let X = 2.25 * Y * u_ / v_; 119 | let Z = 39 * Y * this.L / v_ - X / 3 - 5 * Y; 120 | return new CIEXYZ(X, Y, Z); 121 | } 122 | sRGB() { 123 | return this.CIEXYZ().sRGB(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /hackergame/frontend/static/date.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Date.prototype.toYearString = function () { 4 | return this.getFullYear().toString(); 5 | }; 6 | Date.prototype.toMonthString = function () { 7 | return (this.getMonth() + 1).toString().padStart(2, '0'); 8 | }; 9 | Date.prototype.toDateString = function () { 10 | return this.getDate().toString().padStart(2, '0'); 11 | }; 12 | Date.prototype.toHourString = function () { 13 | return this.getHours().toString().padStart(2, '0'); 14 | }; 15 | Date.prototype.toMinuteString = function () { 16 | return this.getMinutes().toString().padStart(2, '0'); 17 | }; 18 | Date.prototype.toSecondString = function () { 19 | return this.getSeconds().toString().padStart(2, '0'); 20 | }; 21 | Date.prototype.toLocaleDateString = function () { 22 | return [ 23 | this.toYearString(), 24 | this.toMonthString(), 25 | this.toDateString(), 26 | ].join('-'); 27 | }; 28 | Date.prototype.toLocaleTimeString = function () { 29 | return [ 30 | this.toHourString(), 31 | this.toMinuteString(), 32 | this.toSecondString(), 33 | ].join(':'); 34 | }; 35 | Date.prototype.toLocaleString = function () { 36 | return [ 37 | this.toLocaleDateString(), 38 | this.toLocaleTimeString(), 39 | ].join(' '); 40 | }; 41 | -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/frontend/static/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /hackergame/frontend/static/katex/auto-render.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var i={};return function(){n.d(i,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,i=0,a=e.length;n0&&(i.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=a.test(d)?d:e.slice(t[l].left.length,n);i.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&i.push({type:"text",data:e}),i},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var i=document.createDocumentFragment(),a=0;a{% endblock %} 5 | 6 | {% block coltype %}colMS{% endblock %} 7 | 8 | {% block bodyclass %}{{ block.super }} dashboard{% endblock %} 9 | 10 | {% block breadcrumbs %}{% endblock %} 11 | 12 | {% block content %} 13 |
14 | 15 | {% if app_list %} 16 | {% for app in app_list %} 17 |
18 | 19 | 22 | {% for model in app.models %} 23 | 24 | {% if model.admin_url %} 25 | 26 | {% else %} 27 | 28 | {% endif %} 29 | 30 | {% if model.add_url %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | 36 | {% if model.admin_url %} 37 | {% if model.view_only %} 38 | 39 | {% else %} 40 | 41 | {% endif %} 42 | {% else %} 43 | 44 | {% endif %} 45 | 46 | {% endfor %} 47 |
20 | {{ app.name }} 21 |
{{ model.name }}管理 {{ app.name }}{% trans 'Add' %} {% trans 'View' %}{% trans 'Change' %} 
48 |
49 | {% endfor %} 50 | {% else %} 51 |

{% trans "You don't have permission to view or edit anything." %}

52 | {% endif %} 53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/admin_base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %} 5 | {{ block.super }} 6 | 7 | 8 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | {% endblock %} 30 | 31 | {% block breadcrumbs %} 32 | 36 | {% endblock %} 37 | 38 | {% block coltype %}flex{% endblock %} 39 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/announcements.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block js %} 5 | {{ block.super }} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |

公告

11 | {% verbatim %} 12 |
13 |
14 |
15 | {{ new Date(announcement.time).toLocaleString() }} 16 |
17 |
18 | {% endverbatim %} 19 | {{ announcements|json_script:'json-announcements' }} 20 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/credits.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block js %} 5 | {{ block.super }} 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
{{ credits.content|safe }}
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block css %} 4 | {{ block.super }} 5 | 12 | {% endblock %} 13 | 14 | {% block content %} 15 |

注册/登录

16 | {% if debug %} 17 |

调试登录

18 | {% endif %} 19 |

协办单位选手

20 | 40 |

其他选手

41 | 46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block js %} 5 | {{ block.super }} 6 | 7 | 8 | 12 | {% endblock %} 13 | 14 | {% block content %} 15 | {% block form %} 16 |

{% block heading %}登录{% endblock %}

17 |
18 | {% csrf_token %} 19 |
20 | {% block identity %} 21 | 22 | 23 | {% endblock %} 24 | {% verbatim %} 25 | 26 | {% endverbatim %} 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 | {% block info %}{% endblock %} 37 | {% endblock %} 38 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login_debug.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

调试登录

5 |
6 | {% csrf_token %} 7 | 12 | 13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login_email.html: -------------------------------------------------------------------------------- 1 | {% extends 'login_base.html' %} 2 | 3 | {% block heading %}{{ provider_name }}邮箱登录{% endblock %} 4 | 5 | {% block identity %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block info %} 11 |
12 |

收不到邮件怎么办?

13 |
    14 |
  • 部分合作高校邮件送达可能较慢,请耐心等待,并检查垃圾邮件;
  • 15 |
  • 校验码有效期为一小时,收到后再打开此页面,无须重新获取,直接填写即可注册;
  • 16 |
  • 如果还是收不到,请先作为“其他选手”注册,再联系组委会为你修改参赛组别。
  • 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login_info.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% comment %}包含自定义 info 的登录页模板{% endcomment %} 4 | 5 | {% block content %} 6 | {% block form %} 7 |

{% block heading %}{{ provider_name }}登录{% endblock %}

8 | 9 | {% block info %} 10 |
11 |

{{ info }}

12 |
13 | {% endblock %} 14 | {% endblock %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/login_sms.html: -------------------------------------------------------------------------------- 1 | {% extends 'login_base.html' %} 2 | 3 | {% block heading %}手机号码登录{% endblock %} 4 | 5 | {% block identity %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block info %} 11 |
12 |

收不到短信怎么办?

13 |
    14 |
  • 只支持中国大陆 11 位手机号码;
  • 15 |
  • 偶尔短信下发缓慢,请至少等待一分钟再重新尝试获取;
  • 16 |
  • 如果重新尝试也收不到,请联系组委会进行人工验证。
  • 17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/qa.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
{{ qa.content|safe }}
5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block js %} 5 | {{ block.super }} 6 | 7 | 8 | 12 | {% endblock %} 13 | 14 | {% block content %} 15 |

查询用户信息

16 | {% verbatim %} 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
加群验证码:{{ obj.code===undefined ? '你还没有权限查看' : (obj.code || '不是科大或合作高校') }}
25 |
Token: {{ obj.token_short }}
26 |
用户组: {{ groups[obj.group]||obj.group }}
27 |
昵称: {{ obj.nickname }}
28 |
姓名: {{ obj.name }}
29 |
学号: {{ obj.sno }}
30 |
电话: {{ obj.tel }}
31 |
邮箱: {{ obj.email }}
32 |
性别: {{ {'':'','female':'女','male':'男','other':'其他'}[obj.gender] }}
33 |
QQ: {{ obj.qq }}
34 |
个人主页/博客:{{ obj.website }}
35 |
学院: {{ obj.school }}
36 |
年级: {{ obj.grade }}
37 |
专业:{{ obj.major }}
38 |
校区:{{ obj.campus }}
39 |
了解比赛的渠道: {{ obj.aff }}
40 |
41 |
42 |
AccountLog 信息:
43 |
44 |
{{ log.content_type }}: {{ log.contents }}
45 |
46 |
没有记录
47 |
48 |
49 |
50 | {% endverbatim %} 51 | {{ groups|json_script:'json-groups' }} 52 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /hackergame/frontend/templates/ustcprofile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

选择参赛组

5 |
6 | 只有在读同学有资格参加中国科学技术大学校内排行榜和获得奖项,选择后不可更改! 7 |
8 |
9 | {% csrf_token %} 10 | 11 | 12 |
13 |

14 |
15 | {% csrf_token %} 16 | 17 | 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /hackergame/frontend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_test import * 2 | from .command_test import * 3 | -------------------------------------------------------------------------------- /hackergame/frontend/tests/command_test.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | from django.test import TestCase 3 | import os 4 | import shutil 5 | 6 | from server.challenge.interface import Challenge 7 | from server.context import Context 8 | from server.challenge.expr_flags import expr_flag 9 | 10 | 11 | FILE1 = """--- 12 | enabled: false 13 | name: 示例题目 1 14 | category: general 15 | url: files/example.txt 16 | prompt: flag{...} 17 | index: 0 18 | flags: 19 | - name: '' 20 | score: 0 21 | type: text 22 | flag: flag{example} 23 | --- 24 | 25 | 一段描述。 26 | """ 27 | 28 | FILE2 = """--- 29 | enabled: true 30 | name: 示例题目 2 31 | category: math 32 | url: files/example.txt 33 | prompt: flag{...} 34 | index: 1 35 | flags: 36 | - name: '1+1=2' 37 | score: 1 38 | type: expr 39 | flag: f"flag{{{1+1}=2, {token}}}" 40 | - name: '2+2=4' 41 | score: 2 42 | type: text 43 | flag: flag{2+2=4} 44 | --- 45 | 46 | 一段描述 2。 47 | """ 48 | 49 | FILE3 = """--- 50 | enabled: true 51 | name: 示例题目 3 52 | category: web 53 | url: http://example.com/?token={token} 54 | prompt: flag{...} 55 | index: 2 56 | check_url_clicked: true 57 | flags: 58 | - name: '' 59 | score: 0 60 | type: text 61 | flag: flag{example3} 62 | --- 63 | 64 | 一段描述 3。 65 | 66 | ```python 67 | print("Hello, world!") 68 | ``` 69 | """ 70 | 71 | 72 | FILES = (FILE1, FILE2, FILE3) 73 | 74 | 75 | class ImportDataTest(TestCase): 76 | def setUp(self) -> None: 77 | # dir: /dev/shm/hackergame-django-test 78 | self.DIR_NAME = "/dev/shm/hackergame-django-test" 79 | try: 80 | shutil.rmtree(self.DIR_NAME) 81 | except FileNotFoundError: 82 | pass 83 | os.mkdir(self.DIR_NAME) 84 | # create some example README.md files 85 | for i in range(3): 86 | os.mkdir(f"{self.DIR_NAME}/example{i}") 87 | with open(f"{self.DIR_NAME}/example{i}/README.md", "w") as f: 88 | f.write(FILES[i]) 89 | 90 | def tearDown(self) -> None: 91 | shutil.rmtree(self.DIR_NAME) 92 | 93 | def test_import(self): 94 | call_command("import_data", self.DIR_NAME) 95 | # See what's inside challenges 96 | context = Context(elevated=True) 97 | challenges = {i.name: i for i in Challenge.get_all(context)} 98 | keys = challenges.keys() 99 | self.assert_("示例题目 1" not in keys) 100 | self.assert_("示例题目 2" in keys) 101 | self.assert_("示例题目 3" in keys) 102 | c2 = challenges["示例题目 2"] 103 | c3 = challenges["示例题目 3"] 104 | self.assert_(c2.url_orig.endswith("/example.txt")) 105 | self.assertEqual(c3.url_orig, "http://example.com/?token={token}") 106 | self.assertEqual(c2.check_url_clicked, False) 107 | self.assertEqual(c3.check_url_clicked, True) 108 | self.assert_("codehilite" in c3.detail) 109 | flag1 = c2.flags[0] 110 | self.assertEqual(flag1["type"], "expr") 111 | self.assertEqual("flag{2=2, 1:14}", expr_flag(flag1["flag"], "1:14")) 112 | flag2 = c2.flags[1] 113 | self.assertEqual(flag2["type"], "text") 114 | self.assertEqual("flag{2+2=4}", flag2["flag"]) 115 | -------------------------------------------------------------------------------- /hackergame/frontend/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import path, include 5 | 6 | from . import views 7 | 8 | urlpatterns = [ 9 | path('', views.HubView.as_view(), name='hub'), 10 | path('announcements/', views.AnnouncementsView.as_view(), 11 | name='announcements'), 12 | path('board/', views.BoardView.as_view(), name='board'), 13 | path('first/', views.FirstView.as_view(), name='first'), 14 | path('login/', views.LoginView.as_view(), name='login'), 15 | path('logout/', views.LogoutView.as_view(), name='logout'), 16 | path('profile/', views.ProfileView.as_view(), name='profile'), 17 | path('terms/', views.TermsView.as_view(), name='terms'), 18 | path('user/', views.UserView.as_view()), 19 | path('qa/', views.QaView.as_view(), name='qa'), 20 | path('credits/', views.CreditsView.as_view(), name='credits'), 21 | path('account/', views.AccountView.as_view(), name='account'), 22 | path('error/', views.ErrorView.as_view()), 23 | path('data/core.json', views.CoreDataView.as_view(), name='coredata'), 24 | path('challenge//', views.ChallengeURLView.as_view(), name='challenge_url'), 25 | path('score/', views.ScoreView.as_view(), name='score'), 26 | 27 | path('profile/ustc/', views.UstcProfileView.as_view(), name='ustcprofile'), 28 | 29 | path('accounts/', include('frontend.auth_providers.debug')), 30 | path('accounts/', include('frontend.auth_providers.ustc')), 31 | path('accounts/', include('frontend.auth_providers.zju')), 32 | path('accounts/', include('frontend.auth_providers.jlu')), 33 | path('accounts/', include('frontend.auth_providers.nuaa')), 34 | path('accounts/', include('frontend.auth_providers.neu')), 35 | path('accounts/', include('frontend.auth_providers.sysu')), 36 | path('accounts/', include('frontend.auth_providers.xidian')), 37 | path('accounts/', include('frontend.auth_providers.hit')), 38 | path('accounts/', include('frontend.auth_providers.nudt')), 39 | path('accounts/', include('frontend.auth_providers.fdu')), 40 | path('accounts/', include('frontend.auth_providers.tongji')), 41 | path('accounts/', include('frontend.auth_providers.gdou')), 42 | path('accounts/', include('frontend.auth_providers.gdut')), 43 | path('accounts/', include('frontend.auth_providers.gzhu')), 44 | path('accounts/', include('frontend.auth_providers.sustech')), 45 | path('accounts/', include('frontend.auth_providers.xmut')), 46 | path('accounts/', include('frontend.auth_providers.shu')), 47 | path('accounts/', include('frontend.auth_providers.nyist')), 48 | path('accounts/', include('frontend.auth_providers.sms')), 49 | path('accounts/', include('allauth.socialaccount.providers.google.urls')), 50 | path('accounts/', include('allauth.socialaccount.providers.microsoft.urls')), 51 | 52 | path('admin/announcement/', views.AnnouncementAdminView.as_view()), 53 | path('admin/challenge/', views.ChallengeAdminView.as_view()), 54 | path('admin/submission/', views.SubmissionAdminView.as_view()), 55 | path('admin/terms/', views.TermsAdminView.as_view()), 56 | path('admin/trigger/', views.TriggerAdminView.as_view()), 57 | path('admin/user/', views.UserAdminView.as_view()), 58 | path('admin/', admin.site.urls), 59 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 60 | -------------------------------------------------------------------------------- /hackergame/frontend/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from django.conf import settings 5 | from django.core import mail 6 | from django.utils.timezone import now 7 | from django.utils.log import AdminEmailHandler 8 | 9 | 10 | class ThrottledAdminEmailHandler(AdminEmailHandler): 11 | def send_mail(self, subject, message, *args, **kwargs): 12 | try: 13 | d = settings.MEDIA_ROOT + '/admin_email_throttle' 14 | try: 15 | try: 16 | os.mkdir(settings.MEDIA_ROOT) 17 | except FileExistsError: 18 | pass 19 | os.mkdir(d) 20 | except FileExistsError: 21 | pass 22 | t = now().isoformat() 23 | prefix = t[:16] 24 | count = 0 25 | for i in os.listdir(d): 26 | if i.startswith(prefix): 27 | count += 1 28 | else: 29 | try: 30 | os.remove(d + '/' + i) 31 | except FileNotFoundError: 32 | pass 33 | if count < 5: 34 | with open(d + '/' + t, 'w'): 35 | pass 36 | mail.mail_admins(subject, message, *args, connection=self.connection(), **kwargs) 37 | except Exception as e: 38 | mail.mail_admins(f'{subject} - {type(e)}: {e}', message, *args, connection=self.connection(), **kwargs) 39 | 40 | 41 | class UserInfoFilter(logging.Filter): 42 | def filter(self, record): 43 | if hasattr(record, 'request'): 44 | try: 45 | record.userid = "user-" + str(record.request.user.id) 46 | except AttributeError: 47 | # 'WSGIRequest' object has no attribute 'user' 48 | record.userid = "user-unknown" 49 | record.ip = record.request.META.get('REMOTE_ADDR') 50 | return True 51 | -------------------------------------------------------------------------------- /hackergame/frontend/wsgi.py: -------------------------------------------------------------------------------- 1 | from django.core.wsgi import get_wsgi_application 2 | 3 | application = get_wsgi_application() 4 | -------------------------------------------------------------------------------- /hackergame/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from django.core.management import execute_from_command_line 4 | 5 | if __name__ == '__main__': 6 | execute_from_command_line(sys.argv) 7 | -------------------------------------------------------------------------------- /hackergame/requirements-manual.txt: -------------------------------------------------------------------------------- 1 | # Packages used directly 2 | # requirements.txt should be generated by `pip freeze > requirements.txt`. 3 | aliyun-python-sdk-core==2.13.36 4 | Django==4.2.7 5 | django-allauth==0.57.0 6 | gevent==23.9.1 7 | Markdown==3.4.4 8 | psycopg==3.1.12 9 | psycopg-binary==3.1.12 10 | psycopg-pool==3.1.8 11 | pycryptodome==3.19.0 12 | pygments==2.16.1 13 | pymemcache==4.0.0 14 | pyOpenSSL==23.2.0 15 | PyYAML==6.0.1 16 | requests==2.31.0 17 | uWSGI==2.0.22 18 | -------------------------------------------------------------------------------- /hackergame/requirements.txt: -------------------------------------------------------------------------------- 1 | aliyun-python-sdk-core==2.13.36 2 | asgiref==3.7.2 3 | certifi==2023.7.22 4 | cffi==1.16.0 5 | charset-normalizer==3.3.0 6 | cryptography==41.0.7 7 | defusedxml==0.7.1 8 | Django==4.2.7 9 | django-allauth==0.57.0 10 | gevent==23.9.1 11 | greenlet==3.0.0 12 | idna==3.4 13 | jmespath==0.10.0 14 | Markdown==3.4.4 15 | oauthlib==3.2.2 16 | psycopg==3.1.12 17 | psycopg-binary==3.1.12 18 | psycopg-pool==3.1.8 19 | pycparser==2.21 20 | pycryptodome==3.19.0 21 | Pygments==2.16.1 22 | PyJWT==2.8.0 23 | pymemcache==4.0.0 24 | pyOpenSSL==23.2.0 25 | python3-openid==3.2.0 26 | PyYAML==6.0.1 27 | requests==2.31.0 28 | requests-oauthlib==1.3.1 29 | sqlparse==0.4.4 30 | typing-extensions==4.8.0 31 | urllib3==2.0.7 32 | uWSGI==2.0.22 33 | zope.event==5.0 34 | zope.interface==6.1 35 | -------------------------------------------------------------------------------- /hackergame/server/README.md: -------------------------------------------------------------------------------- 1 | 这里存放所有“新式”Django app,它们有明确的接口(`interface.py`),其他代码只应与接口交互,不应触及底层实现(如 `models.py`)。 2 | 3 | 绝大多数 `classmethod` 接口的第一个参数都是 `context`,其中包含当前请求的上下文信息。 4 | -------------------------------------------------------------------------------- /hackergame/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/__init__.py -------------------------------------------------------------------------------- /hackergame/server/announcement/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/announcement/__init__.py -------------------------------------------------------------------------------- /hackergame/server/announcement/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AnnouncementConfig(AppConfig): 5 | name = 'server.announcement' 6 | -------------------------------------------------------------------------------- /hackergame/server/announcement/interface.py: -------------------------------------------------------------------------------- 1 | from server.user.interface import User, PermissionRequired 2 | from server.exceptions import NotFound 3 | from . import models 4 | 5 | 6 | class Announcement: 7 | json_fields = ('pk', 'content', 'time') 8 | update_fields = ('content',) 9 | subscribers = [] 10 | 11 | def __init__(self, context, obj: models.Announcement): 12 | self._context = context 13 | self._obj = obj 14 | 15 | @classmethod 16 | def create(cls, context, content): 17 | User.test_permission(context, 'announcement.full') 18 | obj = models.Announcement.objects.create(content=content, 19 | time=context.time) 20 | self = cls(context, obj) 21 | new = self._json_all 22 | for subscriber in self.subscribers: 23 | subscriber(None, new) 24 | return self 25 | 26 | @classmethod 27 | def get(cls, context, pk): 28 | try: 29 | return cls(context, models.Announcement.objects.get(pk=pk)) 30 | except models.Announcement.DoesNotExist: 31 | raise NotFound('公告不存在') 32 | 33 | @classmethod 34 | def get_all(cls, context): 35 | return [cls(context, obj) for obj in models.Announcement.objects.all()] 36 | 37 | @classmethod 38 | def get_latest(cls, context): 39 | obj = models.Announcement.objects.first() 40 | if obj is None: 41 | raise NotFound('公告不存在') 42 | return cls(context, obj) 43 | 44 | def delete(self): 45 | User.test_permission(self._context, 'announcement.full') 46 | old = self._json_all 47 | self._obj.delete() 48 | self._obj = None 49 | for subscriber in self.subscribers: 50 | subscriber(old, None) 51 | 52 | @property 53 | def json(self): 54 | result = {} 55 | for i in self.json_fields: 56 | try: 57 | result[i] = getattr(self, i) 58 | except PermissionRequired: 59 | pass 60 | return result 61 | 62 | @property 63 | def _json_all(self): 64 | return type(self)(self._context.copy(elevated=True), self._obj).json 65 | 66 | @property 67 | def pk(self): 68 | return self._obj.pk 69 | 70 | @property 71 | def content(self): 72 | return self._obj.content 73 | 74 | @property 75 | def time(self): 76 | return self._obj.time 77 | -------------------------------------------------------------------------------- /hackergame/server/announcement/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-05 12:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Announcement', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('content', models.TextField()), 19 | ('time', models.DateTimeField()), 20 | ], 21 | options={ 22 | 'ordering': ('-time',), 23 | 'permissions': [('full', '管理公告')], 24 | 'default_permissions': (), 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /hackergame/server/announcement/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/announcement/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/announcement/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Announcement(models.Model): 5 | content = models.TextField() 6 | time = models.DateTimeField() 7 | 8 | class Meta: 9 | default_permissions = () 10 | permissions = [ 11 | ('full', '管理公告'), 12 | ] 13 | ordering = ('-time',) 14 | -------------------------------------------------------------------------------- /hackergame/server/announcement/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..context import Context 4 | from .interface import Announcement as AnnouncementInterface 5 | from .models import Announcement as AnnouncementModel 6 | 7 | 8 | class CheckInterfaceFields(TestCase): 9 | def setUp(self) -> None: 10 | self.context = Context() 11 | self.announcement = AnnouncementInterface(self.context, AnnouncementModel()) 12 | return super().setUp() 13 | 14 | def test_fields(self): 15 | for i in self.announcement.json_fields: 16 | self.assertIn(i, dir(self.announcement)) 17 | for i in self.announcement.update_fields: 18 | self.assertIn(i, dir(self.announcement)) 19 | -------------------------------------------------------------------------------- /hackergame/server/challenge/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/challenge/__init__.py -------------------------------------------------------------------------------- /hackergame/server/challenge/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChallengeConfig(AppConfig): 5 | name = 'server.challenge' 6 | 7 | def ready(self): 8 | from .interface import Challenge 9 | Challenge.app_ready() 10 | -------------------------------------------------------------------------------- /hackergame/server/challenge/expr_flags.py: -------------------------------------------------------------------------------- 1 | import base64 as b64 2 | import hashlib 3 | 4 | functions = {'hashlib': hashlib} 5 | 6 | 7 | def base64(s): 8 | if isinstance(s, str): 9 | s = s.encode() 10 | return b64.b64encode(s).decode() 11 | 12 | 13 | functions['base64'] = base64 14 | 15 | for method in ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 16 | 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'): 17 | def f(s, method=method): 18 | if isinstance(s, str): 19 | s = s.encode() 20 | return getattr(hashlib, method)(s).hexdigest() 21 | 22 | 23 | functions[method] = f 24 | 25 | 26 | def expr_flag(expr, token): 27 | return eval(expr, functions, {'token': token}) 28 | -------------------------------------------------------------------------------- /hackergame/server/challenge/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-03 07:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Challenge', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.TextField(unique=True)), 20 | ('category', models.TextField()), 21 | ('enabled', models.BooleanField()), 22 | ('detail', models.TextField()), 23 | ('url', models.TextField(null=True)), 24 | ('prompt', models.TextField(null=True)), 25 | ('index', models.IntegerField(db_index=True)), 26 | ('flags', models.TextField()), 27 | ], 28 | options={ 29 | 'ordering': ['index'], 30 | 'permissions': [('full', '管理题目')], 31 | 'default_permissions': (), 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='Expr', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('flag_index', models.IntegerField()), 39 | ('expr', models.TextField(db_index=True)), 40 | ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='challenge.Challenge')), 41 | ], 42 | options={ 43 | 'default_permissions': (), 44 | }, 45 | ), 46 | migrations.CreateModel( 47 | name='ExprFlag', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('expr', models.TextField(db_index=True)), 51 | ('user', models.IntegerField(db_index=True)), 52 | ('flag', models.TextField(db_index=True)), 53 | ], 54 | options={ 55 | 'default_permissions': (), 56 | }, 57 | ), 58 | migrations.CreateModel( 59 | name='User', 60 | fields=[ 61 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 62 | ('user', models.IntegerField(unique=True)), 63 | ], 64 | options={ 65 | 'default_permissions': (), 66 | }, 67 | ), 68 | migrations.AlterUniqueTogether( 69 | name='exprflag', 70 | unique_together={('expr', 'user')}, 71 | ), 72 | migrations.AlterUniqueTogether( 73 | name='expr', 74 | unique_together={('challenge', 'flag_index')}, 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /hackergame/server/challenge/migrations/0002_auto_20191011_2116.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-11 13:16 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('challenge', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='challenge', 15 | options={'default_permissions': (), 'ordering': ['index'], 'permissions': [('full', '管理题目'), ('view', '查看题目')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/challenge/migrations/0003_rename_url_challenge_url_orig_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-10 08:18 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("challenge", "0002_auto_20191011_2116"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name="challenge", 15 | old_name="url", 16 | new_name="url_orig", 17 | ), 18 | migrations.AddField( 19 | model_name="challenge", 20 | name="check_url_clicked", 21 | field=models.BooleanField(default=False), 22 | ), 23 | migrations.CreateModel( 24 | name="ChallengeURLRecord", 25 | fields=[ 26 | ( 27 | "id", 28 | models.AutoField( 29 | auto_created=True, 30 | primary_key=True, 31 | serialize=False, 32 | verbose_name="ID", 33 | ), 34 | ), 35 | ("user", models.IntegerField(db_index=True)), 36 | ("date", models.DateTimeField(auto_now_add=True)), 37 | ( 38 | "challenge", 39 | models.ForeignKey( 40 | on_delete=django.db.models.deletion.CASCADE, 41 | to="challenge.challenge", 42 | ), 43 | ), 44 | ], 45 | options={ 46 | "default_permissions": (), 47 | "unique_together": {("challenge", "user")}, 48 | }, 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /hackergame/server/challenge/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/challenge/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/challenge/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Challenge(models.Model): 5 | name = models.TextField(unique=True) 6 | category = models.TextField() 7 | enabled = models.BooleanField() 8 | detail = models.TextField() 9 | url_orig = models.TextField(null=True) 10 | prompt = models.TextField(null=True) 11 | index = models.IntegerField(db_index=True) 12 | flags = models.TextField() 13 | check_url_clicked = models.BooleanField(default=False) 14 | 15 | class Meta: 16 | default_permissions = () 17 | permissions = [ 18 | ('full', '管理题目'), 19 | ('view', '查看题目'), 20 | ] 21 | ordering = ['index'] 22 | 23 | 24 | class User(models.Model): 25 | user = models.IntegerField(unique=True) 26 | 27 | class Meta: 28 | default_permissions = () 29 | 30 | 31 | class Expr(models.Model): 32 | challenge = models.ForeignKey(Challenge, models.CASCADE) 33 | flag_index = models.IntegerField() 34 | expr = models.TextField(db_index=True) 35 | 36 | class Meta: 37 | default_permissions = () 38 | unique_together = ('challenge', 'flag_index') 39 | 40 | 41 | class ExprFlag(models.Model): 42 | expr = models.TextField(db_index=True) 43 | user = models.IntegerField(db_index=True) 44 | flag = models.TextField(db_index=True) 45 | 46 | class Meta: 47 | default_permissions = () 48 | unique_together = ('expr', 'user') 49 | 50 | 51 | class ChallengeURLRecord(models.Model): 52 | challenge = models.ForeignKey(Challenge, models.CASCADE) 53 | user = models.IntegerField(db_index=True) 54 | date = models.DateTimeField(auto_now_add=True) 55 | 56 | class Meta: 57 | default_permissions = () 58 | unique_together = ('challenge', 'user') 59 | -------------------------------------------------------------------------------- /hackergame/server/challenge/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..context import Context 4 | from .interface import Challenge as ChallengeInterface 5 | from .models import Challenge as ChallengeModel 6 | from .expr_flags import expr_flag 7 | 8 | 9 | class CheckInterfaceFields(TestCase): 10 | def setUp(self) -> None: 11 | self.context = Context() 12 | self.challenge = ChallengeInterface(self.context, ChallengeModel()) 13 | return super().setUp() 14 | 15 | def test_fields(self): 16 | for i in self.challenge.json_fields: 17 | self.assertIn(i, dir(self.challenge)) 18 | for i in self.challenge.update_fields: 19 | self.assertIn(i, dir(self.challenge)) 20 | 21 | 22 | class TestExprFlags(TestCase): 23 | def setUp(self) -> None: 24 | # a nonsense token 25 | self.token = "2:BBBBBBu1qB2iBBjcpBBBwpBlBnlvryj508f3BeBs5gBaaBB5BiBBn+B4xBx/2+l+c1o81BqBcwBBBnBBpB5fBBewBBz20yw=" 26 | return super().setUp() 27 | 28 | def test_expr(self): 29 | res = expr_flag("'flag{' + md5('secret' + token)[:16] + '}'", self.token) 30 | self.assertEqual(res, "flag{6a513c83d63baea4}") 31 | res = expr_flag("'flag{' + str(int(md5('secret' + token), 16) % 10001) + '}'", self.token) 32 | self.assertEqual(res, "flag{9011}") 33 | -------------------------------------------------------------------------------- /hackergame/server/context.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AnonymousUser 2 | from django.utils.timezone import now 3 | 4 | 5 | class Context: 6 | def __init__(self, user=None, time=None, elevated=False): 7 | self.user = user or AnonymousUser() 8 | self.time = time or now() 9 | self.elevated = elevated 10 | self.permissions = {} 11 | 12 | @classmethod 13 | def from_request(cls, request): 14 | return cls(request.user) 15 | 16 | # Django's has_perm() is too expensive. 17 | def has_perm(self, permission): 18 | if permission not in self.permissions: 19 | self.permissions[permission] = self.user.has_perm(permission) 20 | return self.permissions[permission] 21 | 22 | def copy(self, **kwargs): 23 | d = {'user': self.user, 'time': self.time, 'elevated': self.elevated} 24 | d.update(kwargs) 25 | return type(self)(**d) 26 | -------------------------------------------------------------------------------- /hackergame/server/exceptions.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | code = 'error' 3 | message = '服务器错误' 4 | 5 | def __init__(self, message=None): 6 | if message is not None: 7 | self.message = message 8 | 9 | def __str__(self): 10 | return f'{self.code}: {self.message}' 11 | 12 | @property 13 | def json(self): 14 | return { 15 | 'code': self.code, 16 | 'message': self.message, 17 | } 18 | 19 | 20 | class WrongArguments(Error): 21 | code = 'wrong_arguments' 22 | message = '参数错误' 23 | 24 | 25 | class WrongFormat(Error): 26 | code = 'wrong_format' 27 | message = '格式错误' 28 | 29 | 30 | class NotFound(Error): 31 | code = 'not_found' 32 | message = '对象不存在' 33 | -------------------------------------------------------------------------------- /hackergame/server/submission/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/submission/__init__.py -------------------------------------------------------------------------------- /hackergame/server/submission/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SubmissionConfig(AppConfig): 5 | name = 'server.submission' 6 | 7 | def ready(self): 8 | from .interface import Submission 9 | Submission.app_ready() 10 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0002_auto_20191005_1331.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-05 05:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('submission', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterUniqueTogether( 14 | name='score', 15 | unique_together={('user', 'category')}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0003_submission_group.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-07 10:40 2 | 3 | from django.db import migrations, models 4 | 5 | from server.user.interface import User 6 | from server.context import Context 7 | 8 | 9 | def set_group(apps, schema_editor): 10 | Submission = apps.get_model('submission', 'Submission') 11 | db_alias = schema_editor.connection.alias 12 | for obj in Submission.objects.using(db_alias).all(): 13 | obj.group = User.get(Context(elevated=True), obj.user).group 14 | obj.save() 15 | 16 | 17 | def unset_group(apps, schema_editor): 18 | pass 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [ 24 | ('submission', '0002_auto_20191005_1331'), 25 | ] 26 | 27 | operations = [ 28 | migrations.AddField( 29 | model_name='submission', 30 | name='group', 31 | field=models.TextField(null=True), 32 | preserve_default=False, 33 | ), 34 | migrations.RunPython(set_group, unset_group), 35 | migrations.AlterField( 36 | model_name='submission', 37 | name='group', 38 | field=models.TextField(), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0004_auto_20191011_2116.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-11 13:16 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('submission', '0003_submission_group'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='submission', 15 | options={'default_permissions': (), 'permissions': [('full', '管理提交记录'), ('view', '查看提交记录')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0005_auto_20191012_2325.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-12 15:25 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('submission', '0004_auto_20191011_2116'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='flagviolation', 15 | old_name='flag', 16 | new_name='violation_flag', 17 | ), 18 | migrations.RenameField( 19 | model_name='flagviolation', 20 | old_name='user', 21 | new_name='violation_user', 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0006_auto_20191015_0202.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-14 18:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('submission', '0005_auto_20191012_2325'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='submission', 15 | name='challenge', 16 | field=models.IntegerField(db_index=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='submission', 20 | name='group', 21 | field=models.TextField(db_index=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='submission', 25 | name='user', 26 | field=models.IntegerField(db_index=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0007_auto_20201022_1721.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-22 09:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('submission', '0006_auto_20191015_0202'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='challengefirst', 15 | name='group', 16 | field=models.TextField(db_index=True, default='$all'), 17 | preserve_default=False, 18 | ), 19 | migrations.AlterField( 20 | model_name='flagfirst', 21 | name='group', 22 | field=models.TextField(db_index=True, default='$all'), 23 | preserve_default=False, 24 | ), 25 | migrations.AlterField( 26 | model_name='score', 27 | name='category', 28 | field=models.TextField(db_index=True, default='$all'), 29 | preserve_default=False, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/0008_remove_flagviolation_violation_flag_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-09-10 12:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("submission", "0007_auto_20201022_1721"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="flagviolation", 14 | name="violation_flag", 15 | ), 16 | migrations.AddField( 17 | model_name="flagviolation", 18 | name="reason", 19 | field=models.TextField(null=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /hackergame/server/submission/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/submission/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/submission/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Submission(models.Model): 5 | user = models.IntegerField(db_index=True) 6 | group = models.TextField(db_index=True) 7 | challenge = models.IntegerField(db_index=True) 8 | text = models.TextField() 9 | time = models.DateTimeField() 10 | 11 | class Meta: 12 | default_permissions = () 13 | permissions = [ 14 | ('full', '管理提交记录'), 15 | ('view', '查看提交记录'), 16 | ] 17 | 18 | 19 | class ChallengeClear(models.Model): 20 | user = models.IntegerField(db_index=True) 21 | group = models.TextField(db_index=True) 22 | challenge = models.IntegerField(db_index=True) 23 | time = models.DateTimeField(db_index=True) 24 | 25 | class Meta: 26 | default_permissions = () 27 | unique_together = ('user', 'challenge') 28 | 29 | 30 | class FlagClear(models.Model): 31 | submission = models.ForeignKey(Submission, models.CASCADE) 32 | user = models.IntegerField(db_index=True) 33 | group = models.TextField(db_index=True) 34 | challenge = models.IntegerField(db_index=True) 35 | flag = models.IntegerField() 36 | time = models.DateTimeField(db_index=True) 37 | 38 | class Meta: 39 | default_permissions = () 40 | unique_together = ('user', 'challenge', 'flag') 41 | 42 | 43 | class FlagViolation(models.Model): 44 | submission = models.ForeignKey(Submission, models.CASCADE) 45 | violation_user = models.IntegerField() 46 | reason = models.TextField(null=True) 47 | 48 | class Meta: 49 | default_permissions = () 50 | 51 | 52 | class ChallengeFirst(models.Model): 53 | challenge = models.IntegerField() 54 | group = models.TextField(db_index=True) 55 | user = models.IntegerField() 56 | time = models.DateTimeField() 57 | 58 | class Meta: 59 | default_permissions = () 60 | unique_together = ('challenge', 'group') 61 | 62 | 63 | class FlagFirst(models.Model): 64 | challenge = models.IntegerField() 65 | flag = models.IntegerField() 66 | group = models.TextField(db_index=True) 67 | user = models.IntegerField() 68 | time = models.DateTimeField() 69 | 70 | class Meta: 71 | default_permissions = () 72 | unique_together = ('challenge', 'flag', 'group') 73 | 74 | 75 | class Score(models.Model): 76 | user = models.IntegerField(db_index=True) 77 | group = models.TextField() 78 | category = models.TextField(db_index=True) 79 | score = models.IntegerField() 80 | time = models.DateTimeField() 81 | 82 | class Meta: 83 | default_permissions = () 84 | unique_together = ('user', 'category') 85 | -------------------------------------------------------------------------------- /hackergame/server/terms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/terms/__init__.py -------------------------------------------------------------------------------- /hackergame/server/terms/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TermsConfig(AppConfig): 5 | name = 'server.terms' 6 | -------------------------------------------------------------------------------- /hackergame/server/terms/interface.py: -------------------------------------------------------------------------------- 1 | from server.user.interface import User, PermissionRequired 2 | from server.exceptions import Error, WrongArguments, NotFound 3 | from . import models 4 | 5 | 6 | class TermsRequired(Error): 7 | code = 'terms_required' 8 | message = '请同意用户条款' 9 | 10 | 11 | class Terms: 12 | json_fields = ('pk', 'name', 'content', 'enabled') 13 | update_fields = ('name', 'content', 'enabled') 14 | subscribers = [] 15 | 16 | def __init__(self, context, obj: models.Terms): 17 | self._context = context 18 | self._obj = obj 19 | 20 | @classmethod 21 | def test_agreed_enabled(cls, context): 22 | if context.elevated: 23 | return 24 | queryset = ( 25 | models.Terms.objects 26 | .filter(enabled=True) 27 | .exclude(agreement__user=context.user.pk) 28 | ) 29 | if queryset.exists(): 30 | raise TermsRequired() 31 | 32 | @classmethod 33 | def get(cls, context, pk): 34 | try: 35 | return cls(context, models.Terms.objects.get(pk=pk)) 36 | except models.Terms.DoesNotExist: 37 | raise NotFound() 38 | 39 | @classmethod 40 | def get_all(cls, context): 41 | return [cls(context, obj) for obj in models.Terms.objects.all()] 42 | 43 | @classmethod 44 | def get_enabled(cls, context): 45 | return [cls(context, obj) for obj in ( 46 | models.Terms.objects 47 | .filter(enabled=True) 48 | )] 49 | 50 | @classmethod 51 | def create(cls, context, **kwargs): 52 | User.test_permission(context, 'terms.full') 53 | self = cls(context, models.Terms()) 54 | self._update(**kwargs) 55 | new = self._json_all 56 | for subscriber in self.subscribers: 57 | subscriber(None, new) 58 | return self 59 | 60 | def agree(self, user): 61 | if self._context.user.pk != user: 62 | User.test_permission(self._context) 63 | models.Agreement.objects.get_or_create(user=user, terms=self._obj) 64 | 65 | def update(self, **kwargs): 66 | User.test_permission(self._context, 'terms.full') 67 | old = self._json_all 68 | self._update(**kwargs) 69 | new = self._json_all 70 | for subscriber in self.subscribers: 71 | subscriber(old, new) 72 | 73 | def _update(self, **kwargs): 74 | for k, v in kwargs.items(): 75 | if k in {'name', 'content'}: 76 | v = v or None 77 | setattr(self._obj, k, v) 78 | elif k in {'enabled'}: 79 | setattr(self._obj, k, v) 80 | else: 81 | raise WrongArguments() 82 | self._obj.save() 83 | self._obj.refresh_from_db() 84 | 85 | def delete(self): 86 | User.test_permission(self._context, 'terms.full') 87 | old = self._json_all 88 | self._obj.delete() 89 | self._obj = None 90 | for subscriber in self.subscribers: 91 | subscriber(old, None) 92 | 93 | @property 94 | def json(self): 95 | result = {} 96 | for i in self.json_fields: 97 | try: 98 | result[i] = getattr(self, i) 99 | except PermissionRequired: 100 | pass 101 | return result 102 | 103 | @property 104 | def _json_all(self): 105 | return type(self)(self._context.copy(elevated=True), self._obj).json 106 | 107 | @property 108 | def pk(self): 109 | return self._obj.pk 110 | 111 | @property 112 | def name(self): 113 | return self._obj.name 114 | 115 | @property 116 | def content(self): 117 | return self._obj.content 118 | 119 | @property 120 | def enabled(self): 121 | return self._obj.enabled 122 | -------------------------------------------------------------------------------- /hackergame/server/terms/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-09-29 10:05 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Agreement', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('user', models.IntegerField(db_index=True)), 20 | ], 21 | options={ 22 | 'default_permissions': (), 23 | }, 24 | ), 25 | migrations.CreateModel( 26 | name='Terms', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('name', models.TextField()), 30 | ('content', models.TextField()), 31 | ('enabled', models.BooleanField()), 32 | ], 33 | options={ 34 | 'permissions': [('full', '管理用户条款')], 35 | 'default_permissions': (), 36 | }, 37 | ), 38 | migrations.AddField( 39 | model_name='agreement', 40 | name='terms', 41 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terms.Terms'), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /hackergame/server/terms/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/terms/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/terms/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Terms(models.Model): 5 | name = models.TextField() 6 | content = models.TextField() 7 | enabled = models.BooleanField() 8 | 9 | class Meta: 10 | default_permissions = () 11 | permissions = [ 12 | ('full', '管理用户条款'), 13 | ] 14 | 15 | 16 | class Agreement(models.Model): 17 | user = models.IntegerField(db_index=True) 18 | terms = models.ForeignKey(Terms, models.CASCADE) 19 | 20 | class Meta: 21 | default_permissions = () 22 | -------------------------------------------------------------------------------- /hackergame/server/terms/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..context import Context 4 | from .interface import Terms as TermsInterface 5 | from .models import Terms as TermsModel 6 | 7 | 8 | class CheckInterfaceFields(TestCase): 9 | def setUp(self) -> None: 10 | self.context = Context() 11 | self.terms = TermsInterface(self.context, TermsModel()) 12 | return super().setUp() 13 | 14 | def test_fields(self): 15 | for i in self.terms.json_fields: 16 | self.assertIn(i, dir(self.terms)) 17 | for i in self.terms.update_fields: 18 | self.assertIn(i, dir(self.terms)) 19 | -------------------------------------------------------------------------------- /hackergame/server/trigger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/trigger/__init__.py -------------------------------------------------------------------------------- /hackergame/server/trigger/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TriggerConfig(AppConfig): 5 | name = 'server.trigger' 6 | -------------------------------------------------------------------------------- /hackergame/server/trigger/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-09-30 13:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Trigger', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('time', models.DateTimeField()), 19 | ('state', models.BooleanField()), 20 | ('note', models.TextField(null=True)), 21 | ], 22 | options={ 23 | 'permissions': [('full', '管理定时器')], 24 | 'default_permissions': (), 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /hackergame/server/trigger/migrations/0002_auto_20201012_2213.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-12 14:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('trigger', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='trigger', 15 | old_name='state', 16 | new_name='can_view_challenges', 17 | ), 18 | migrations.AddField( 19 | model_name='trigger', 20 | name='can_submit', 21 | field=models.BooleanField(default=False), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='trigger', 26 | name='can_try', 27 | field=models.BooleanField(default=False), 28 | preserve_default=False, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /hackergame/server/trigger/migrations/0003_trigger_can_update_profile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-10-12 07:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('trigger', '0002_auto_20201012_2213'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='trigger', 15 | name='can_update_profile', 16 | field=models.BooleanField(default=True), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /hackergame/server/trigger/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/trigger/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/trigger/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Trigger(models.Model): 5 | time = models.DateTimeField() 6 | can_view_challenges = models.BooleanField() 7 | can_try = models.BooleanField() 8 | can_submit = models.BooleanField() 9 | can_update_profile = models.BooleanField() 10 | note = models.TextField(null=True) 11 | 12 | class Meta: 13 | default_permissions = () 14 | permissions = [ 15 | ('full', '管理定时器'), 16 | ] 17 | -------------------------------------------------------------------------------- /hackergame/server/trigger/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..context import Context 4 | from .interface import Trigger as TriggerInterface 5 | from .models import Trigger as TriggerModel 6 | 7 | 8 | class CheckInterfaceFields(TestCase): 9 | def setUp(self) -> None: 10 | self.context = Context() 11 | self.trigger = TriggerInterface(self.context, TriggerModel()) 12 | return super().setUp() 13 | 14 | def test_fields(self): 15 | for i in self.trigger.json_fields: 16 | self.assertIn(i, dir(self.trigger)) 17 | for i in self.trigger.update_fields: 18 | self.assertIn(i, dir(self.trigger)) 19 | -------------------------------------------------------------------------------- /hackergame/server/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/user/__init__.py -------------------------------------------------------------------------------- /hackergame/server/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | name = 'server.user' 6 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-04 09:32 2 | 3 | import random 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | def gen_hash(): 9 | return f'{random.randrange(10000):04d}' 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='User', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('user', models.IntegerField(unique=True)), 25 | ('hash', models.TextField(default=gen_hash)), 26 | ('group', models.TextField()), 27 | ('nickname', models.TextField(null=True)), 28 | ('name', models.TextField(null=True)), 29 | ('sno', models.TextField(null=True)), 30 | ('tel', models.TextField(null=True)), 31 | ('email', models.TextField(null=True)), 32 | ('token', models.TextField()), 33 | ], 34 | options={ 35 | 'permissions': [('full', '管理个人信息')], 36 | 'default_permissions': (), 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0002_auto_20191010_1458.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-10 06:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='user', 15 | name='hash', 16 | ), 17 | migrations.AddField( 18 | model_name='user', 19 | name='gender', 20 | field=models.TextField(null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0003_user_qq.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-10 12:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0002_auto_20191010_1458'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='qq', 16 | field=models.TextField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0004_auto_20191011_1700.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-11 09:00 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0003_user_qq'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nankai', '查看南开大学个人信息'), ('view_bupt', '查看北京邮电大学个人信息'), ('view_cqu', '查看重庆大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息'), ('view_neu', '查看东北大学个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0005_auto_20191011_1842.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.12 on 2019-10-11 10:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0004_auto_20191011_1700'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='grade', 16 | field=models.TextField(null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='user', 20 | name='school', 21 | field=models.TextField(null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0006_auto_20201019_2248.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-19 14:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0005_auto_20191011_1842'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息'), ('view_xjtu', '查看西安交通大学个人信息'), ('view_cqu', '查看重庆大学个人信息'), ('view_bupt', '查看北京邮电大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0007_user_aff.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-19 15:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0006_auto_20201019_2248'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='aff', 16 | field=models.TextField(null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0008_userlog.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-23 03:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0007_user_aff'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='UserLog', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('context_user', models.IntegerField(null=True)), 18 | ('context_time', models.DateTimeField()), 19 | ('context_elevated', models.BooleanField()), 20 | ('user', models.IntegerField()), 21 | ('group', models.TextField()), 22 | ('nickname', models.TextField(null=True)), 23 | ('name', models.TextField(null=True)), 24 | ('sno', models.TextField(null=True)), 25 | ('tel', models.TextField(null=True)), 26 | ('email', models.TextField(null=True)), 27 | ('gender', models.TextField(null=True)), 28 | ('qq', models.TextField(null=True)), 29 | ('school', models.TextField(null=True)), 30 | ('grade', models.TextField(null=True)), 31 | ('aff', models.TextField(null=True)), 32 | ('token', models.TextField()), 33 | ], 34 | options={ 35 | 'default_permissions': (), 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0009_auto_20211010_1930.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-10 11:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0008_userlog'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_sysu', '查看中山大学个人信息'), ('view_xidian', '查看西安电子科技大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息')]}, 16 | ), 17 | migrations.AddField( 18 | model_name='user', 19 | name='website', 20 | field=models.TextField(null=True), 21 | ), 22 | migrations.AddField( 23 | model_name='userlog', 24 | name='website', 25 | field=models.TextField(null=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0010_alter_user_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-10-24 03:57 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0009_auto_20211010_1930'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view', '查看个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_sysu', '查看中山大学个人信息'), ('view_xidian', '查看西安电子科技大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0011_add_major_and_campus.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-10-06 13:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0010_alter_user_options'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='campus', 16 | field=models.TextField(null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='user', 20 | name='major', 21 | field=models.TextField(null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='userlog', 25 | name='campus', 26 | field=models.TextField(null=True), 27 | ), 28 | migrations.AddField( 29 | model_name='userlog', 30 | name='major', 31 | field=models.TextField(null=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0012_add_groups.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-10-06 15:32 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0011_add_major_and_campus'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view', '查看个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_sysu', '查看中山大学个人信息'), ('view_xidian', '查看西安电子科技大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息'), ('view_nudt', '查看国防科技大学个人信息'), ('view_fdu', '查看复旦大学个人信息'), ('view_ouc', '查看中国海洋大学个人信息'), ('view_tongji', '查看同济大学个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0013_user_suspicious_user_suspicious_reason_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-09-07 18:43 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("user", "0012_add_groups"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="suspicious", 15 | field=models.BooleanField(default=False), 16 | ), 17 | migrations.AddField( 18 | model_name="user", 19 | name="suspicious_reason", 20 | field=models.TextField(null=True), 21 | ), 22 | migrations.AddField( 23 | model_name="userlog", 24 | name="suspicious", 25 | field=models.BooleanField(default=False), 26 | ), 27 | migrations.AddField( 28 | model_name="userlog", 29 | name="suspicious_reason", 30 | field=models.TextField(null=True), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0014_user_suspicious_ddl_userlog_suspicious_ddl.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-09-08 08:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("user", "0013_user_suspicious_user_suspicious_reason_and_more"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="user", 14 | name="suspicious_ddl", 15 | field=models.DateTimeField(null=True), 16 | ), 17 | migrations.AddField( 18 | model_name="userlog", 19 | name="suspicious_ddl", 20 | field=models.DateTimeField(null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0015_update_groups_2023.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-09 07:42 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0014_user_suspicious_ddl_userlog_suspicious_ddl'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view', '查看个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_sysu', '查看中山大学个人信息'), ('view_xidian', '查看西安电子科技大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息'), ('view_nudt', '查看国防科技大学个人信息'), ('view_fdu', '查看复旦大学个人信息'), ('view_tongji', '查看同济大学个人信息'), ('view_gdou', '查看广东海洋大学个人信息'), ('view_gdut', '查看广东工业大学个人信息'), ('view_gzhu', '查看广州大学个人信息'), ('view_sustech', '查看南方科技大学个人信息'), ('view_xmut', '查看厦门理工学院个人信息'), ('view_shu', '查看上海大学个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/0016_add_nyist.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.5 on 2023-10-14 13:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('user', '0015_update_groups_2023'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={'default_permissions': (), 'permissions': [('full', '管理个人信息'), ('view', '查看个人信息'), ('view_ustc', '查看中国科学技术大学个人信息'), ('view_zju', '查看浙江大学个人信息'), ('view_jlu', '查看吉林大学个人信息'), ('view_nuaa', '查看南京航空航天大学个人信息'), ('view_neu', '查看东北大学个人信息'), ('view_sysu', '查看中山大学个人信息'), ('view_xidian', '查看西安电子科技大学个人信息'), ('view_hit', '查看哈尔滨工业大学个人信息'), ('view_nudt', '查看国防科技大学个人信息'), ('view_fdu', '查看复旦大学个人信息'), ('view_tongji', '查看同济大学个人信息'), ('view_gdou', '查看广东海洋大学个人信息'), ('view_gdut', '查看广东工业大学个人信息'), ('view_gzhu', '查看广州大学个人信息'), ('view_sustech', '查看南方科技大学个人信息'), ('view_xmut', '查看厦门理工学院个人信息'), ('view_shu', '查看上海大学个人信息'), ('view_nyist', '查看南阳理工学院个人信息')]}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /hackergame/server/user/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taoky/container-slides/138a10417cf74a3860720be5267e69fcff41c9c9/hackergame/server/user/migrations/__init__.py -------------------------------------------------------------------------------- /hackergame/server/user/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class User(models.Model): 5 | user = models.IntegerField(unique=True) 6 | group = models.TextField() 7 | nickname = models.TextField(null=True) 8 | name = models.TextField(null=True) 9 | sno = models.TextField(null=True) 10 | tel = models.TextField(null=True) 11 | email = models.TextField(null=True) 12 | gender = models.TextField(null=True) 13 | qq = models.TextField(null=True) 14 | website = models.TextField(null=True) 15 | school = models.TextField(null=True) 16 | grade = models.TextField(null=True) 17 | major = models.TextField(null=True) 18 | campus = models.TextField(null=True) 19 | aff = models.TextField(null=True) 20 | token = models.TextField() 21 | suspicious = models.BooleanField(default=False) 22 | suspicious_reason = models.TextField(null=True) 23 | suspicious_ddl = models.DateTimeField(null=True) 24 | 25 | class Meta: 26 | default_permissions = () 27 | permissions = [ 28 | ('full', '管理个人信息'), 29 | ('view', '查看个人信息'), 30 | ('view_ustc', '查看中国科学技术大学个人信息'), 31 | ('view_zju', '查看浙江大学个人信息'), 32 | ('view_jlu', '查看吉林大学个人信息'), 33 | ('view_nuaa', '查看南京航空航天大学个人信息'), 34 | ('view_neu', '查看东北大学个人信息'), 35 | ('view_sysu', '查看中山大学个人信息'), 36 | ('view_xidian', '查看西安电子科技大学个人信息'), 37 | ('view_hit', '查看哈尔滨工业大学个人信息'), 38 | ('view_nudt', '查看国防科技大学个人信息'), 39 | ('view_fdu', '查看复旦大学个人信息'), 40 | ('view_tongji', '查看同济大学个人信息'), 41 | ('view_gdou', '查看广东海洋大学个人信息'), 42 | ('view_gdut', '查看广东工业大学个人信息'), 43 | ('view_gzhu', '查看广州大学个人信息'), 44 | ('view_sustech', '查看南方科技大学个人信息'), 45 | ('view_xmut', '查看厦门理工学院个人信息'), 46 | ('view_shu', '查看上海大学个人信息'), 47 | ('view_nyist', '查看南阳理工学院个人信息'), 48 | ] 49 | 50 | 51 | class UserLog(models.Model): 52 | context_user = models.IntegerField(null=True) 53 | context_time = models.DateTimeField() 54 | context_elevated = models.BooleanField() 55 | user = models.IntegerField() 56 | group = models.TextField() 57 | nickname = models.TextField(null=True) 58 | name = models.TextField(null=True) 59 | sno = models.TextField(null=True) 60 | tel = models.TextField(null=True) 61 | email = models.TextField(null=True) 62 | gender = models.TextField(null=True) 63 | qq = models.TextField(null=True) 64 | website = models.TextField(null=True) 65 | school = models.TextField(null=True) 66 | grade = models.TextField(null=True) 67 | major = models.TextField(null=True) 68 | campus = models.TextField(null=True) 69 | aff = models.TextField(null=True) 70 | token = models.TextField() 71 | suspicious = models.BooleanField(default=False) 72 | suspicious_reason = models.TextField(null=True) 73 | suspicious_ddl = models.DateTimeField(null=True) 74 | 75 | class Meta: 76 | default_permissions = () 77 | -------------------------------------------------------------------------------- /hackergame/server/user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..context import Context 4 | from .interface import User as UserInterface 5 | from .models import User as UserModel 6 | 7 | 8 | class CheckInterfaceFields(TestCase): 9 | def setUp(self) -> None: 10 | self.context = Context() 11 | self.user = UserInterface(self.context, UserModel()) 12 | return super().setUp() 13 | 14 | def test_fields(self): 15 | for i in self.user.json_fields: 16 | self.assertIn(i, dir(self.user)) 17 | for i in self.user.update_fields: 18 | self.assertIn(i, dir(self.user)) 19 | -------------------------------------------------------------------------------- /hmcl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ustclug/debian:bookworm 2 | 3 | # Install deps 4 | RUN apt update && apt install -y --no-install-recommends \ 5 | wget ca-certificates 6 | 7 | # Download latest hmcl 8 | RUN wget -O /hmcl.jar https://github.com/huanghongxun/HMCL/releases/download/release-3.5.5/HMCL-3.5.5.jar 9 | 10 | # Install openjdk-17 and x-related packages 11 | RUN apt install -y --no-install-recommends \ 12 | openjdk-17-jre \ 13 | libxext6 libxrender1 libxtst6 libxi6 \ 14 | adduser \ 15 | pipewire pipewire-alsa alsa-utils \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # Create user 19 | RUN adduser --disabled-password --gecos "" --uid 1000 hmcl 20 | USER 1000:1000 21 | WORKDIR /home/hmcl 22 | 23 | CMD ["java", "-jar", "/hmcl.jar"] 24 | # CMD ["tail", "-f", "/dev/null"] 25 | -------------------------------------------------------------------------------- /hmcl/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | desktop: 4 | build: . 5 | environment: 6 | - DISPLAY=$DISPLAY 7 | - XAUTHORITY=$XAUTHORITY 8 | - XDG_RUNTIME_DIR=/tmp 9 | volumes: 10 | - /tmp/.X11-unix:/tmp/.X11-unix 11 | - $XAUTHORITY:$XAUTHORITY 12 | - /run/user/1000/pipewire-0:/tmp/pipewire-0 13 | - $HOME/.local/share/hmcl:/home/hmcl/.local/share/hmcl 14 | - $HOME/Games/Minecraft/.minecraft:/home/hmcl/.minecraft 15 | - /dev/dri:/dev/dri 16 | #network_mode: "host" 17 | --------------------------------------------------------------------------------