Skip to content

ustclug/hackergame

Repository files navigation

Hackergame 比赛平台

开发环境部署

  1. 创建 venv:python3 -m venv .venv
  2. 进入 venv:. .venv/bin/activate
  3. 安装依赖:pip install --upgrade pippip install -r requirements.txt
  4. 密钥配置:cp conf/local_settings.py.example conf/local_settings.py,编辑 conf/local_settings.py,其中有两条命令,需要执行并把输出贴在相应位置。
  5. 设置环境变量:export DJANGO_SETTINGS_MODULE=conf.settings.dev
  6. 创建数据目录:mkdir var
  7. 数据库初始化:./manage.py migrate
  8. (可选)Google 与 Microsoft app secret 写入数据库:./manage.py setup
  9. 见下方“运行”一节。
  10. 退出 venv:deactivate

生产环境部署

生产环境中会额外用到:Nginx、uWSGI、PostgreSQL、Memcached、PgBouncer。以下流程在 Debian 12 测试过。

  1. 安装依赖:apt install python3-dev build-essential python3-venv nginx postgresql memcached pgbouncer
  2. (建议)本地连接 PostgreSQL 无需鉴权:修改 /etc/postgresql/15/main/pg_hba.conf,将 local all all peer 一行改为 local all all trust,然后执行 systemctl reload postgresql
  3. 创建数据库:su postgrespsqlcreate user hackergame; create database hackergame;, \c hackergame, grant create on schema public to hackergame;
  4. 克隆代码:cd /optgit clone https://github.com/ustclug/hackergame.git
  5. Media 目录:mkdir -p /var/opt/hackergame/mediachown www-data: /var/opt/hackergame/media
  6. 创建 venv:cd /opt/hackergamepython3 -m venv .venv
  7. 进入 venv:. .venv/bin/activate
  8. 安装依赖:pip install --upgrade pippip install -r requirements.txt
  9. 密钥配置:cp conf/local_settings.py.example conf/local_settings.py,编辑 conf/local_settings.py,其中有两条命令,需要执行并把输出贴在相应位置。
  10. 设置环境变量:export DJANGO_SETTINGS_MODULE=conf.settings.hackergame
  11. 数据库初始化:./manage.py migrate
  12. Static 目录初始化:./manage.py collectstatic
  13. Google 与 Microsoft app secret 写入数据库:./manage.py setup
  14. 退出 venv:deactivate
  15. uWSGI 相关配置文件:cp conf/systemd/[email protected] /etc/systemd/system/, cp conf/logrotate/uwsgi /etc/logrotate.d/, systemctl daemon-reload, systemctl enable --now [email protected]
  16. Nginx 配置文件:cp conf/nginx-sites/hackergame /etc/nginx/sites-available/hackergameln -s /etc/nginx/sites-available/hackergame /etc/nginx/sites-enabled/hackergamesystemctl reload nginx
  17. 其他配置文件:cp conf/pgbouncer.ini /etc/pgbouncer/, systemctl restart pgbouncer
  18. 配置反向代理的客户端 IP 透传:前置反向代理需使用 X-Real-IP 请求头传递客户端 IP,/etc/nginx/sites-enabled/hackergame 中需添加一行 set_real_ip_from <reverse-proxy-ip> 以信任来自 reverse-proxy-ip 的指示客户端 IP 的请求头,否则平台不能正确获取用户 IP。

另外我们提供 docker compose 样例,但是实际部署不使用该容器版本。

uWSGI 运行模型

uWSGI 支持以下三种方式:

  • prefork 模式,每个连接占用一个进程,进程数量由 workers 或 processes 参数控制;
    • workers 参数同时也控制了下面两者中进程的数量。
  • threaded 模式,每个连接占用一个线程,线程数量由 threaded 参数控制;
  • gevent 模式,每个连接占用一个 gevent 绿色线程,绿色线程数量由 gevent 参数控制。

相关参数由 conf/uwsgi.iniconf/uwsgi-apps/ 下对应的 ini 文件控制,由 systemd service 的参数选择使用哪个 ini 文件(例如,[email protected] 即对应 hackergame.ini)。

由于部分请求比较耗时(socket 相关的代码,例如 OAuth),prefork 在部分场景下无法提供足够的并发,因此 conf/uwsgi-apps 下默认为 gevent 模式。如果不希望使用 gevent,可将相关配置中 gevent 开头的配置注释,并且添加/调整其他对应的参数。

另外,如果需要使用 Debian 自带的 uWSGI 与 gevent plugin 等相关设施(包括 init 服务和 logrotate 配置,而非 pip 与本仓库的配置),需要取消注释 plugin 项。

数据库连接池

由于 gevent 模式不支持 Django 自带的连接池特性(CONN_MAX_AGE,会导致 Django 开启的数据库连接一直无法释放),这里部署时采用了 PgBouncer 作为外部的连接池(或者说是数据库连接的代理)。

运行情况检查

可以使用 uwsgitop 来查看 uWSGI 运行情况,相关信息对于非 gevent 的 uwsgi 模式来讲很有帮助。

  1. 安装 pip install uwsgitop
  2. 执行 uwsgitop /run/uwsgi/app/hackergame/stats.socket 查看。

运行

注:运行所有以 ./manage.py 开头的命令都需要先进入 venv 和设置环境变量。

在开发环境中,用 ./manage.py runserver 运行服务器。

为了方便测试,./manage.py fake_data 会用随机生成的数据填充数据库。在登录时选择“调试登录”,输入 root 可以登录这样创建的超级管理员账号,输入数字可以登录这样创建的某个用户账号。

在生产环境中,需要打开网站注册,然后看 Token 开头的数字,这是你的用户 ID。运行 ./manage.py shell 并执行以下语句来将你设为超级管理员:

uid = 1  # 你的用户 ID
from django.contrib.auth.models import User
u = User.objects.get(pk=uid)
u.is_staff = True
u.is_superuser = True
u.save()

./manage.py import_data 可以导入题目仓库。

在罕见情况下,排行榜计算可能因为缓存逻辑而出现错误,可以用 ./manage.py regen_all 来重新生成所有缓存。

代码结构说明

假设读者已经熟悉 Django app 中包含的常见内容,只列出其他需要说明的项目。

conf/                           各种配置文件
    local_settings.py           不应提交进 git 的密钥等信息
    local_settings.py.example   模板
    nginx-sites/                Nginx 配置文件
    settings/                   Django settings
    uwsgi-apps/                 uWSGI 配置文件
frontend/                       “前端”,和登录、HTTP、HTML 等打交道
    auth_providers/             allauth 库以外的登录方式
    adapters.py                 allauth 库登录时执行的逻辑
    utils.py                    这里写了一个每分钟最多发 5 封报错邮件的逻辑
server/                         “后端”,只处理业务和权限逻辑
    announcement/               公告
        interface.py            对外接口,只要不绕过它,业务和权限逻辑就有保证
        models.py               interface.py 内部数据,别人不应该读写
    challenge/                  题目和动态 flag
    submission/                 提交判定、成绩计算、排行榜
    terms/                      用户条款
    trigger/                    比赛时间节点
    user/                       用户、组别、个人信息
    context.py                  表示当前用户权限和时间,几乎所有操作都需要提供
    exceptions.py               异常基础设施

用户和权限相关

在 Django 原生的 auth 模块后台(/admin/auth/)可以管理(原生的)用户和组。这些原生用户的用户名会比较乱,不好找到某个用户,建议先确定用户 id,然后随便点开一个用户后,改 URL 中的数字。Django 原生的用户概念和 hackergame 的用户概念是两种不同的对象,但 id 相同。后者在这里管理 /admin/user/。

对于 Django 原生的用户和权限概念,很多是没什么意义的,代码中并没有用到,用到的有:

  • “工作人员状态”(is_staff)控制这个用户会不会在首页题目列表底部看到一个“管理”链接,点击可以跳转到后台。但不影响任何权限
  • “超级用户状态”(is_superuser)可以绕过一切权限检查
  • 可以随便自行创建用户组,来方便给多个用户授予相同的权限集合
  • 权限中,这些是比较常用的:
    • announcement | announcement | *
    • challenge | challenge | *
    • frontend | credits | *
    • frontend | page | *
    • frontend | qa | *
    • submission | submission | *
    • terms | terms | *
    • trigger | trigger | *
    • user | user | *

注意:这些权限仅仅是给用户做了一种标记,至于各种操作到底能不能成功,能看到什么结果,还取决于代码中写的条件。例如

queryset = models.Challenge.objects.all()
try:
User.test_permission(context, 'challenge.full', 'challenge.view')
except PermissionRequired:
User.test_authenticated(context)
Terms.test_agreed_enabled(context)
User.test_profile(context)
Trigger.test_can_view_challenges(context)
queryset = queryset.filter(enabled=True)
如果有管理题目或查看题目权限,可以用这个接口加载任何一道题的信息。但即使没有,只要用户已登录、已同意用户条款、已填好个人信息、当前比赛处于可以看题的状态(也就是比赛中或结束后)、这道题是 enabled,这些条件全部满足,也可以加载。

报错邮件

发生未捕获的异常时会给管理员发报错邮件,收件人列表是 settings.ADMINS。代码中有专门的设计来实现邮件限速,短时间内达到报错次数上限时会丢弃之后的报错。以下报错是已知常见并且不需要在意的:

Internal Server Error: /accounts/microsoft/login/callback/
NoReverseMatch at /accounts/microsoft/login/callback/
Reverse for 'socialaccount_signup' not found. 'socialaccount_signup' is not a valid view function or pattern name.

常见问题

问:怎么查看某个组别/某个分类排行榜?

答:/board//first/ 两个 URL 支持形如 ?group=ustc&category=web 的参数。

问:怎么管理用户权限?

答:需要先知道用户 ID,假设为 1,在 /admin/auth/user/1/change/ 可以管理权限。

问:怎么编辑首页?

答:需要“frontend | page | Can change page”权限,然后在 /admin/frontend/page/ 编辑唯一一条记录。

问:怎样才能在首页看到各种管理功能(例如所有排行榜,以及题目列表底部的“管理”按钮)?

答:相应用户需要被勾选“工作人员状态”,这个选项和权限无关,仅影响界面显示。

问:“不计分”、“待审核”和“已封禁”这三个组别有什么区别?

答:不计分组的分数不计入排行榜;待审核组可以正常参赛,但分数暂时不计入排行榜,在比赛期间及比赛结束后 24 小时内提交审核材料并通过后,分数会重新计入排行榜;已封禁组被禁止参赛,即不能看到题目,不能做题,不能打开首页。

问:加群验证码是什么?

答:高校组别的选手会在首页上看到 QQ 群号和加群验证码,加群验证码是两个数字,前面的数字是用户 ID。管理员审核加群时,应打开 /user/,输入用户 ID,即可查出正确的加群验证码。需要有查看用户信息权限才能查出此项信息。

问:怎么备份数据库?

答:pg_dump -U hackergame -f backup.sql

问:怎么恢复数据库?

答:psql -U hackergame -f backup.sql,注意只应该向刚创建的空白数据库中执行这个操作。

问:还有什么要备份的信息?

答:Media 目录,里面装着导入的题目中供选手下载的文件,在生产环境部署中位于 /var/opt/hackergame/media

问:普通用户为什么可以访问后台页面 /admin/user/

答:这只是一个 UI,和权限模型无关。用户能看到和修改的内容仍然受自己的权限限制。

问:为什么有的页面的 HTML 中包含了所有用户个人信息/所有题目信息?

答:每个用户能看到的内容仍然受自己的权限限制。所有用户的 ID、组别、昵称都是公开的,所有人都可以看到。如果你有特别的权限(例如查看题目 flag),你会看到更多信息,不要泄露你得到的 HTML 源代码。

问:一些特别的查询需求怎么实现?

答:只要是你有权限调用的接口,都可以自己发请求调用。打开任何一个载入了 axios.min.js 的页面,如 /user/,打开浏览器的 console,即可写类似这样的代码:

axios.post('/admin/user/', {method: 'get', args: {pk: 1}}).then(v => console.log(v));