├── .all-contributorsrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── go.yml │ ├── lsif.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cardinal.go ├── crowdin.yml ├── frontend └── fs.go ├── go.mod ├── go.sum ├── internal ├── asteroid │ ├── client.go │ ├── handler.go │ ├── hub.go │ ├── message.go │ └── model.go ├── auth │ ├── manager │ │ └── manager.go │ ├── middleware.go │ └── team │ │ └── team.go ├── bootstrap │ ├── bootstrap.go │ └── bridge.go ├── bulletin │ └── bulletin.go ├── clock │ ├── clock.go │ ├── clock_test.go │ ├── error.go │ └── process.go ├── cmd │ ├── cmd.go │ └── web.go ├── conf │ ├── conf.go │ ├── conf_test.go │ ├── static.go │ ├── test.go │ └── testdata │ │ └── custom.toml ├── container │ ├── container.go │ ├── docker.go │ └── image.go ├── context │ └── context.go ├── db │ ├── actions.go │ ├── actions_test.go │ ├── bulletins.go │ ├── bulletins_test.go │ ├── challenges.go │ ├── challenges_test.go │ ├── db.go │ ├── flag.go │ ├── flag_test.go │ ├── game_boxes.go │ ├── game_boxes_test.go │ ├── logs.go │ ├── logs_test.go │ ├── main_test.go │ ├── managers.go │ ├── managers_test.go │ ├── rank.go │ ├── rank_test.go │ ├── score.go │ ├── score_test.go │ ├── teams.go │ └── teams_test.go ├── dbold │ ├── model.go │ └── mysql.go ├── dbutil │ ├── clock.go │ └── testing.go ├── dynamic_config │ └── dynamic_config.go ├── form │ ├── bulletin.go │ ├── challenge.go │ ├── form.go │ ├── game_box.go │ ├── manager.go │ └── team.go ├── game │ ├── bridge.go │ ├── challenge.go │ ├── check.go │ ├── flag.go │ ├── gamebox.go │ ├── rank.go │ └── score.go ├── healthy │ ├── healthy.go │ └── panel.go ├── i18n │ └── i18n.go ├── install │ ├── install.go │ └── install_util.go ├── livelog │ ├── handler.go │ ├── livelog.go │ ├── stream.go │ └── subscriber.go ├── locales │ └── i18n.go ├── logger │ └── log.go ├── misc │ ├── sentry.go │ ├── version.go │ └── webhook │ │ └── webhook.go ├── rank │ └── cache.go ├── route │ ├── auth.go │ ├── bulletin.go │ ├── bulletin_test.go │ ├── challenge.go │ ├── challenge_test.go │ ├── flag.go │ ├── flag_test.go │ ├── game_box.go │ ├── game_box_test.go │ ├── general.go │ ├── manager.go │ ├── route.go │ ├── router_old.go │ ├── team.go │ ├── team_test.go │ ├── testing.go │ └── wrapper.go ├── store │ └── store.go ├── timer │ ├── bridge.go │ └── timer.go ├── upload │ └── file.go └── utils │ ├── const.go │ ├── utils.go │ └── utils_test.go ├── locales ├── embed.go ├── en-US.yml └── zh-CN.yml └── test ├── bulletin_test.go ├── cardinal_test.go ├── challenge_test.go ├── log_test.go ├── manager_test.go ├── team_test.go └── webhook_test.go /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "wuhan005", 10 | "name": "E99p1ant", 11 | "avatar_url": "https://avatars3.githubusercontent.com/u/12731778?v=4", 12 | "profile": "https://github.com/wuhan005", 13 | "contributions": [ 14 | "code", 15 | "design", 16 | "doc", 17 | "maintenance" 18 | ] 19 | }, 20 | { 21 | "login": "Moesang", 22 | "name": "Moesang", 23 | "avatar_url": "https://avatars2.githubusercontent.com/u/46858006?v=4", 24 | "profile": "https://github.com/Moesang", 25 | "contributions": [ 26 | "code", 27 | "doc", 28 | "maintenance" 29 | ] 30 | }, 31 | { 32 | "login": "michaelfyc", 33 | "name": "michaelfyc", 34 | "avatar_url": "https://avatars2.githubusercontent.com/u/45136049?v=4", 35 | "profile": "https://github.com/michaelfyc", 36 | "contributions": [ 37 | "translation" 38 | ] 39 | } 40 | ], 41 | "contributorsPerLine": 7, 42 | "projectName": "Cardinal", 43 | "projectOwner": "vidar-team", 44 | "repoType": "github", 45 | "repoHost": "https://github.com", 46 | "skipCi": true 47 | } 48 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wuhan005 2 | custom: [ 'https://sponsors.github.red/' ] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | **运行环境 Environment**: 14 | 15 | - 操作系统 / Operating System: 16 | 17 | **发生了什么?/ What happened?** 18 | 19 | **您的预期结果是什么?/ What did you expect to happen?** 20 | 21 | **如何复现您的问题?/ How to reproduce your issue?** 22 | 23 | **额外的补充信息(堆栈信息、错误日志等)/ Any associated stack traces or error logs** 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning - Action" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - '**.go' 8 | - 'go.mod' 9 | - '.github/workflows/codeql.yml' 10 | pull_request: 11 | paths: 12 | - '**.go' 13 | - 'go.mod' 14 | - '.github/workflows/codeql.yml' 15 | schedule: 16 | # ┌───────────── minute (0 - 59) 17 | # │ ┌───────────── hour (0 - 23) 18 | # │ │ ┌───────────── day of the month (1 - 31) 19 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 20 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 21 | # │ │ │ │ │ 22 | # │ │ │ │ │ 23 | # │ │ │ │ │ 24 | # * * * * * 25 | - cron: '30 1 * * 0' 26 | 27 | jobs: 28 | CodeQL-Build: 29 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v2 35 | 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@v1 39 | # Override language selection by uncommenting this and choosing your languages 40 | with: 41 | languages: go 42 | 43 | - name: Perform CodeQL Analysis 44 | uses: github/codeql-action/analyze@v1 -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: [ master ] 5 | paths: 6 | - '**.go' 7 | pull_request: 8 | paths: 9 | - '**.go' 10 | env: 11 | GOPROXY: "https://proxy.golang.org" 12 | 13 | jobs: 14 | lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run golangci-lint 20 | uses: golangci/golangci-lint-action@v2 21 | with: 22 | version: latest 23 | args: --timeout=30m 24 | 25 | test: 26 | name: Test 27 | runs-on: ubuntu-latest 28 | services: 29 | mysql: 30 | image: mysql 31 | env: 32 | MYSQL_ROOT_PASSWORD: cardinal_passw0rd 33 | MYSQL_DATABASE: old_test_db 34 | ports: 35 | - 3306:3306 36 | 37 | postgres: 38 | image: postgres:12 39 | env: 40 | POSTGRES_PASSWORD: postgres 41 | options: >- 42 | --health-cmd pg_isready 43 | --health-interval 10s 44 | --health-timeout 5s 45 | --health-retries 5 46 | ports: 47 | - 5432:5432 48 | steps: 49 | - name: Install Go 50 | uses: actions/setup-go@v2 51 | with: 52 | go-version: 1.16.x 53 | - name: Checkout code 54 | uses: actions/checkout@v2 55 | - name: Cache downloaded modules 56 | uses: actions/cache@v1 57 | with: 58 | path: ~/go/pkg/mod 59 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 60 | restore-keys: | 61 | ${{ runner.os }}-go- 62 | - name: Run tests 63 | # add `-race` to check data race 64 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 65 | env: 66 | DBPORT: 3306 67 | DBHOST: localhost 68 | DBUSER: root 69 | DBPASSWORD: cardinal_passw0rd 70 | DBNAME: old_test_db # For old database test 71 | 72 | PGPORT: 5432 73 | PGHOST: localhost 74 | PGUSER: postgres 75 | PGPASSWORD: postgres 76 | PGSSLMODE: disable 77 | 78 | CARDINAL_TEST: true 79 | - name: Upload coverage to Codecov 80 | uses: codecov/codecov-action@v1 81 | with: 82 | files: ./coverage.txt 83 | flags: unittests 84 | fail_ci_if_error: true 85 | -------------------------------------------------------------------------------- /.github/workflows/lsif.yml: -------------------------------------------------------------------------------- 1 | name: LSIF 2 | on: 3 | push: 4 | paths: 5 | - '**.go' 6 | - 'go.mod' 7 | - '.github/workflows/lsif.yml' 8 | env: 9 | GOPROXY: "https://proxy.golang.org" 10 | 11 | jobs: 12 | lsif-go: 13 | runs-on: ubuntu-latest 14 | container: sourcegraph/lsif-go:v1.3.1 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Generate LSIF data 19 | run: lsif-go 20 | - name: Upload LSIF data 21 | run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }} -repo github.com/${{ github.repository }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | name: Build 5 | jobs: 6 | release-linux-386: 7 | name: release linux/386 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: compile and release 12 | uses: Cardinal-Platform/release_action@v0.1.0 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 15 | GOARCH: "386" 16 | GOOS: linux 17 | release-linux-amd64: 18 | name: release linux/amd64 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: compile and release 23 | uses: Cardinal-Platform/release_action@v0.1.0 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 26 | GOARCH: amd64 27 | GOOS: linux 28 | release-linux-arm: 29 | name: release linux/arm 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@master 33 | - name: compile and release 34 | uses: Cardinal-Platform/release_action@v0.1.0 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 37 | GOARCH: arm 38 | GOOS: linux 39 | release-linux-arm64: 40 | name: release linux/arm64 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@master 44 | - name: compile and release 45 | uses: Cardinal-Platform/release_action@v0.1.0 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 48 | GOARCH: arm64 49 | GOOS: linux 50 | release-darwin-386: 51 | name: release darwin/386 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@master 55 | - name: compile and release 56 | uses: Cardinal-Platform/release_action@v0.1.0 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 59 | GOARCH: "386" 60 | GOOS: darwin 61 | release-darwin-amd64: 62 | name: release darwin/amd64 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@master 66 | - name: compile and release 67 | uses: Cardinal-Platform/release_action@v0.1.0 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 70 | GOARCH: amd64 71 | GOOS: darwin 72 | release-windows-386: 73 | name: release windows/386 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@master 77 | - name: compile and release 78 | uses: Cardinal-Platform/release_action@v0.1.0 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 81 | GOARCH: "386" 82 | GOOS: windows 83 | release-windows-amd64: 84 | name: release windows/amd64 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@master 88 | - name: compile and release 89 | uses: Cardinal-Platform/release_action@v0.1.0 90 | env: 91 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 92 | GOARCH: amd64 93 | GOOS: windows -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea 17 | 18 | uploads 19 | Cardinal.toml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.7.3+dev(`master`) 4 | 5 | ### Added 6 | 7 | ### Changed 8 | 9 | ### Removed 10 | 11 | ## 0.7.2(`master`) 12 | 13 | ### Added 14 | 15 | * 管理员后台可筛选 Flag。 16 | * Timer 增加返回当前服务器时间戳。 17 | * gofmt CI 检查代码。 18 | * README 加入 Sourcegraph 链接。 19 | * README 加入兔小巢链接。 20 | * 发送请求检查版本更新时,加入请求超时时间,适配在无外网环境。 21 | * 管理员前端加入靶机状态信息显示。 22 | * 数据库版本检测。 23 | 24 | ### Changed 25 | 26 | * 日志包更换为 `unknwon.dev/clog/v2`。 27 | * 折叠显示 README 图片。 28 | * 修改 GitHub 打赏链接。 29 | 30 | ### Removed 31 | 32 | * 去除消息队列功能。 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Cardinal Logo](https://img.cdn.n3ko.co/lsky/2020/02/16/e75b82afd0932.png)](https://cardinal.ink) 2 | 3 | [![Go](https://github.com/vidar-team/Cardinal/actions/workflows/go.yml/badge.svg)](https://github.com/vidar-team/Cardinal/actions/workflows/go.yml) 4 | [![Code Scanning - Action](https://github.com/vidar-team/Cardinal/actions/workflows/codeql.yml/badge.svg)](https://github.com/vidar-team/Cardinal/actions/workflows/codeql.yml) 5 | [![codecov](https://codecov.io/gh/vidar-team/Cardinal/branch/master/graph/badge.svg?token=FZ9WKD0YE4)](https://codecov.io/gh/vidar-team/Cardinal) 6 | [![GoReport](https://goreportcard.com/badge/github.com/vidar-team/Cardinal)](https://goreportcard.com/report/github.com/vidar-team/Cardinal) 7 | [![Crowdin](https://badges.crowdin.net/cardinal/localized.svg)](https://crowdin.com/project/cardinal) 8 | [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?logo=sourcegraph)](https://sourcegraph.com/github.com/vidar-team/Cardinal) 9 | [![QQ Group](https://img.shields.io/badge/QQ%E7%BE%A4-130818749-blue.svg?logo=Tencent%20QQ)](https://shang.qq.com/wpa/qunwpa?idkey=c6a35c5fbec05fdcd2d2605e08b4b5f8d6e5854471fefd8c03d370d14870b818) 10 | 11 | # [Cardinal](https://cardinal.ink) —— CTF AWD 线下赛平台 12 | 13 | ## 版权说明 14 | 15 | > [!IMPORTANT] 16 | > 17 | > 18 | > 本项目为凌武科技 [lwsec.cn](https://lwsec.cn/) 开源版竞赛平台。 19 | > 20 | > 商业版支持 CTF、AWD、理论答题赛、车联网安全赛、数据安全赛、工控安全赛、供应链安全赛等更多赛制与功能,商业合作请于官网联系我们。 21 | 22 | ## 介绍 23 | 24 | Cardinal 是由 Vidar-Team 开发的 AWD 比赛平台,使用 Go 编写。本程序可以作为 CTF 线下比赛平台,亦可用于团队内部 AWD 模拟练习。 25 | 26 | ![Cardinal Frontend](https://s1.ax1x.com/2020/05/28/tVPltI.png) 27 | 28 |
29 | 更多图片 30 | 31 | ![Cardinal Backend](https://s1.ax1x.com/2020/05/28/tVP1ht.png) 32 | 33 | ![Asteroid](https://s1.ax1x.com/2020/05/28/tVP6jU.png) 34 | (该 AWD 实时 3D 攻击大屏为项目 [Asteroid](https://github.com/wuhan005/Asteroid),已适配接入 Cardinal) 35 | 36 |
37 | 38 | ## 文档 39 | 40 | ### 官方文档 [cardinal.ink](https://cardinal.ink) 41 | 42 | > 请您在使用前认真阅读官方使用文档,谢谢 ♪(・ω・)ノ 43 | 44 | ### 教程 45 | 46 | [AWD平台搭建–Cardinal](https://cloud.tencent.com/developer/article/1744139) 47 | 48 | ## 功能介绍 49 | 50 | * 管理员创建题目、分配题目靶机、参赛队伍、生成 Flag、发布公告 51 | * 支持上传参赛队伍 Logo 52 | * 题目可设置状态开放、下线,队伍分数同步更新 53 | * 批量生成 Flag 并导出,方便 Check bot 54 | 55 | * 每轮结束后自动结算分数,并更新排行榜 56 | * 自动对分数计算正确性进行检查 57 | * 分数计算异常日志提醒 58 | * 自定义攻击、Checkdown 分数 59 | * 队伍平分靶机分数 60 | * 自动更新靶机 Flag 61 | * 触发 WebHook,接入第三方应用 62 | 63 | * 管理端首页数据总览查看 64 | * 管理员、系统重要操作日志记录 65 | * 系统运行状态查看 66 | 67 | * 选手查看自己的队伍信息,靶机信息,Token,总排行榜,公告 68 | * 总排行榜靶机状态实时更新 69 | 70 | * 前后端分离,前端开源可定制 71 | 72 | ## 安装 73 | 74 | **Cardinal 当前正在进行部分基础架构的重写。目前强烈建议您通过 Release 或 Docker 75 | 安装而非直接源码编译。若实在需要进行源码上的变更,请从 [eaea493d](https://github.com/vidar-team/Cardinal/commit/eaea493d847546786e8f2fe9e717ee11c79324b6) 76 | 处进行编写。** 77 | 78 | ### Release 安装 79 | 80 | [下载](https://github.com/vidar-team/Cardinal/releases)适用于您目标机器的架构程序,运行即可。 81 | 82 | ``` 83 | # 解压程序包 84 | tar -zxvf Cardinal_VERSION_OS_ARCH.tar.gz 85 | 86 | # 赋予执行权限 87 | chmod +x ./Cardinal 88 | 89 | # 运行 90 | ./Cardinal 91 | ``` 92 | 93 | ### 编译安装 94 | 95 | 克隆代码,编译后运行生成的二进制文件即可。 96 | 97 | ``` 98 | # 克隆代码 99 | git clone https://github.com/vidar-team/Cardinal.git 100 | 101 | # 编译 102 | go build -o Cardinal 103 | 104 | # 赋予执行权限 105 | chmod +x ./Cardinal 106 | 107 | # 运行 108 | ./Cardinal 109 | ``` 110 | 111 | ### Docker 部署 112 | 113 | 首先请从 [Docker 官网](https://docs.docker.com) 安装 `docker` 与 `docker-compose` 114 | 115 | 确保当前用户拥有 `docker` 及 `docker-compose` 权限,然后执行 116 | 117 | ```bash 118 | curl https://sh.cardinal.ink | bash 119 | ``` 120 | 121 | 初次使用应当在下载后配置 `docker-compose.yml` 内的各项参数 122 | 123 | ## 开始使用 124 | 125 | 默认端口: `19999` 126 | 127 | * 选手端 `http://localhost:19999/` 128 | * 后台管理 `http://localhost:19999/manager` 129 | 130 | ## 开源协议 131 | 132 | © Vidar-Team 133 | 134 | GNU Affero General Public License v3.0 135 | -------------------------------------------------------------------------------- /cardinal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/vidar-team/Cardinal/internal/bootstrap" 4 | 5 | func main() { 6 | bootstrap.LinkStart() 7 | } 8 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | pull_request_title: "i18n: sync from Crowdin" 2 | commit_message: "i18n: sync from Crowdin" 3 | append_commit_message: false 4 | 5 | files: 6 | - source: /locales/zh-CN.yml 7 | translation: /locales/%locale%.yml 8 | -------------------------------------------------------------------------------- /frontend/fs.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | challenger "github.com/vidar-team/Cardinal_frontend/dist" 8 | manager "github.com/vidar-team/Cardinal_manager_frontend/dist" 9 | ) 10 | 11 | type frontendFS struct { 12 | frontendFS http.FileSystem 13 | managerFS http.FileSystem 14 | } 15 | 16 | // FS is the filesystem of the frontend. 17 | func FS() *frontendFS { 18 | return &frontendFS{ 19 | frontendFS: challenger.New(), 20 | managerFS: manager.New(), 21 | } 22 | } 23 | 24 | // Open: open file. 25 | func (f *frontendFS) Open(name string) (http.File, error) { 26 | if strings.HasPrefix(name, "/manager") { 27 | return f.managerFS.Open(name) 28 | } 29 | return f.frontendFS.Open(name) 30 | } 31 | 32 | // Exists: check if the file exist. 33 | func (f *frontendFS) Exists(prefix string, filePath string) bool { 34 | if strings.HasPrefix(filePath, "/manager") { 35 | if _, err := f.managerFS.Open(filePath); err != nil { 36 | return false 37 | } 38 | return true 39 | } 40 | if _, err := f.frontendFS.Open(filePath); err != nil { 41 | return false 42 | } 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vidar-team/Cardinal 2 | 3 | go 1.16 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/Cardinal-Platform/testify v0.0.0-20210919113340-9072c9681cc6 8 | github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect 9 | github.com/andybalholm/cascadia v1.1.0 // indirect 10 | github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect 11 | github.com/containerd/containerd v1.4.8 // indirect 12 | github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec // indirect 13 | github.com/disintegration/imaging v1.6.2 // indirect 14 | github.com/docker/distribution v2.7.1+incompatible // indirect 15 | github.com/docker/docker v17.12.0-ce-rc1.0.20200728121027-0f41a77c6993+incompatible 16 | github.com/docker/go-connections v0.4.0 // indirect 17 | github.com/docker/go-units v0.4.0 // indirect 18 | github.com/dustin/go-humanize v1.0.0 19 | github.com/elazarl/goproxy v0.0.0-20200426045556-49ad98f6dac1 // indirect 20 | github.com/fatih/color v1.13.0 // indirect 21 | github.com/flamego/binding v0.0.0-20210826141511-a26e848cdaa3 22 | github.com/flamego/cors v0.0.0-20211007054804-b92c10841d19 // indirect 23 | github.com/flamego/flamego v0.0.0-20211002062834-f84595763ddb 24 | github.com/flamego/session v0.0.0-20210607182212-8d30fdff82f2 25 | github.com/flamego/validator v0.0.0-20210821065223-7cb80dd2ce7a 26 | github.com/getsentry/sentry-go v0.7.0 27 | github.com/gin-contrib/cors v1.3.0 28 | github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 29 | github.com/gin-gonic/gin v1.7.0 30 | github.com/go-playground/validator/v10 v10.9.0 // indirect 31 | github.com/go-sql-driver/mysql v1.6.0 32 | github.com/gogo/protobuf v1.3.1 // indirect 33 | github.com/golang/protobuf v1.4.3 // indirect 34 | github.com/google/go-cmp v0.5.1 // indirect 35 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect 36 | github.com/gorilla/mux v1.7.4 // indirect 37 | github.com/gorilla/sessions v1.2.0 // indirect 38 | github.com/gorilla/websocket v1.4.2 39 | github.com/gosimple/slug v1.9.0 // indirect 40 | github.com/jackc/pgconn v1.8.1 41 | github.com/jinzhu/gorm v1.9.12 42 | github.com/json-iterator/go v1.1.9 43 | github.com/lib/pq v1.8.0 // indirect 44 | github.com/manifoldco/promptui v0.8.0 45 | github.com/mattn/go-colorable v0.1.11 // indirect 46 | github.com/moby/term v0.0.0-20200611042045-63b9a826fb74 // indirect 47 | github.com/morikuni/aec v1.0.0 // indirect 48 | github.com/opencontainers/go-digest v1.0.0 // indirect 49 | github.com/opencontainers/image-spec v1.0.1 // indirect 50 | github.com/parnurzeal/gorequest v0.2.16 51 | github.com/patrickmn/go-cache v2.1.0+incompatible 52 | github.com/pelletier/go-toml v1.9.3 53 | github.com/pkg/errors v0.9.1 54 | github.com/qor/admin v0.0.0-20200229145930-e279f96c8c05 // indirect 55 | github.com/qor/assetfs v0.0.0-20170713023933-ff57fdc13a14 // indirect 56 | github.com/qor/cache v0.0.0-20171031031927-c9d48d1f13ba // indirect 57 | github.com/qor/i18n v0.0.0-20181014061908-f7206d223bcd 58 | github.com/qor/media v0.0.0-20191022071353-19cf289e17d4 // indirect 59 | github.com/qor/middlewares v0.0.0-20170822143614-781378b69454 // indirect 60 | github.com/qor/oss v0.0.0-20191031055114-aef9ba66bf76 // indirect 61 | github.com/qor/qor v0.0.0-20200224122013-457d2e3f50e1 // indirect 62 | github.com/qor/responder v0.0.0-20171031032654-b6def473574f // indirect 63 | github.com/qor/roles v0.0.0-20171127035124-d6375609fe3e // indirect 64 | github.com/qor/serializable_meta v0.0.0-20180510060738-5fd8542db417 // indirect 65 | github.com/qor/session v0.0.0-20170907035918-8206b0adab70 // indirect 66 | github.com/qor/validations v0.0.0-20171228122639-f364bca61b46 // indirect 67 | github.com/satori/go.uuid v1.2.0 68 | github.com/sirupsen/logrus v1.7.0 // indirect 69 | github.com/smartystreets/assertions v1.0.1 // indirect 70 | github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 71 | github.com/thanhpk/randstr v1.0.4 72 | github.com/theplant/cldr v0.0.0-20190423050709-9f76f7ce4ee8 // indirect 73 | github.com/theplant/htmltestingutils v0.0.0-20190423050759-0e06de7b6967 // indirect 74 | github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61 // indirect 75 | github.com/unknwon/com v1.0.1 76 | github.com/urfave/cli/v2 v2.3.0 77 | github.com/vidar-team/Cardinal_frontend v0.7.3 78 | github.com/vidar-team/Cardinal_manager_frontend v0.7.3 79 | github.com/yosssi/gohtml v0.0.0-20200424144038-a48de20dd9dd // indirect 80 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 81 | golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed // indirect 82 | golang.org/x/text v0.3.6 83 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 85 | google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect 86 | google.golang.org/grpc v1.33.1 // indirect 87 | google.golang.org/protobuf v1.25.0 // indirect 88 | gopkg.in/yaml.v2 v2.3.0 // indirect 89 | gorm.io/driver/mysql v1.1.2 90 | gorm.io/driver/postgres v1.1.0 91 | gorm.io/gorm v1.21.12 92 | moul.io/http2curl v1.0.0 // indirect 93 | unknwon.dev/clog/v2 v2.1.2 94 | ) 95 | -------------------------------------------------------------------------------- /internal/asteroid/client.go: -------------------------------------------------------------------------------- 1 | package asteroid 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | log "unknwon.dev/clog/v2" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | const ( 13 | // Time allowed to write a message to the peer. 14 | writeWait = 10 * time.Second 15 | 16 | // Time allowed to read the next pong message from the peer. 17 | pongWait = 60 * time.Second 18 | 19 | // Send pings to peer with this period. Must be less than pongWait. 20 | pingPeriod = (pongWait * 9) / 10 21 | ) 22 | 23 | // client is a middleman between the websocket connection and the hub. 24 | type client struct { 25 | hub *Hub 26 | 27 | // The websocket connection. 28 | conn *websocket.Conn 29 | 30 | // Buffered channel of outbound messages. 31 | send chan []byte 32 | } 33 | 34 | func (c *client) writePump() { 35 | ticker := time.NewTicker(pingPeriod) 36 | defer func() { 37 | ticker.Stop() 38 | _ = c.conn.Close() 39 | }() 40 | 41 | // Init message 42 | initData, _ := json.Marshal(&unityData{ 43 | Type: INIT, 44 | Data: refresh(), 45 | }) 46 | c.send <- initData 47 | 48 | for { 49 | select { 50 | case message, ok := <-c.send: 51 | _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 52 | if !ok { 53 | // The hub closed the channel. 54 | _ = c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 55 | return 56 | } 57 | 58 | w, err := c.conn.NextWriter(websocket.TextMessage) 59 | if err != nil { 60 | return 61 | } 62 | 63 | _, _ = w.Write(message) 64 | if err := w.Close(); err != nil { 65 | log.Error("Failed to close: %v", err) 66 | delete(hub.clients, c) 67 | close(c.send) 68 | return 69 | } 70 | case <-ticker.C: 71 | _ = c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 72 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 73 | delete(hub.clients, c) 74 | close(c.send) 75 | return 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/asteroid/handler.go: -------------------------------------------------------------------------------- 1 | package asteroid 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/vidar-team/Cardinal/internal/locales" 6 | "github.com/vidar-team/Cardinal/internal/utils" 7 | ) 8 | 9 | func GetAsteroidStatus(c *gin.Context) (int, interface{}) { 10 | return utils.MakeSuccessJSON(gin.H{ 11 | // TODO 12 | }) 13 | } 14 | 15 | func Attack(c *gin.Context) (int, interface{}) { 16 | var attackData struct { 17 | From int `binding:"required"` 18 | To int `binding:"required"` 19 | } 20 | if err := c.BindJSON(&attackData); err != nil { 21 | return utils.MakeErrJSON(400, 40038, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 22 | } 23 | sendAttack(attackData.From, attackData.To) 24 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 25 | } 26 | 27 | func Rank(c *gin.Context) (int, interface{}) { 28 | sendRank() 29 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 30 | } 31 | 32 | func Status(c *gin.Context) (int, interface{}) { 33 | var status struct { 34 | Id int `binding:"required"` 35 | Status string `binding:"required"` 36 | } 37 | if err := c.BindJSON(&status); err != nil { 38 | return utils.MakeErrJSON(400, 40038, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 39 | } 40 | if status.Status != "down" && status.Status != "attacked" { 41 | return utils.MakeErrJSON(400, 40039, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 42 | } 43 | sendStatus(status.Id, status.Status) 44 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 45 | } 46 | 47 | func Round(c *gin.Context) (int, interface{}) { 48 | var round struct { 49 | Round int `binding:"required"` 50 | } 51 | if err := c.BindJSON(&round); err != nil { 52 | return utils.MakeErrJSON(400, 40038, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 53 | } 54 | sendRound(round.Round) 55 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 56 | } 57 | 58 | func EasterEgg(c *gin.Context) (int, interface{}) { 59 | sendEasterEgg() 60 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 61 | } 62 | 63 | func Time(c *gin.Context) (int, interface{}) { 64 | var time struct { 65 | Time int `binding:"required"` 66 | } 67 | if err := c.BindJSON(&time); err != nil { 68 | return utils.MakeErrJSON(400, 40038, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 69 | } 70 | sendTime(time.Time) 71 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 72 | } 73 | 74 | func Clear(c *gin.Context) (int, interface{}) { 75 | var clear struct { 76 | Id int `binding:"required"` 77 | } 78 | if err := c.BindJSON(&clear); err != nil { 79 | return utils.MakeErrJSON(400, 40038, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 80 | } 81 | sendClear(clear.Id) 82 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 83 | } 84 | 85 | func ClearAll(c *gin.Context) (int, interface{}) { 86 | sendClearAll() 87 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 88 | } 89 | -------------------------------------------------------------------------------- /internal/asteroid/hub.go: -------------------------------------------------------------------------------- 1 | package asteroid 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/gorilla/websocket" 9 | log "unknwon.dev/clog/v2" 10 | ) 11 | 12 | // Hub contains all the connections and the action signal. 13 | type Hub struct { 14 | // Registered clients. 15 | clients map[*client]bool 16 | 17 | // Inbound messages from the clients. 18 | broadcast chan []byte 19 | 20 | // Register requests from the clients. 21 | register chan *client 22 | 23 | // Unregister requests from clients. 24 | unregister chan *client 25 | } 26 | 27 | func newHub() *Hub { 28 | return &Hub{ 29 | broadcast: make(chan []byte), 30 | register: make(chan *client), 31 | unregister: make(chan *client), 32 | clients: make(map[*client]bool), 33 | } 34 | } 35 | 36 | // ServeWebSocket handles websocket requests from the peer. 37 | func ServeWebSocket(c *gin.Context) { 38 | hub.serve(c) 39 | } 40 | 41 | func (h *Hub) run() { 42 | for { 43 | select { 44 | case client := <-h.register: 45 | h.clients[client] = true 46 | case client := <-h.unregister: 47 | if _, ok := h.clients[client]; ok { 48 | delete(h.clients, client) 49 | close(client.send) 50 | } 51 | case message := <-h.broadcast: 52 | for client := range h.clients { 53 | select { 54 | case client.send <- message: 55 | default: 56 | close(client.send) 57 | delete(h.clients, client) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | func (h *Hub) serve(c *gin.Context) { 65 | // Upgrade the request to websocket. 66 | var upgrader = websocket.Upgrader{ 67 | CheckOrigin: func(r *http.Request) bool { 68 | return true // Ignore origin checking. 69 | }, 70 | } 71 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 72 | if err != nil { 73 | log.Error("Failed to upgrade: %v", err) 74 | return 75 | } 76 | client := &client{hub: h, conn: conn, send: make(chan []byte, 256)} 77 | client.hub.register <- client 78 | 79 | // Allow collection of memory referenced by the caller by doing all work in 80 | // new goroutines. 81 | go client.writePump() 82 | } 83 | 84 | func (h *Hub) sendMessage(messageType string, data interface{}) { 85 | jsonData, _ := json.Marshal(&unityData{ 86 | Type: messageType, 87 | Data: data, 88 | }) 89 | h.broadcast <- jsonData 90 | } 91 | -------------------------------------------------------------------------------- /internal/asteroid/message.go: -------------------------------------------------------------------------------- 1 | package asteroid 2 | 3 | const ( 4 | INIT = "init" 5 | ATTACK = "attack" 6 | RANK = "rank" 7 | STATUS = "status" 8 | ROUND = "round" 9 | EGG = "easterEgg" 10 | TIME = "time" 11 | CLEAR = "clear" 12 | CLEAR_ALL = "clearAll" 13 | ) 14 | 15 | var hub *Hub 16 | var refresh func() Greet // Used to get title, team, score, time data. 17 | 18 | // Init is used to init the asteroid. A function will be given to get the team rank data. 19 | func Init(function func() Greet) { 20 | refresh = function 21 | hub = newHub() 22 | 23 | // Start to handle the request. 24 | go hub.run() 25 | } 26 | 27 | // NewRoundAction runs in the new round begin. 28 | // Refresh rank, clean all gameboxes' status, set round text, set time text. 29 | func NewRoundAction() { 30 | sendRank() 31 | sendClearAll() 32 | sendRound(refresh().Round) 33 | sendTime(refresh().Time) 34 | } 35 | 36 | // SendStatus sends the teams' status message. 37 | func SendStatus(team int, statusString string) { 38 | sendStatus(team, statusString) 39 | } 40 | 41 | // SendAttack sends an attack action message. 42 | func SendAttack(from int, to int) { 43 | sendAttack(from, to) 44 | } 45 | 46 | // sendAttack sends an attack action message. 47 | func sendAttack(from int, to int) { 48 | hub.sendMessage(ATTACK, attack{ 49 | From: from, 50 | To: to, 51 | }) 52 | } 53 | 54 | // sendRank sends the team rank list message. 55 | func sendRank() { 56 | hub.sendMessage(RANK, rank{Team: refresh().Team}) 57 | } 58 | 59 | // sendStatus sends the teams' status message. 60 | func sendStatus(team int, statusString string) { 61 | hub.sendMessage(STATUS, status{ 62 | Id: team, 63 | Status: statusString, 64 | }) 65 | } 66 | 67 | // sendRound sends now round. 68 | func sendRound(roundNumber int) { 69 | hub.sendMessage(ROUND, round{Round: roundNumber}) 70 | } 71 | 72 | // sendEasterEgg can send a meteorite!! 73 | func sendEasterEgg() { 74 | hub.sendMessage(EGG, nil) 75 | } 76 | 77 | // sendTime sends time message. 78 | func sendTime(time int) { 79 | hub.sendMessage(TIME, clock{Time: time}) 80 | } 81 | 82 | // sendClear removes the status of the team. 83 | func sendClear(team int) { 84 | hub.sendMessage(CLEAR, clearStatus{Id: team}) 85 | } 86 | 87 | // sendClearAll removes all the teams' status. 88 | func sendClearAll() { 89 | hub.sendMessage(CLEAR_ALL, nil) 90 | } 91 | -------------------------------------------------------------------------------- /internal/asteroid/model.go: -------------------------------------------------------------------------------- 1 | package asteroid 2 | 3 | // Greet will been sent when the client connect to the server firstly. 4 | type Greet struct { 5 | Title string 6 | Time int 7 | Round int 8 | Team []Team 9 | } 10 | 11 | type Team struct { 12 | Id int 13 | Name string 14 | Rank int 15 | Image string 16 | Score int 17 | } 18 | 19 | type unityData struct { 20 | Type string 21 | Data interface{} 22 | } 23 | 24 | type attack struct { 25 | From int 26 | To int 27 | } 28 | 29 | type rank struct { 30 | Team []Team 31 | } 32 | 33 | type status struct { 34 | Id int 35 | Status string 36 | } 37 | 38 | type round struct { 39 | Round int 40 | } 41 | 42 | type clock struct { 43 | Time int 44 | } 45 | 46 | type clearStatus struct { 47 | Id int 48 | } 49 | -------------------------------------------------------------------------------- /internal/auth/middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/vidar-team/Cardinal/internal/dbold" 6 | "github.com/vidar-team/Cardinal/internal/locales" 7 | "github.com/vidar-team/Cardinal/internal/utils" 8 | ) 9 | 10 | // TeamAuthRequired is the team permission check middleware. 11 | func TeamAuthRequired() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | token := c.GetHeader("Authorization") 14 | if token == "" { 15 | c.JSON(utils.MakeErrJSON(403, 40300, 16 | locales.I18n.T(c.GetString("lang"), "general.no_auth"), 17 | )) 18 | c.Abort() 19 | return 20 | } 21 | 22 | var tokenData dbold.Token 23 | dbold.MySQL.Where(&dbold.Token{Token: token}).Find(&tokenData) 24 | if tokenData.ID == 0 { 25 | c.JSON(utils.MakeErrJSON(401, 40100, 26 | locales.I18n.T(c.GetString("lang"), "general.no_auth"), 27 | )) 28 | c.Abort() 29 | return 30 | } 31 | 32 | c.Set("teamID", tokenData.TeamID) 33 | c.Next() 34 | } 35 | } 36 | 37 | // AdminAuthRequired is the admin permission check middleware. 38 | func AdminAuthRequired() gin.HandlerFunc { 39 | return func(c *gin.Context) { 40 | token := c.GetHeader("Authorization") 41 | if token == "" { 42 | c.JSON(utils.MakeErrJSON(403, 40302, 43 | locales.I18n.T(c.GetString("lang"), "general.no_auth"), 44 | )) 45 | c.Abort() 46 | return 47 | } 48 | 49 | var managerData dbold.Manager 50 | dbold.MySQL.Where(&dbold.Manager{Token: token}).Find(&managerData) 51 | if managerData.ID == 0 { 52 | c.JSON(utils.MakeErrJSON(401, 40101, 53 | locales.I18n.T(c.GetString("lang"), "general.no_auth"), 54 | )) 55 | c.Abort() 56 | return 57 | } 58 | 59 | c.Set("managerData", managerData) 60 | c.Set("isCheck", managerData.IsCheck) 61 | c.Next() 62 | } 63 | } 64 | 65 | // ManagerRequired make sure the account is the manager. 66 | func ManagerRequired() gin.HandlerFunc { 67 | return func(c *gin.Context) { 68 | if c.GetBool("isCheck") { 69 | c.JSON(utils.MakeErrJSON(401, 40102, 70 | locales.I18n.T(c.GetString("lang"), "manager.manager_required"), 71 | )) 72 | c.Abort() 73 | return 74 | } 75 | c.Next() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/bootstrap/bootstrap.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | log "unknwon.dev/clog/v2" 5 | 6 | "github.com/vidar-team/Cardinal/internal/asteroid" 7 | "github.com/vidar-team/Cardinal/internal/conf" 8 | "github.com/vidar-team/Cardinal/internal/dbold" 9 | "github.com/vidar-team/Cardinal/internal/dynamic_config" 10 | "github.com/vidar-team/Cardinal/internal/game" 11 | "github.com/vidar-team/Cardinal/internal/install" 12 | "github.com/vidar-team/Cardinal/internal/livelog" 13 | "github.com/vidar-team/Cardinal/internal/misc" 14 | "github.com/vidar-team/Cardinal/internal/misc/webhook" 15 | "github.com/vidar-team/Cardinal/internal/route" 16 | "github.com/vidar-team/Cardinal/internal/store" 17 | "github.com/vidar-team/Cardinal/internal/timer" 18 | ) 19 | 20 | func init() { 21 | // Init log 22 | _ = log.NewConsole(100) 23 | } 24 | 25 | // LinkStart starts the Cardinal. 26 | func LinkStart() { 27 | // Install 28 | install.Init() 29 | 30 | // Config 31 | if err := conf.Init("./conf/Cardinal.toml"); err != nil { 32 | log.Fatal("Failed to load configuration file: %v", err) 33 | } 34 | 35 | // Check version 36 | misc.CheckVersion() 37 | 38 | // Sentry 39 | misc.Sentry() 40 | 41 | // Init MySQL database. 42 | dbold.InitMySQL() 43 | 44 | // Check manager 45 | install.InitManager() 46 | 47 | // Refresh the dynamic config from the database. 48 | dynamic_config.Init() 49 | 50 | // Check if the database need update. 51 | misc.CheckDatabaseVersion() 52 | 53 | // Game timer. 54 | GameToTimerBridge() 55 | timer.Init() 56 | 57 | // Cache 58 | store.Init() 59 | webhook.RefreshWebHookStore() 60 | 61 | // Unity3D Asteroid 62 | asteroid.Init(game.AsteroidGreetData) 63 | 64 | // Live log 65 | livelog.Init() 66 | 67 | // Web router. 68 | router := route.Init() 69 | 70 | log.Fatal("Failed to start web server: %v", router.Run(conf.App.HTTPAddr)) 71 | } 72 | -------------------------------------------------------------------------------- /internal/bootstrap/bridge.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/vidar-team/Cardinal/internal/game" 5 | "github.com/vidar-team/Cardinal/internal/timer" 6 | ) 7 | 8 | func GameToTimerBridge() { 9 | timer.SetRankListTitle = game.SetRankListTitle 10 | timer.SetRankList = game.SetRankList 11 | timer.CleanGameBoxStatus = game.CleanGameBoxStatus 12 | timer.GetLatestScoreRound = game.GetLatestScoreRound 13 | timer.RefreshFlag = game.RefreshFlag 14 | timer.CalculateRoundScore = game.CalculateRoundScore 15 | } 16 | -------------------------------------------------------------------------------- /internal/bulletin/bulletin.go: -------------------------------------------------------------------------------- 1 | package bulletin 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/jinzhu/gorm" 8 | "github.com/vidar-team/Cardinal/internal/dbold" 9 | "github.com/vidar-team/Cardinal/internal/locales" 10 | "github.com/vidar-team/Cardinal/internal/utils" 11 | ) 12 | 13 | // GetAllBulletins returns all bulletins from the database. 14 | func GetAllBulletins(c *gin.Context) (int, interface{}) { 15 | var bulletins []dbold.Bulletin 16 | dbold.MySQL.Model(&dbold.Bulletin{}).Order("`id` DESC").Find(&bulletins) 17 | return utils.MakeSuccessJSON(bulletins) 18 | } 19 | 20 | // NewBulletin is post new bulletin handler for manager. 21 | func NewBulletin(c *gin.Context) (int, interface{}) { 22 | type InputForm struct { 23 | Title string `binding:"required"` 24 | Content string `binding:"required"` 25 | } 26 | var inputForm InputForm 27 | err := c.BindJSON(&inputForm) 28 | if err != nil { 29 | return utils.MakeErrJSON(400, 40031, 30 | locales.I18n.T(c.GetString("lang"), "general.error_payload"), 31 | ) 32 | } 33 | 34 | tx := dbold.MySQL.Begin() 35 | if tx.Create(&dbold.Bulletin{ 36 | Title: inputForm.Title, 37 | Content: inputForm.Content, 38 | }).RowsAffected != 1 { 39 | tx.Rollback() 40 | return utils.MakeErrJSON(500, 50019, 41 | locales.I18n.T(c.GetString("lang"), "bulletin.post_error"), 42 | ) 43 | } 44 | tx.Commit() 45 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "bulletin.post_success")) 46 | } 47 | 48 | // EditBulletin is edit new bulletin handler for manager. 49 | func EditBulletin(c *gin.Context) (int, interface{}) { 50 | type InputForm struct { 51 | ID uint `binding:"required"` 52 | Title string `binding:"required"` 53 | Content string `binding:"required"` 54 | } 55 | var inputForm InputForm 56 | err := c.BindJSON(&inputForm) 57 | if err != nil { 58 | return utils.MakeErrJSON(400, 40031, 59 | locales.I18n.T(c.GetString("lang"), "general.error_payload"), 60 | ) 61 | } 62 | 63 | var checkBulletin dbold.Bulletin 64 | dbold.MySQL.Where(&dbold.Bulletin{Model: gorm.Model{ID: inputForm.ID}}).Find(&checkBulletin) 65 | if checkBulletin.ID == 0 { 66 | return utils.MakeErrJSON(404, 40404, 67 | locales.I18n.T(c.GetString("lang"), "bulletin.not_found"), 68 | ) 69 | } 70 | 71 | newBulletin := &dbold.Bulletin{ 72 | Title: inputForm.Title, 73 | Content: inputForm.Content, 74 | } 75 | tx := dbold.MySQL.Begin() 76 | if tx.Model(&dbold.Bulletin{}).Where(&dbold.Bulletin{Model: gorm.Model{ID: inputForm.ID}}).Updates(&newBulletin).RowsAffected != 1 { 77 | tx.Rollback() 78 | return utils.MakeErrJSON(500, 50020, 79 | locales.I18n.T(c.GetString("lang"), "bulletin.put_error"), 80 | ) 81 | } 82 | tx.Commit() 83 | 84 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "bulletin.put_success")) 85 | } 86 | 87 | // DeleteBulletin is delete new bulletin handler for manager. 88 | func DeleteBulletin(c *gin.Context) (int, interface{}) { 89 | idStr, ok := c.GetQuery("id") 90 | if !ok { 91 | return utils.MakeErrJSON(400, 40032, 92 | locales.I18n.T(c.GetString("lang"), "general.error_query"), 93 | ) 94 | } 95 | id, err := strconv.Atoi(idStr) 96 | if err != nil { 97 | return utils.MakeErrJSON(400, 40032, 98 | locales.I18n.T(c.GetString("lang"), "general.must_be_number", gin.H{"key": "id"}), 99 | ) 100 | } 101 | 102 | var checkBulletin dbold.Bulletin 103 | dbold.MySQL.Where(&dbold.Bulletin{Model: gorm.Model{ID: uint(id)}}).Find(&checkBulletin) 104 | if checkBulletin.ID == 0 { 105 | return utils.MakeErrJSON(404, 40404, 106 | locales.I18n.T(c.GetString("lang"), "bulletin.not_found"), 107 | ) 108 | } 109 | 110 | tx := dbold.MySQL.Begin() 111 | if tx.Where("id = ?", id).Delete(&dbold.Bulletin{}).RowsAffected != 1 { 112 | tx.Rollback() 113 | return utils.MakeErrJSON(500, 50021, 114 | locales.I18n.T(c.GetString("lang"), "bulletin.delete_error"), 115 | ) 116 | } 117 | tx.Commit() 118 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "bulletin.delete_success")) 119 | } 120 | -------------------------------------------------------------------------------- /internal/clock/clock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package clock 6 | 7 | import ( 8 | "math" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/vidar-team/Cardinal/internal/conf" 14 | ) 15 | 16 | type Status int 17 | 18 | const ( 19 | StatusWait Status = iota 20 | StatusRunning 21 | StatusPause 22 | StatusEnd 23 | ) 24 | 25 | var T = new(Clock) 26 | 27 | type Clock struct { 28 | StartAt time.Time 29 | EndAt time.Time 30 | RoundDuration time.Duration 31 | RestTime [][]time.Time 32 | RunTime [][]time.Time 33 | TotalRound uint 34 | CurrentRound uint 35 | RoundRemainDuration time.Duration 36 | Status Status 37 | 38 | stopChan chan struct{} 39 | } 40 | 41 | func Init() error { 42 | if conf.Game.RoundDuration == 0 { 43 | return ErrZeroRoundDuration 44 | } 45 | 46 | T = &Clock{ 47 | StartAt: conf.Game.StartAt.In(time.Local), 48 | EndAt: conf.Game.EndAt.In(time.Local), 49 | RoundDuration: time.Duration(conf.Game.RoundDuration) * time.Minute, 50 | } 51 | 52 | restTime := make([][]time.Time, 0, len(conf.Game.PauseTime)) 53 | for _, t := range conf.Game.PauseTime { 54 | restTime = append(restTime, []time.Time{t.StartAt.In(time.Local), t.EndAt.In(time.Local)}) 55 | } 56 | T.RestTime = restTime 57 | 58 | // Check timer configuration. 59 | if err := T.checkConfig(); err != nil { 60 | return errors.Wrap(err, "check config") 61 | } 62 | 63 | T.RestTime = combineDuration(T.RestTime) 64 | 65 | // Set competition run time cycle. 66 | if len(T.RestTime) != 0 { 67 | // StartAt -> RestTime[0][Start] 68 | T.RunTime = append(T.RunTime, []time.Time{T.StartAt, T.RestTime[0][0]}) 69 | for i := 0; i < len(T.RestTime)-1; i++ { 70 | // Runtime = RestHeadEnd -> RestNextBegin 71 | T.RunTime = append(T.RunTime, []time.Time{T.RestTime[i][1], T.RestTime[i+1][0]}) 72 | } 73 | // RestTime[Last][End] -> EndAt 74 | T.RunTime = append(T.RunTime, []time.Time{T.RestTime[len(T.RestTime)-1][1], T.EndAt}) 75 | 76 | } else { 77 | T.RunTime = append(T.RunTime, []time.Time{T.StartAt, T.EndAt}) 78 | } 79 | 80 | // Calculate total round count. 81 | var totalTime time.Duration 82 | for _, duration := range T.RunTime { 83 | totalTime += duration[1].Sub(duration[0]) 84 | } 85 | T.TotalRound = uint(math.Ceil(totalTime.Minutes() / T.RoundDuration.Minutes())) 86 | 87 | return nil 88 | } 89 | 90 | // checkConfig checks the time configuration from the configuration file. 91 | // It checks the order of StartAt and EndAt, each RestTime. 92 | func (c *Clock) checkConfig() error { 93 | if c.StartAt.After(c.EndAt) { 94 | return ErrStartTimeOrder 95 | } 96 | 97 | // Check rest time. 98 | for key, duration := range c.RestTime { 99 | if len(duration) != 2 { 100 | return ErrRestTimeFormat 101 | } 102 | 103 | start := duration[0] 104 | end := duration[1] 105 | 106 | if start.After(end) { 107 | return ErrRestTimeOrder 108 | } 109 | 110 | if start.Before(c.StartAt) || end.After(c.EndAt) { 111 | return ErrRestTimeOverflow 112 | } 113 | 114 | // RestTime should in order. 115 | if key != 0 { 116 | previousStart := c.RestTime[key-1][0] 117 | if start.Before(previousStart) { 118 | return ErrRestTimeListOrder 119 | } 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // combineDuration combines time duration, the operation is idempotent. 127 | // If two time duration are overlapped, the former one will be combined with the latter one, 128 | // and the former duration will be set to nil and be removed. 129 | func combineDuration(d [][]time.Time) [][]time.Time { 130 | for i := 0; i < len(d)-1; i++ { 131 | headIndex := i 132 | nextIndex := i + 1 133 | 134 | headBegin, headEnd := d[headIndex][0], d[headIndex][1] 135 | nextBegin, nextEnd := d[nextIndex][0], d[nextIndex][1] 136 | 137 | // Head: ... ============== E 138 | // Next: B ===... 139 | if headEnd.After(nextBegin) { 140 | if headEnd.After(nextEnd) { 141 | // Head: ... ============ E 142 | // Next: B === E 143 | // v 144 | // Head: ... ============ E 145 | // Next: ... ============ E 146 | d[nextIndex] = d[headIndex] 147 | } else { 148 | // Head: ... ============ E 149 | // Next: B ============= E 150 | // v 151 | // Head: ... ============ E 152 | // Next: ... ================= E 153 | d[nextIndex][0] = headBegin 154 | } 155 | d[headIndex] = nil 156 | } 157 | } 158 | 159 | // Remove the empty rest time element. 160 | for i := 0; i < len(d); i++ { 161 | if d[i] == nil { 162 | d = append(d[:i], d[i+1:]...) 163 | } 164 | } 165 | 166 | return d 167 | } 168 | -------------------------------------------------------------------------------- /internal/clock/clock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package clock 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func Test_checkConfig(t *testing.T) { 15 | for _, tc := range []struct { 16 | name string 17 | clock *Clock 18 | error error 19 | }{ 20 | { 21 | name: "normal", 22 | clock: &Clock{ 23 | StartAt: date(2021, 10, 3, 12, 0, 0), 24 | EndAt: date(2021, 10, 5, 12, 0, 0), 25 | RestTime: [][]time.Time{ 26 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 27 | {date(2021, 10, 4, 20, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 28 | }, 29 | }, 30 | error: nil, 31 | }, 32 | { 33 | name: "same start and end time", 34 | clock: &Clock{ 35 | StartAt: date(2021, 10, 3, 12, 0, 0), 36 | EndAt: date(2021, 10, 3, 12, 0, 0), 37 | }, 38 | error: nil, 39 | }, 40 | { 41 | name: "start time order", 42 | clock: &Clock{ 43 | StartAt: date(2021, 10, 3, 12, 0, 0), 44 | EndAt: date(2021, 10, 3, 5, 0, 0), 45 | }, 46 | error: ErrStartTimeOrder, 47 | }, 48 | { 49 | name: "rest time format", 50 | clock: &Clock{ 51 | StartAt: date(2021, 10, 3, 12, 0, 0), 52 | EndAt: date(2021, 10, 5, 12, 0, 0), 53 | RestTime: [][]time.Time{ 54 | {date(2021, 10, 3, 20, 0, 0)}, 55 | }, 56 | }, 57 | error: ErrRestTimeFormat, 58 | }, 59 | { 60 | name: "rest time format", 61 | clock: &Clock{ 62 | StartAt: date(2021, 10, 3, 12, 0, 0), 63 | EndAt: date(2021, 10, 5, 12, 0, 0), 64 | RestTime: [][]time.Time{ 65 | {date(2021, 10, 2, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 66 | {date(2021, 10, 4, 20, 0, 0), date(2021, 10, 6, 8, 0, 0)}, 67 | }, 68 | }, 69 | error: ErrRestTimeOverflow, 70 | }, 71 | { 72 | name: "rest time order", 73 | clock: &Clock{ 74 | StartAt: date(2021, 10, 3, 12, 0, 0), 75 | EndAt: date(2021, 10, 5, 12, 0, 0), 76 | RestTime: [][]time.Time{ 77 | {date(2021, 10, 4, 20, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 78 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 79 | }, 80 | }, 81 | error: ErrRestTimeListOrder, 82 | }, 83 | } { 84 | t.Run(tc.name, func(t *testing.T) { 85 | got := tc.clock.checkConfig() 86 | assert.Equal(t, tc.error, got) 87 | }) 88 | } 89 | } 90 | 91 | func Test_combineDuration(t *testing.T) { 92 | for _, tc := range []struct { 93 | name string 94 | durations [][]time.Time 95 | want [][]time.Time 96 | }{ 97 | { 98 | name: "empty duration time", 99 | durations: [][]time.Time{}, 100 | want: [][]time.Time{}, 101 | }, 102 | { 103 | name: "no overlap", 104 | durations: [][]time.Time{ 105 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 106 | {date(2021, 10, 4, 20, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 107 | }, 108 | want: [][]time.Time{ 109 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 110 | {date(2021, 10, 4, 20, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 111 | }, 112 | }, 113 | { 114 | name: "former includes latter", 115 | durations: [][]time.Time{ 116 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 5, 20, 0, 0)}, 117 | {date(2021, 10, 4, 8, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 118 | }, 119 | want: [][]time.Time{ 120 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 5, 20, 0, 0)}, 121 | }, 122 | }, 123 | { 124 | name: "overlap", 125 | durations: [][]time.Time{ 126 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 20, 0, 0)}, 127 | {date(2021, 10, 4, 8, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 128 | }, 129 | want: [][]time.Time{ 130 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 131 | }, 132 | }, 133 | { 134 | name: "complex case", 135 | durations: [][]time.Time{ 136 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 137 | {date(2021, 10, 4, 0, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 138 | {date(2021, 10, 4, 13, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 139 | {date(2021, 10, 4, 15, 0, 0), date(2021, 10, 5, 0, 0, 0)}, 140 | }, 141 | want: [][]time.Time{ 142 | {date(2021, 10, 3, 20, 0, 0), date(2021, 10, 4, 8, 0, 0)}, 143 | {date(2021, 10, 4, 13, 0, 0), date(2021, 10, 5, 8, 0, 0)}, 144 | }, 145 | }, 146 | } { 147 | t.Run(tc.name, func(t *testing.T) { 148 | got := combineDuration(tc.durations) 149 | assert.Equal(t, tc.want, got) 150 | }) 151 | } 152 | } 153 | 154 | func date(year, month, day, hour, min, sec int) time.Time { 155 | return time.Date(year, time.Month(month), day, hour, min, sec, 0, time.Local) 156 | } 157 | -------------------------------------------------------------------------------- /internal/clock/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package clock 6 | 7 | import ( 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | ErrZeroRoundDuration = errors.New("round duration is zero") 13 | ErrStartTimeOrder = errors.New("start time should before end time") 14 | ErrRestTimeFormat = errors.New("rest time format error") 15 | ErrRestTimeOrder = errors.New("rest start time should before end time") 16 | ErrRestTimeOverflow = errors.New("rest time overflow") 17 | ErrRestTimeListOrder = errors.New("rest time list should in order") 18 | ) 19 | -------------------------------------------------------------------------------- /internal/clock/process.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package clock 6 | 7 | import ( 8 | "context" 9 | "math" 10 | "sync" 11 | "time" 12 | 13 | log "unknwon.dev/clog/v2" 14 | 15 | "github.com/vidar-team/Cardinal/internal/db" 16 | "github.com/vidar-team/Cardinal/internal/misc/webhook" 17 | "github.com/vidar-team/Cardinal/internal/rank" 18 | //"github.com/vidar-team/Cardinal/internal/score" 19 | ) 20 | 21 | // Start starts the game clock processor routine. 22 | func Start() { 23 | // TODO: only one timer started. 24 | go T.start() 25 | } 26 | 27 | // Stop stops the game clock processor. 28 | func Stop() { 29 | T.stopChan <- struct{}{} 30 | } 31 | 32 | func (c *Clock) start() { 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | 35 | // Refresh the ranking list. 36 | if err := rank.SetTitle(ctx); err != nil { 37 | log.Error("Failed to set rank title: %v", err) 38 | } 39 | if err := rank.SetRankList(ctx); err != nil { 40 | log.Error("Failed to set rank list: %v", err) 41 | } 42 | 43 | var latestCalculateRound uint 44 | lastRound := sync.Once{} 45 | 46 | for { 47 | select { 48 | case <-c.stopChan: 49 | cancel() 50 | close(c.stopChan) 51 | return 52 | 53 | default: 54 | currentTime := time.Now() 55 | 56 | if currentTime.Before(c.StartAt) { 57 | // The game is not started. 58 | c.Status = StatusWait 59 | continue 60 | 61 | } else if currentTime.After(c.EndAt) { 62 | // The game is over. 63 | // Calculate the score of the last round when the game is over. 64 | lastRound.Do(func() { 65 | if err := db.Scores.Calculate(ctx, c.TotalRound); err != nil { 66 | log.Error("Failed to calculate the last round score: %v", err) 67 | } 68 | 69 | go webhook.Add(webhook.END_HOOK, nil) 70 | // TODO logger.New(logger.IMPORTANT, "system", locales.T("timer.end")) 71 | }) 72 | 73 | c.Status = StatusEnd 74 | continue 75 | } 76 | 77 | // The game is running. 78 | // Get which time cycle now. 79 | currentRunTimeIndex := -1 80 | for index, duration := range c.RunTime { 81 | if currentTime.After(duration[0]) && currentTime.Before(duration[1]) { 82 | currentRunTimeIndex = index 83 | break 84 | } 85 | } 86 | 87 | if currentRunTimeIndex == -1 { 88 | // Suspended 89 | if c.Status != StatusPause { 90 | go webhook.Add(webhook.PAUSE_HOOK, nil) 91 | } 92 | c.Status = StatusPause 93 | 94 | } else { 95 | // In progress 96 | c.Status = StatusRunning 97 | 98 | // Cumulative time until now. 99 | var runningDuration time.Duration 100 | for index, duration := range c.RunTime { 101 | if index < currentRunTimeIndex { 102 | runningDuration += duration[1].Sub(duration[0]) 103 | } else { 104 | // The last runtime cycle for now. 105 | runningDuration += currentTime.Sub(duration[0]) 106 | break 107 | } 108 | } 109 | 110 | // Get current round. 111 | currentRound := uint(math.Ceil(runningDuration.Seconds() / c.RoundDuration.Seconds())) 112 | // Calculate the time duration next round. 113 | c.RoundRemainDuration = time.Duration(currentRound)*c.RoundDuration - runningDuration 114 | 115 | // Check if it is a new round. 116 | if c.CurrentRound < currentRound { 117 | c.CurrentRound = currentRound 118 | if c.CurrentRound == 1 { 119 | go webhook.Add(webhook.BEGIN_HOOK, nil) 120 | } 121 | 122 | go webhook.Add(webhook.BEGIN_HOOK, c.CurrentRound) 123 | 124 | // Clean the status of the game boxes. 125 | if err := db.GameBoxes.CleanAllStatus(ctx); err != nil { 126 | log.Error("Failed to clean game boxes' status: %v", err) 127 | } 128 | 129 | // Refresh the ranking list. 130 | if err := rank.SetTitle(ctx); err != nil { 131 | log.Error("Failed to set rank title: %v", err) 132 | } 133 | if err := rank.SetRankList(ctx); err != nil { 134 | log.Error("Failed to set rank list: %v", err) 135 | } 136 | 137 | // If Cardinal has been restart by accident, get the latest round score and chick if it needs to calculate the scores of previous round. 138 | // The default value of `latestCalculateRound` is 0, it means that Cardinal will calculate the score when started. 139 | if latestCalculateRound < c.CurrentRound-1 { 140 | if err := db.Scores.Calculate(ctx, c.CurrentRound-1); err != nil { 141 | log.Error("Failed to calculate score: %v", err) 142 | } 143 | latestCalculateRound = c.CurrentRound - 1 144 | } 145 | 146 | // TODO Auto refresh flag 147 | //RefreshFlag() 148 | 149 | // TODO Asteroid Unity3D refresh. 150 | //asteroid.NewRoundAction() 151 | } 152 | } 153 | 154 | time.Sleep(1 * time.Second) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func stringFlag(name, value, usage string) *cli.StringFlag { 12 | return &cli.StringFlag{ 13 | Name: name, 14 | Value: value, 15 | Usage: usage, 16 | } 17 | } 18 | 19 | func intFlag(name string, value int, usage string) *cli.IntFlag { 20 | return &cli.IntFlag{ 21 | Name: name, 22 | Value: value, 23 | Usage: usage, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/cmd/web.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "github.com/urfave/cli/v2" 9 | log "unknwon.dev/clog/v2" 10 | 11 | "github.com/vidar-team/Cardinal/internal/clock" 12 | "github.com/vidar-team/Cardinal/internal/conf" 13 | "github.com/vidar-team/Cardinal/internal/db" 14 | "github.com/vidar-team/Cardinal/internal/locales" 15 | "github.com/vidar-team/Cardinal/internal/route" 16 | "github.com/vidar-team/Cardinal/internal/store" 17 | ) 18 | 19 | var Web = &cli.Command{ 20 | Name: "web", 21 | Usage: "Start web server", 22 | Description: `Cardinal web server is the only thing you need to run, 23 | and it takes care of all the other things for you`, 24 | Action: runWeb, 25 | Flags: []cli.Flag{ 26 | intFlag("port, p", 19999, "Temporary port number to prevent conflict"), 27 | stringFlag("config, c", "", "Custom configuration file path"), 28 | }, 29 | } 30 | 31 | func runWeb(c *cli.Context) error { 32 | err := conf.Init(c.String("config")) 33 | if err != nil { 34 | log.Fatal("Failed to load config: %v", err) 35 | } 36 | log.Trace(locales.T("config.load_success")) 37 | 38 | if err = db.Init(); err != nil { 39 | log.Fatal("Failed to init database: %v", err) 40 | } 41 | 42 | // TODO Install 43 | 44 | store.Init() 45 | 46 | if err := clock.Init(); err != nil { 47 | log.Fatal("Failed to init clock: %v", err) 48 | } 49 | clock.Start() 50 | 51 | f := route.NewRouter() 52 | log.Info("Listen on http://0.0.0.0:%d", c.Int("port")) 53 | 54 | f.Run("0.0.0.0", c.Int("port")) 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/conf/conf.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/pelletier/go-toml" 11 | "github.com/pkg/errors" 12 | log "unknwon.dev/clog/v2" 13 | ) 14 | 15 | func init() { 16 | err := log.NewConsole() 17 | if err != nil { 18 | panic("init console logger: " + err.Error()) 19 | } 20 | } 21 | 22 | func Init(customConf string) error { 23 | if customConf == "" { 24 | customConf = "./conf/Cardinal.toml" 25 | } 26 | 27 | config, err := toml.LoadFile(customConf) 28 | if err != nil { 29 | return errors.Wrap(err, "load toml config file") 30 | } 31 | return parse(config) 32 | } 33 | 34 | // parseTree parses the given toml Tree. 35 | func parse(config *toml.Tree) error { 36 | if err := config.Get("App").(*toml.Tree).Unmarshal(&App); err != nil { 37 | return errors.Wrap(err, "mapping [App] section") 38 | } 39 | 40 | if err := config.Get("Database").(*toml.Tree).Unmarshal(&Database); err != nil { 41 | return errors.Wrap(err, "mapping [Database] section") 42 | } 43 | 44 | if err := config.Get("Game").(*toml.Tree).Unmarshal(&Game); err != nil { 45 | return errors.Wrap(err, "mapping [Game] section") 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func Save(customConf string) error { 52 | if customConf == "" { 53 | customConf = "./conf/Cardinal.toml" 54 | } 55 | 56 | configBytes, err := toml.Marshal(map[string]interface{}{ 57 | "App": App, 58 | "Database": Database, 59 | "Game": Game, 60 | }) 61 | if err != nil { 62 | return errors.Wrap(err, "marshal") 63 | } 64 | 65 | if err := os.WriteFile(customConf, configBytes, 0644); err != nil { 66 | return errors.Wrap(err, "write file") 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/conf/conf_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/Cardinal-Platform/testify/assert" 11 | ) 12 | 13 | func TestNewInit(t *testing.T) { 14 | assert.Nil(t, Init("./testdata/custom.toml")) 15 | } 16 | -------------------------------------------------------------------------------- /internal/conf/static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "github.com/pelletier/go-toml" 9 | ) 10 | 11 | // Build time and commit information. 12 | // It should only be set by "-ldflags". 13 | var ( 14 | Version = "develop" 15 | BuildTime string 16 | BuildCommit string 17 | ) 18 | 19 | type Period struct { 20 | StartAt toml.LocalDateTime 21 | EndAt toml.LocalDateTime 22 | } 23 | 24 | var ( 25 | // App is the application settings. 26 | App struct { 27 | Name string 28 | Version string `toml:"-"` // Version should only be set by the main package. 29 | Language string 30 | HTTPAddr string 31 | SeparateFrontend bool 32 | EnableSentry bool 33 | SecuritySalt string 34 | } 35 | 36 | // Database is the database settings. 37 | Database struct { 38 | Type string 39 | Host string 40 | Port uint 41 | Name string 42 | User string 43 | Password string 44 | SSLMode string 45 | MaxOpenConns int 46 | MaxIdleConns int 47 | } 48 | 49 | // Game is the game settings. 50 | Game struct { 51 | StartAt toml.LocalDateTime 52 | EndAt toml.LocalDateTime 53 | PauseTime []Period 54 | RoundDuration uint 55 | 56 | FlagPrefix string 57 | FlagSuffix string 58 | 59 | AttackScore int 60 | CheckDownScore int 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /internal/conf/test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | import ( 8 | "os" 9 | "time" 10 | 11 | "github.com/pelletier/go-toml" 12 | "github.com/pkg/errors" 13 | "github.com/thanhpk/randstr" 14 | ) 15 | 16 | func TestInit() error { 17 | config, err := toml.Load( 18 | ` 19 | [App] 20 | Language = "zh-CN" 21 | HTTPAddr = ":19999" 22 | SeparateFrontend = false 23 | EnableSentry = false 24 | 25 | [Database] 26 | Type = "mysql" 27 | SSLMode = "disable" 28 | MaxOpenConns = 50 29 | MaxIdleConns = 50 30 | 31 | [Game] 32 | RoundDuration = 300 33 | AttackScore = 10 34 | CheckDownScore = 10 35 | `) 36 | if err != nil { 37 | return errors.Wrap(err, "load test config") 38 | } 39 | 40 | if err := parse(config); err != nil { 41 | return errors.Wrap(err, "parse config") 42 | } 43 | 44 | App.SecuritySalt = randstr.String(64) 45 | 46 | // Connect to the test environment database. 47 | Database.Host = os.ExpandEnv("$DBHOST:$DBPORT") 48 | Database.User = os.Getenv("DBUSER") 49 | Database.Password = os.Getenv("DBPASSWORD") 50 | Database.Name = os.Getenv("DBNAME") 51 | 52 | Game.StartAt = toml.LocalDateTimeOf(time.Now()) 53 | Game.EndAt = toml.LocalDateTimeOf(time.Now().Add(12 * time.Hour)) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/conf/testdata/custom.toml: -------------------------------------------------------------------------------- 1 | [App] 2 | Name = "HCTF" 3 | EnableSentry = true 4 | HTTPAddr = ":19999" 5 | Language = "zh-CN" 6 | SeparateFrontend = true 7 | 8 | [Database] 9 | Type = "mysql" 10 | Host = "tcp(127.0.0.1:3306)" 11 | Name = "cardinal" 12 | User = "root" 13 | Password = "root" 14 | SSLMode = "" 15 | MaxIdleConns = 150 16 | MaxOpenConns = 200 17 | 18 | [Game] 19 | FlagPrefix = "hctf{" 20 | FlagSuffix = "}" 21 | AttackScore = 50 22 | CheckDownScore = 50 23 | Duration = 5 24 | -------------------------------------------------------------------------------- /internal/container/container.go: -------------------------------------------------------------------------------- 1 | package container 2 | -------------------------------------------------------------------------------- /internal/container/docker.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | "github.com/parnurzeal/gorequest" 13 | 14 | "github.com/vidar-team/Cardinal/internal/dbold" 15 | "github.com/vidar-team/Cardinal/internal/utils" 16 | ) 17 | 18 | type dockerImage struct { 19 | Layers []struct { 20 | Digest string `json:"digest"` 21 | Instruction string `json:"instruction"` 22 | Size int `json:"size"` 23 | } `json:"layers"` 24 | } 25 | 26 | func GetImageData(c *gin.Context) (int, interface{}) { 27 | type inputForm struct { 28 | User string `binding:"required"` 29 | Image string `binding:"required"` 30 | Tag string `binding:"required"` 31 | } 32 | 33 | var form inputForm 34 | err := c.BindJSON(&form) 35 | if err != nil { 36 | return utils.MakeErrJSON(400, 40041, "payload error") 37 | } 38 | req := gorequest.New().Get(fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/%s/tags/%s/images", form.User, form.Image, form.Tag)) 39 | req.Timeout(5 * time.Second) 40 | resp, body, _ := req.End() 41 | if resp == nil || resp.StatusCode != 200 { 42 | return utils.MakeErrJSON(500, 50028, "request dockerhub failed") 43 | } 44 | 45 | var imageInfo []dockerImage 46 | if err := json.Unmarshal([]byte(body), &imageInfo); err != nil { 47 | return utils.MakeErrJSON(500, 50029, "dockerhub json unmarshal error") 48 | } 49 | if len(imageInfo) == 0 { 50 | return utils.MakeErrJSON(500, 50030, "dockerhub repo is empty") 51 | } 52 | 53 | var ports []int 54 | 55 | reg := regexp.MustCompile(`EXPOSE\s+(\d+)`) 56 | // parse dockerfile. 57 | for _, layer := range imageInfo[0].Layers { 58 | portStr := reg.FindStringSubmatch(layer.Instruction) 59 | for _, str := range portStr { 60 | port, err := strconv.Atoi(str) 61 | if err == nil { 62 | ports = append(ports, port) 63 | } 64 | } 65 | } 66 | 67 | return utils.MakeSuccessJSON(gin.H{ 68 | "Image": fmt.Sprintf("%s/%s:%s", form.User, form.Image, form.Tag), 69 | "Name": form.Image, 70 | "Ports": ports, 71 | }) 72 | } 73 | 74 | func DeployFromDocker(c *gin.Context) (int, interface{}) { 75 | type port struct { 76 | In uint `binding:"required"` 77 | Out uint `binding:"required"` 78 | } 79 | 80 | type inputForm struct { 81 | Image string `binding:"required"` 82 | Challenge uint `binding:"required"` 83 | IP string `binding:"required"` 84 | ServicePort uint `binding:"required"` 85 | SSHPort uint `binding:"required"` 86 | RootSSHName string `binding:"required"` 87 | UserSSHName string `binding:"required"` 88 | Description string `binding:"required"` 89 | Ports []port `binding:"required"` 90 | } 91 | 92 | var form inputForm 93 | err := c.BindJSON(&form) 94 | if err != nil { 95 | return utils.MakeErrJSON(400, 40042, "payload error") 96 | } 97 | 98 | // Pre-check 99 | 100 | // challenge exist 101 | var chall dbold.Challenge 102 | dbold.MySQL.Model(&dbold.Challenge{}).Where(&dbold.Challenge{Model: gorm.Model{ID: form.Challenge}}).Find(&chall) 103 | if chall.ID == 0 { 104 | return utils.MakeErrJSON(404, 40406, "payload error") 105 | } 106 | // port check 107 | if form.ServicePort == 0 || form.ServicePort > 65536 || form.SSHPort == 0 || form.SSHPort > 65536 { 108 | return utils.MakeErrJSON(400, 40043, "error port") 109 | } 110 | for i1, p1 := range form.Ports { 111 | if p1.In == 0 || p1.In > 65536 || p1.Out == 0 || p1.Out > 65536 { 112 | return utils.MakeErrJSON(400, 40043, "error port") 113 | } 114 | for i2, p2 := range form.Ports { 115 | if i1 != i2 && (p1.In == p2.In || p1.Out == p2.Out) { 116 | return utils.MakeErrJSON(400, 40044, "error port") 117 | } 118 | } 119 | } 120 | // check name 121 | if form.RootSSHName == form.UserSSHName { 122 | return utils.MakeErrJSON(400, 40045, "name repeat") 123 | } 124 | 125 | // get the docker image 126 | // TODO 127 | 128 | return utils.MakeSuccessJSON("") 129 | } 130 | -------------------------------------------------------------------------------- /internal/container/image.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/docker/docker/api/types" 10 | "github.com/docker/docker/client" 11 | ) 12 | 13 | // FetchImage pull the image from the given registry. 14 | func FetchImage(registry string, repo string, name string, tag string) error { 15 | dockerCli, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation()) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | events, err := dockerCli.ImagePull( 21 | context.Background(), 22 | fmt.Sprintf("%s/%s/%s:%s", registry, repo, name, tag), 23 | types.ImagePullOptions{}, 24 | ) 25 | if err != nil { 26 | return err 27 | } 28 | d := json.NewDecoder(events) 29 | 30 | type Event struct { 31 | Status string `json:"status"` 32 | Error string `json:"error"` 33 | Progress string `json:"progress"` 34 | ProgressDetail struct { 35 | Current int `json:"current"` 36 | Total int `json:"total"` 37 | } `json:"progressDetail"` 38 | } 39 | 40 | var event *Event 41 | for { 42 | if err := d.Decode(&event); err != nil { 43 | if err == io.EOF { 44 | break 45 | } 46 | panic(err) 47 | } 48 | fmt.Printf("EVENT: %+v\n", event) 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "net/http" 9 | "strconv" 10 | 11 | "github.com/flamego/flamego" 12 | jsoniter "github.com/json-iterator/go" 13 | "github.com/unknwon/com" 14 | log "unknwon.dev/clog/v2" 15 | ) 16 | 17 | // Context represents context of a request. 18 | type Context struct { 19 | flamego.Context 20 | } 21 | 22 | func (c *Context) Success(data ...interface{}) error { 23 | c.ResponseWriter().Header().Set("Content-Type", "application/json") 24 | c.ResponseWriter().WriteHeader(http.StatusOK) 25 | 26 | var d interface{} 27 | if len(data) == 1 { 28 | d = data[0] 29 | } else { 30 | d = "" 31 | } 32 | 33 | err := jsoniter.NewEncoder(c.ResponseWriter()).Encode( 34 | map[string]interface{}{ 35 | "error": 0, 36 | "data": d, 37 | }, 38 | ) 39 | if err != nil { 40 | log.Error("Failed to encode: %v", err) 41 | } 42 | return nil 43 | } 44 | 45 | func (c *Context) ServerError() error { 46 | return c.Error(http.StatusInternalServerError*100, "Internal server error") 47 | } 48 | 49 | func (c *Context) Error(errorCode uint, message string) error { 50 | statusCode := int(errorCode / 100) 51 | 52 | c.ResponseWriter().Header().Set("Content-Type", "application/json") 53 | c.ResponseWriter().WriteHeader(statusCode) 54 | 55 | err := jsoniter.NewEncoder(c.ResponseWriter()).Encode( 56 | map[string]interface{}{ 57 | "error": errorCode, 58 | "msg": message, 59 | }, 60 | ) 61 | if err != nil { 62 | log.Error("Failed to encode: %v", err) 63 | } 64 | return nil 65 | } 66 | 67 | // Query queries form parameter. 68 | func (c *Context) Query(name string) string { 69 | return c.Request().URL.Query().Get(name) 70 | } 71 | 72 | // QueryInt returns query result in int type. 73 | func (c *Context) QueryInt(name string) int { 74 | return com.StrTo(c.Query(name)).MustInt() 75 | } 76 | 77 | // QueryInt64 returns query result in int64 type. 78 | func (c *Context) QueryInt64(name string) int64 { 79 | return com.StrTo(c.Query(name)).MustInt64() 80 | } 81 | 82 | // QueryFloat64 returns query result in float64 type. 83 | func (c *Context) QueryFloat64(name string) float64 { 84 | v, _ := strconv.ParseFloat(c.Query(name), 64) 85 | return v 86 | } 87 | 88 | // Contexter initializes a classic context for a request. 89 | func Contexter() flamego.Handler { 90 | return func(ctx flamego.Context) { 91 | c := Context{ 92 | Context: ctx, 93 | } 94 | 95 | c.Map(c) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/db/bulletins.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/pkg/errors" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var _ BulletinsStore = (*bulletins)(nil) 15 | 16 | // Bulletins is the default instance of the BulletinsStore. 17 | var Bulletins BulletinsStore 18 | 19 | // BulletinsStore is the persistent interface for bulletins. 20 | type BulletinsStore interface { 21 | // Create creates a new bulletin and persists to database. 22 | // It returns the bulletin ID when bulletin created. 23 | Create(ctx context.Context, opts CreateBulletinOptions) (uint, error) 24 | // Get returns all the bulletins. 25 | Get(ctx context.Context) ([]*Bulletin, error) 26 | // GetByID returns the bulletin with given id. 27 | // It returns ErrBulletinNotExists when not found. 28 | GetByID(ctx context.Context, id uint) (*Bulletin, error) 29 | // Update updates the bulletin with given id. 30 | Update(ctx context.Context, id uint, opts UpdateBulletinOptions) error 31 | // DeleteByID deletes the bulletin with given id. 32 | DeleteByID(ctx context.Context, id uint) error 33 | // DeleteAll deletes all the bulletins. 34 | DeleteAll(ctx context.Context) error 35 | } 36 | 37 | // NewBulletinsStore returns a BulletinsStore instance with the given database connection. 38 | func NewBulletinsStore(db *gorm.DB) BulletinsStore { 39 | return &bulletins{DB: db} 40 | } 41 | 42 | // Bulletin represents the bulletin which sent to teams. 43 | type Bulletin struct { 44 | gorm.Model 45 | 46 | Title string 47 | Body string 48 | } 49 | 50 | type bulletins struct { 51 | *gorm.DB 52 | } 53 | 54 | type CreateBulletinOptions struct { 55 | Title string 56 | Body string 57 | } 58 | 59 | func (db *bulletins) Create(ctx context.Context, opts CreateBulletinOptions) (uint, error) { 60 | bulletin := &Bulletin{ 61 | Title: opts.Title, 62 | Body: opts.Body, 63 | } 64 | if err := db.WithContext(ctx).Create(bulletin).Error; err != nil { 65 | return 0, err 66 | } 67 | 68 | return bulletin.ID, nil 69 | } 70 | 71 | func (db *bulletins) Get(ctx context.Context) ([]*Bulletin, error) { 72 | var bulletins []*Bulletin 73 | return bulletins, db.DB.WithContext(ctx).Model(&Bulletin{}).Order("id ASC").Find(&bulletins).Error 74 | } 75 | 76 | var ErrBulletinNotExists = errors.New("bulletin does not exist") 77 | 78 | func (db *bulletins) GetByID(ctx context.Context, id uint) (*Bulletin, error) { 79 | var bulletin Bulletin 80 | if err := db.WithContext(ctx).Model(&Bulletin{}).Where("id = ?", id).First(&bulletin).Error; err != nil { 81 | if err == gorm.ErrRecordNotFound { 82 | return nil, ErrBulletinNotExists 83 | } 84 | return nil, errors.Wrap(err, "get") 85 | } 86 | return &bulletin, nil 87 | } 88 | 89 | type UpdateBulletinOptions struct { 90 | Title string 91 | Body string 92 | } 93 | 94 | func (db *bulletins) Update(ctx context.Context, id uint, opts UpdateBulletinOptions) error { 95 | return db.WithContext(ctx).Model(&Bulletin{}).Where("id = ?", id). 96 | Select("Title", "Body"). 97 | Updates(&Bulletin{ 98 | Title: opts.Title, 99 | Body: opts.Body, 100 | }).Error 101 | } 102 | 103 | func (db *bulletins) DeleteByID(ctx context.Context, id uint) error { 104 | return db.WithContext(ctx).Delete(&Bulletin{}, "id = ?", id).Error 105 | } 106 | 107 | func (db *bulletins) DeleteAll(ctx context.Context) error { 108 | return db.WithContext(ctx).Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&Bulletin{}).Error 109 | } 110 | -------------------------------------------------------------------------------- /internal/db/bulletins_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func TestBulletins(t *testing.T) { 17 | if testing.Short() { 18 | t.Skip() 19 | } 20 | 21 | t.Parallel() 22 | 23 | db, cleanup := newTestDB(t) 24 | store := NewBulletinsStore(db) 25 | 26 | for _, tc := range []struct { 27 | name string 28 | test func(t *testing.T, ctx context.Context, db *bulletins) 29 | }{ 30 | {"Create", testBulletinsCreate}, 31 | {"Get", testBulletinsGet}, 32 | {"GetByID", testBulletinsGetByID}, 33 | {"Update", testBulletinsUpdate}, 34 | {"DeleteByID", testBulletinsDeleteByID}, 35 | {"DeleteAll", testBulletinsDeleteAll}, 36 | } { 37 | t.Run(tc.name, func(t *testing.T) { 38 | t.Cleanup(func() { 39 | err := cleanup("bulletins") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | }) 44 | tc.test(t, context.Background(), store.(*bulletins)) 45 | }) 46 | } 47 | } 48 | 49 | func testBulletinsCreate(t *testing.T, ctx context.Context, db *bulletins) { 50 | id, err := db.Create(ctx, CreateBulletinOptions{ 51 | Title: "Welcome to D^3CTF", 52 | Body: "Hey CTFer! Welcome to D^3CTF!", 53 | }) 54 | assert.Equal(t, uint(1), id) 55 | assert.Nil(t, err) 56 | } 57 | 58 | func testBulletinsGet(t *testing.T, ctx context.Context, db *bulletins) { 59 | // Get empty bulletins lists. 60 | got, err := db.Get(ctx) 61 | assert.Nil(t, err) 62 | want := []*Bulletin{} 63 | assert.Equal(t, want, got) 64 | 65 | id, err := db.Create(ctx, CreateBulletinOptions{ 66 | Title: "Welcome to D^3CTF", 67 | Body: "Hey CTFer! Welcome to D^3CTF!", 68 | }) 69 | assert.Equal(t, uint(1), id) 70 | assert.Nil(t, err) 71 | 72 | id, err = db.Create(ctx, CreateBulletinOptions{ 73 | Title: "Web1 Updated", 74 | Body: "Web1 Updated. Please check the hint.", 75 | }) 76 | assert.Equal(t, uint(2), id) 77 | assert.Nil(t, err) 78 | 79 | got, err = db.Get(ctx) 80 | assert.Nil(t, err) 81 | 82 | for _, bulletin := range got { 83 | bulletin.CreatedAt = time.Time{} 84 | bulletin.UpdatedAt = time.Time{} 85 | bulletin.DeletedAt = gorm.DeletedAt{} 86 | } 87 | 88 | want = []*Bulletin{ 89 | { 90 | Model: gorm.Model{ 91 | ID: 1, 92 | }, 93 | Title: "Welcome to D^3CTF", 94 | Body: "Hey CTFer! Welcome to D^3CTF!", 95 | }, 96 | { 97 | Model: gorm.Model{ 98 | ID: 2, 99 | }, 100 | Title: "Web1 Updated", 101 | Body: "Web1 Updated. Please check the hint.", 102 | }, 103 | } 104 | assert.Equal(t, want, got) 105 | } 106 | 107 | func testBulletinsGetByID(t *testing.T, ctx context.Context, db *bulletins) { 108 | id, err := db.Create(ctx, CreateBulletinOptions{ 109 | Title: "Welcome to D^3CTF", 110 | Body: "Hey CTFer! Welcome to D^3CTF!", 111 | }) 112 | assert.Equal(t, uint(1), id) 113 | assert.Nil(t, err) 114 | 115 | got, err := db.GetByID(ctx, 1) 116 | assert.Nil(t, err) 117 | got.CreatedAt = time.Time{} 118 | got.UpdatedAt = time.Time{} 119 | got.DeletedAt = gorm.DeletedAt{} 120 | 121 | want := &Bulletin{ 122 | Model: gorm.Model{ 123 | ID: 1, 124 | }, 125 | Title: "Welcome to D^3CTF", 126 | Body: "Hey CTFer! Welcome to D^3CTF!", 127 | } 128 | assert.Equal(t, want, got) 129 | 130 | // Get not exist bulletin. 131 | got, err = db.GetByID(ctx, 2) 132 | assert.Equal(t, ErrBulletinNotExists, err) 133 | want = (*Bulletin)(nil) 134 | assert.Equal(t, want, got) 135 | } 136 | 137 | func testBulletinsUpdate(t *testing.T, ctx context.Context, db *bulletins) { 138 | id, err := db.Create(ctx, CreateBulletinOptions{ 139 | Title: "Welcome to D^3CTF", 140 | Body: "Hey CTFer! Welcome to D^3CTF!", 141 | }) 142 | assert.Equal(t, uint(1), id) 143 | assert.Nil(t, err) 144 | 145 | err = db.Update(ctx, 1, UpdateBulletinOptions{ 146 | Title: "Welcome to D^3CTF!!!!", 147 | Body: "Hey CTFer! Nice to meet you here!", 148 | }) 149 | assert.Nil(t, err) 150 | 151 | got, err := db.GetByID(ctx, 1) 152 | assert.Nil(t, err) 153 | got.CreatedAt = time.Time{} 154 | got.UpdatedAt = time.Time{} 155 | got.DeletedAt = gorm.DeletedAt{} 156 | 157 | want := &Bulletin{ 158 | Model: gorm.Model{ 159 | ID: 1, 160 | }, 161 | Title: "Welcome to D^3CTF!!!!", 162 | Body: "Hey CTFer! Nice to meet you here!", 163 | } 164 | assert.Equal(t, want, got) 165 | } 166 | 167 | func testBulletinsDeleteByID(t *testing.T, ctx context.Context, db *bulletins) { 168 | id, err := db.Create(ctx, CreateBulletinOptions{ 169 | Title: "Welcome to D^3CTF", 170 | Body: "Hey CTFer! Welcome to D^3CTF!", 171 | }) 172 | assert.Equal(t, uint(1), id) 173 | assert.Nil(t, err) 174 | 175 | err = db.DeleteByID(ctx, 1) 176 | assert.Nil(t, err) 177 | 178 | got, err := db.GetByID(ctx, 1) 179 | assert.Equal(t, ErrBulletinNotExists, err) 180 | want := (*Bulletin)(nil) 181 | assert.Equal(t, want, got) 182 | } 183 | 184 | func testBulletinsDeleteAll(t *testing.T, ctx context.Context, db *bulletins) { 185 | id, err := db.Create(ctx, CreateBulletinOptions{ 186 | Title: "Welcome to D^3CTF", 187 | Body: "Hey CTFer! Welcome to D^3CTF!", 188 | }) 189 | assert.Equal(t, uint(1), id) 190 | assert.Nil(t, err) 191 | 192 | id, err = db.Create(ctx, CreateBulletinOptions{ 193 | Title: "Web1 Updated", 194 | Body: "Web1 Updated. Please check the hint.", 195 | }) 196 | assert.Equal(t, uint(2), id) 197 | assert.Nil(t, err) 198 | 199 | err = db.DeleteAll(ctx) 200 | assert.Nil(t, err) 201 | 202 | got, err := db.Get(ctx) 203 | assert.Nil(t, err) 204 | want := []*Bulletin{} 205 | assert.Equal(t, want, got) 206 | } 207 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "fmt" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/postgres" 14 | "gorm.io/gorm" 15 | log "unknwon.dev/clog/v2" 16 | 17 | "github.com/vidar-team/Cardinal/internal/conf" 18 | "github.com/vidar-team/Cardinal/internal/dbutil" 19 | ) 20 | 21 | var AllTables = []interface{}{ 22 | &Action{}, 23 | &Bulletin{}, 24 | &Challenge{}, 25 | &Flag{}, 26 | &GameBox{}, 27 | &Log{}, 28 | &Manager{}, 29 | &Team{}, 30 | } 31 | 32 | type DatabaseType string 33 | 34 | const ( 35 | DatabaseTypeMySQL DatabaseType = "mysql" 36 | DatabaseTypePostgres DatabaseType = "postgres" 37 | ) 38 | 39 | // Init initializes the database. 40 | func Init() error { 41 | var dialector gorm.Dialector 42 | 43 | switch DatabaseType(conf.Database.Type) { 44 | case DatabaseTypeMySQL: 45 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&loc=Local&charset=utf8mb4,utf8", 46 | conf.Database.User, 47 | conf.Database.Password, 48 | conf.Database.Host, 49 | conf.Database.Port, 50 | conf.Database.Name, 51 | ) 52 | dialector = mysql.Open(dsn) 53 | 54 | case DatabaseTypePostgres: 55 | dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", 56 | conf.Database.User, 57 | conf.Database.Password, 58 | conf.Database.Host, 59 | conf.Database.Port, 60 | conf.Database.Name, 61 | conf.Database.SSLMode, 62 | ) 63 | dialector = postgres.Open(dsn) 64 | 65 | default: 66 | log.Fatal("Unexpected database type: %q", conf.Database.Type) 67 | } 68 | 69 | db, err := gorm.Open(dialector, &gorm.Config{ 70 | NowFunc: func() time.Time { 71 | return dbutil.Now() 72 | }, 73 | }) 74 | if err != nil { 75 | return errors.Wrap(err, "open connection") 76 | } 77 | 78 | // Migrate databases. 79 | if db.AutoMigrate(AllTables...) != nil { 80 | return errors.Wrap(err, "auto migrate") 81 | } 82 | 83 | SetDatabaseStore(db) 84 | 85 | return nil 86 | } 87 | 88 | // SetDatabaseStore sets the database table store. 89 | func SetDatabaseStore(db *gorm.DB) { 90 | Actions = NewActionsStore(db) 91 | Bulletins = NewBulletinsStore(db) 92 | Challenges = NewChallengesStore(db) 93 | Flags = NewFlagsStore(db) 94 | GameBoxes = NewGameBoxesStore(db) 95 | Ranks = NewRanksStore(db) 96 | Scores = NewScoresStore(db) 97 | Logs = NewLogsStore(db) 98 | Managers = NewManagersStore(db) 99 | Teams = NewTeamsStore(db) 100 | } 101 | -------------------------------------------------------------------------------- /internal/db/flag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/pkg/errors" 11 | "gorm.io/gorm" 12 | "gorm.io/gorm/clause" 13 | ) 14 | 15 | var _ FlagsStore = (*flags)(nil) 16 | 17 | // Flags is the default instance of the FlagsStore. 18 | var Flags FlagsStore 19 | 20 | // FlagsStore is the persistent interface for flags. 21 | type FlagsStore interface { 22 | // BatchCreate creates flags and persists to database. 23 | BatchCreate(ctx context.Context, opts CreateFlagOptions) error 24 | // Get returns the flags. 25 | Get(ctx context.Context, opts GetFlagOptions) ([]*Flag, int64, error) 26 | // Count counts the number of the flags with the given options. 27 | Count(ctx context.Context, opts CountFlagOptions) (int64, error) 28 | // Check checks the given flag. 29 | // It returns ErrFlagNotExists when not found. 30 | Check(ctx context.Context, flag string) (*Flag, error) 31 | // DeleteAll deletes all the flags. 32 | DeleteAll(ctx context.Context) error 33 | } 34 | 35 | // NewFlagsStore returns a FlagsStore instance with the given database connection. 36 | func NewFlagsStore(db *gorm.DB) FlagsStore { 37 | return &flags{DB: db} 38 | } 39 | 40 | // Flag represents the flag which team submitted. 41 | type Flag struct { 42 | gorm.Model 43 | 44 | TeamID uint `gorm:"uniqueIndex:flag_unique_idx"` 45 | ChallengeID uint `gorm:"uniqueIndex:flag_unique_idx"` 46 | GameBoxID uint `gorm:"uniqueIndex:flag_unique_idx"` 47 | Round uint `gorm:"uniqueIndex:flag_unique_idx"` 48 | 49 | Value string 50 | } 51 | 52 | type flags struct { 53 | *gorm.DB 54 | } 55 | 56 | type FlagMetadata struct { 57 | GameBoxID uint 58 | Round uint 59 | Value string 60 | } 61 | 62 | type CreateFlagOptions struct { 63 | Flags []FlagMetadata 64 | } 65 | 66 | func (db *flags) BatchCreate(ctx context.Context, opts CreateFlagOptions) error { 67 | tx := db.WithContext(ctx).Begin() 68 | 69 | gameBoxIDs := map[uint]struct{}{} 70 | for _, flag := range opts.Flags { 71 | gameBoxIDs[flag.GameBoxID] = struct{}{} 72 | } 73 | 74 | var err error 75 | gameBoxesStore := NewGameBoxesStore(tx) 76 | gameBoxSets := make(map[uint]*GameBox, len(gameBoxIDs)) 77 | for gameBoxID := range gameBoxIDs { 78 | gameBoxSets[gameBoxID], err = gameBoxesStore.GetByID(ctx, gameBoxID) 79 | if err != nil { 80 | tx.Rollback() 81 | if err == ErrGameBoxNotExists { 82 | return err 83 | } 84 | return errors.Wrap(err, "get game box") 85 | } 86 | } 87 | 88 | flags := make([]*Flag, 0, len(opts.Flags)) 89 | for _, flag := range opts.Flags { 90 | flag := flag 91 | gameBox := gameBoxSets[flag.GameBoxID] 92 | flags = append(flags, &Flag{ 93 | TeamID: gameBox.TeamID, 94 | ChallengeID: gameBox.ChallengeID, 95 | GameBoxID: gameBox.ID, 96 | Round: flag.Round, 97 | Value: flag.Value, 98 | }) 99 | } 100 | 101 | if err := tx.Clauses(clause.OnConflict{ 102 | Columns: []clause.Column{{Name: "team_id"}, {Name: "challenge_id"}, {Name: "game_box_id"}, {Name: "round"}}, 103 | DoUpdates: clause.AssignmentColumns([]string{"value"}), 104 | }).CreateInBatches(flags, len(flags)).Error; err != nil { 105 | tx.Rollback() 106 | return errors.Wrap(err, "batch create flag") 107 | } 108 | 109 | if err := tx.Commit().Error; err != nil { 110 | tx.Rollback() 111 | return err 112 | } 113 | return nil 114 | } 115 | 116 | type GetFlagOptions struct { 117 | Page int 118 | PageSize int 119 | TeamID uint 120 | ChallengeID uint 121 | GameBoxID uint 122 | Round uint 123 | } 124 | 125 | func (db *flags) Get(ctx context.Context, opts GetFlagOptions) ([]*Flag, int64, error) { 126 | var flags []*Flag 127 | var count int64 128 | 129 | q := db.WithContext(ctx).Model(&Flag{}).Where(&Flag{ 130 | TeamID: opts.TeamID, 131 | GameBoxID: opts.GameBoxID, 132 | ChallengeID: opts.ChallengeID, 133 | Round: opts.Round, 134 | }) 135 | q.Count(&count) 136 | 137 | if opts.Page <= 0 { 138 | opts.Page = 1 139 | } 140 | 141 | if opts.PageSize != 0 { 142 | q = q.Offset((opts.Page - 1) * opts.PageSize).Limit(opts.PageSize) 143 | } 144 | 145 | return flags, count, q.Find(&flags).Error 146 | } 147 | 148 | type CountFlagOptions struct { 149 | TeamID uint 150 | ChallengeID uint 151 | GameBoxID uint 152 | Round uint 153 | } 154 | 155 | func (db *flags) Count(ctx context.Context, opts CountFlagOptions) (int64, error) { 156 | var count int64 157 | q := db.WithContext(ctx).Model(&Flag{}).Where(&Flag{ 158 | TeamID: opts.TeamID, 159 | GameBoxID: opts.GameBoxID, 160 | ChallengeID: opts.ChallengeID, 161 | Round: opts.Round, 162 | }) 163 | 164 | return count, q.Count(&count).Error 165 | } 166 | 167 | var ErrFlagNotExists = errors.New("flag does not find") 168 | 169 | func (db *flags) Check(ctx context.Context, flagValue string) (*Flag, error) { 170 | var flag Flag 171 | err := db.WithContext(ctx).Model(&Flag{}).Where("value = ?", flagValue).First(&flag).Error 172 | if err != nil { 173 | if err == gorm.ErrRecordNotFound { 174 | return nil, ErrFlagNotExists 175 | } 176 | return nil, errors.Wrap(err, "get") 177 | } 178 | return &flag, nil 179 | } 180 | 181 | func (db *flags) DeleteAll(ctx context.Context) error { 182 | return db.WithContext(ctx).Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&Flag{}).Error 183 | } 184 | -------------------------------------------------------------------------------- /internal/db/logs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/pkg/errors" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | var _ LogsStore = (*logs)(nil) 15 | 16 | // Logs is the default instance of the LogsStore. 17 | var Logs LogsStore 18 | 19 | // LogsStore is the persistent interface for logs. 20 | type LogsStore interface { 21 | // Create creates a new log and persists to database. 22 | Create(ctx context.Context, opts CreateLogOptions) error 23 | // Get returns all the logs. 24 | Get(ctx context.Context) ([]*Log, error) 25 | // DeleteAll deletes all the logs. 26 | DeleteAll(ctx context.Context) error 27 | } 28 | 29 | // NewLogsStore returns a LogsStore instance with the given database connection. 30 | func NewLogsStore(db *gorm.DB) LogsStore { 31 | return &logs{DB: db} 32 | } 33 | 34 | type LogLevel int 35 | 36 | const ( 37 | LogLevelNormal LogLevel = iota 38 | LogLevelWarning 39 | LogLevelImportant 40 | ) 41 | 42 | type LogType string 43 | 44 | const ( 45 | LogTypeHealthCheck LogType = "health_check" 46 | LogTypeManagerOperate LogType = "manager_operate" 47 | LogTypeSSH LogType = "ssh" 48 | LogTypeSystem LogType = "system" 49 | ) 50 | 51 | // Log represents the log. 52 | type Log struct { 53 | gorm.Model 54 | 55 | Level LogLevel 56 | Type LogType 57 | Body string 58 | } 59 | 60 | type logs struct { 61 | *gorm.DB 62 | } 63 | 64 | type CreateLogOptions struct { 65 | Level LogLevel 66 | Type LogType 67 | Body string 68 | } 69 | 70 | var ErrBadLogLevel = errors.New("bad log level") 71 | var ErrBadLogType = errors.New("bad log type") 72 | 73 | func (db *logs) Create(ctx context.Context, opts CreateLogOptions) error { 74 | switch opts.Level { 75 | case LogLevelNormal, LogLevelWarning, LogLevelImportant: 76 | default: 77 | return ErrBadLogLevel 78 | } 79 | 80 | switch opts.Type { 81 | case LogTypeHealthCheck, LogTypeManagerOperate, LogTypeSSH, LogTypeSystem: 82 | default: 83 | return ErrBadLogType 84 | } 85 | 86 | return db.WithContext(ctx).Create(&Log{ 87 | Level: opts.Level, 88 | Type: opts.Type, 89 | Body: opts.Body, 90 | }).Error 91 | } 92 | 93 | func (db *logs) Get(ctx context.Context) ([]*Log, error) { 94 | var logs []*Log 95 | return logs, db.WithContext(ctx).Model(&Log{}).Order("id DESC").Find(&logs).Error 96 | } 97 | 98 | func (db *logs) DeleteAll(ctx context.Context) error { 99 | return db.WithContext(ctx).Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&Log{}).Error 100 | } 101 | -------------------------------------------------------------------------------- /internal/db/logs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | func TestLogs(t *testing.T) { 17 | if testing.Short() { 18 | t.Skip() 19 | } 20 | 21 | t.Parallel() 22 | 23 | db, cleanup := newTestDB(t) 24 | store := NewLogsStore(db) 25 | 26 | for _, tc := range []struct { 27 | name string 28 | test func(t *testing.T, ctx context.Context, db *logs) 29 | }{ 30 | {"Create", testLogsCreate}, 31 | {"Get", testLogsGet}, 32 | {"DeleteAll", testLogsDeleteAll}, 33 | } { 34 | t.Run(tc.name, func(t *testing.T) { 35 | t.Cleanup(func() { 36 | err := cleanup("logs") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | }) 41 | tc.test(t, context.Background(), store.(*logs)) 42 | }) 43 | } 44 | } 45 | 46 | func testLogsCreate(t *testing.T, ctx context.Context, db *logs) { 47 | err := db.Create(ctx, CreateLogOptions{ 48 | Level: LogLevelNormal, 49 | Type: LogTypeSystem, 50 | Body: "Welcome to Cardinal!", 51 | }) 52 | assert.Nil(t, err) 53 | 54 | err = db.Create(ctx, CreateLogOptions{ 55 | Level: 4, 56 | Type: LogTypeSystem, 57 | Body: "Welcome to Cardinal!", 58 | }) 59 | assert.Equal(t, ErrBadLogLevel, err) 60 | 61 | err = db.Create(ctx, CreateLogOptions{ 62 | Level: LogLevelNormal, 63 | Type: "unexpected_type", 64 | Body: "Welcome to Cardinal!", 65 | }) 66 | assert.Equal(t, ErrBadLogType, err) 67 | } 68 | 69 | func testLogsGet(t *testing.T, ctx context.Context, db *logs) { 70 | err := db.Create(ctx, CreateLogOptions{ 71 | Level: LogLevelNormal, 72 | Type: LogTypeSystem, 73 | Body: "Welcome to Cardinal!", 74 | }) 75 | assert.Nil(t, err) 76 | 77 | err = db.Create(ctx, CreateLogOptions{ 78 | Level: LogLevelImportant, 79 | Type: LogTypeSystem, 80 | Body: "Please update your password!", 81 | }) 82 | assert.Nil(t, err) 83 | 84 | got, err := db.Get(ctx) 85 | assert.Nil(t, err) 86 | 87 | for _, log := range got { 88 | log.CreatedAt = time.Time{} 89 | log.UpdatedAt = time.Time{} 90 | } 91 | 92 | want := []*Log{ 93 | { 94 | Model: gorm.Model{ 95 | ID: 2, 96 | }, 97 | Level: LogLevelImportant, 98 | Type: LogTypeSystem, 99 | Body: "Please update your password!", 100 | }, 101 | { 102 | Model: gorm.Model{ 103 | ID: 1, 104 | }, 105 | Level: LogLevelNormal, 106 | Type: LogTypeSystem, 107 | Body: "Welcome to Cardinal!", 108 | }, 109 | } 110 | assert.Equal(t, want, got) 111 | } 112 | 113 | func testLogsDeleteAll(t *testing.T, ctx context.Context, db *logs) { 114 | err := db.Create(ctx, CreateLogOptions{ 115 | Level: LogLevelNormal, 116 | Type: LogTypeSystem, 117 | Body: "Welcome to Cardinal!", 118 | }) 119 | assert.Nil(t, err) 120 | 121 | err = db.Create(ctx, CreateLogOptions{ 122 | Level: LogLevelImportant, 123 | Type: LogTypeSystem, 124 | Body: "Please update your password!", 125 | }) 126 | assert.Nil(t, err) 127 | 128 | err = db.DeleteAll(ctx) 129 | assert.Nil(t, err) 130 | 131 | got, err := db.Get(ctx) 132 | assert.Nil(t, err) 133 | want := []*Log{} 134 | assert.Equal(t, want, got) 135 | } 136 | -------------------------------------------------------------------------------- /internal/db/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "testing" 9 | 10 | "gorm.io/gorm" 11 | 12 | "github.com/vidar-team/Cardinal/internal/dbutil" 13 | ) 14 | 15 | // newTestDB returns a test database instance with the cleanup function. 16 | func newTestDB(t *testing.T) (*gorm.DB, func(...string) error) { 17 | return dbutil.NewTestDB(t, AllTables...) 18 | } 19 | -------------------------------------------------------------------------------- /internal/db/managers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "crypto/sha256" 10 | "crypto/subtle" 11 | "fmt" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/thanhpk/randstr" 15 | "golang.org/x/crypto/pbkdf2" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | var _ ManagersStore = (*managers)(nil) 20 | 21 | // Managers is the default instance of the ManagersStore. 22 | var Managers ManagersStore 23 | 24 | // ManagersStore is the persistent interface for managers. 25 | type ManagersStore interface { 26 | // Authenticate validates name and password. 27 | // It returns ErrBadCredentials when validate failed. 28 | // The check account can't log in. 29 | Authenticate(ctx context.Context, name, password string) (*Manager, error) 30 | // Create creates a new manager and persists to database. 31 | // It returns the manager when it created. 32 | Create(ctx context.Context, opts CreateManagerOptions) (*Manager, error) 33 | // Get returns all the managers. 34 | Get(ctx context.Context) ([]*Manager, error) 35 | // GetByID returns the manager with given id. 36 | // It returns ErrManagerNotExists when not found. 37 | GetByID(ctx context.Context, id uint) (*Manager, error) 38 | // ChangePassword changes the manager's password with given id. 39 | ChangePassword(ctx context.Context, id uint, newPassword string) error 40 | // Update updates the manager with given id. 41 | Update(ctx context.Context, id uint, opts UpdateManagerOptions) error 42 | // DeleteByID deletes the manager with given id. 43 | DeleteByID(ctx context.Context, id uint) error 44 | // DeleteAll deletes all the managers. 45 | DeleteAll(ctx context.Context) error 46 | } 47 | 48 | // NewManagersStore returns a ManagersStore instance with the given database connection. 49 | func NewManagersStore(db *gorm.DB) ManagersStore { 50 | return &managers{DB: db} 51 | } 52 | 53 | // Manager represents the manager. 54 | type Manager struct { 55 | gorm.Model 56 | 57 | Name string 58 | Password string 59 | Salt string 60 | IsCheckAccount bool 61 | } 62 | 63 | // EncodePassword encodes password to safe format. 64 | func (m *Manager) EncodePassword() { 65 | newPasswd := pbkdf2.Key([]byte(m.Password), []byte(m.Salt), 10000, 50, sha256.New) 66 | m.Password = fmt.Sprintf("%x", newPasswd) 67 | } 68 | 69 | // ValidatePassword checks if given password matches the one belongs to the manager. 70 | func (m *Manager) ValidatePassword(password string) bool { 71 | newManager := &Manager{Password: password, Salt: m.Salt} 72 | newManager.EncodePassword() 73 | return subtle.ConstantTimeCompare([]byte(m.Password), []byte(newManager.Password)) == 1 74 | } 75 | 76 | // getManagerSalt returns a random manager salt token. 77 | func getManagerSalt() string { 78 | return randstr.String(10) 79 | } 80 | 81 | type managers struct { 82 | *gorm.DB 83 | } 84 | 85 | func (db *managers) Authenticate(ctx context.Context, name, password string) (*Manager, error) { 86 | var manager Manager 87 | if err := db.WithContext(ctx).Model(&Manager{}).Where("name = ?", name).First(&manager).Error; err != nil { 88 | return nil, ErrBadCredentials 89 | } 90 | 91 | // Check account can't log in. 92 | if manager.IsCheckAccount || !manager.ValidatePassword(password) { 93 | return nil, ErrBadCredentials 94 | } 95 | return &manager, nil 96 | } 97 | 98 | type CreateManagerOptions struct { 99 | Name string 100 | Password string 101 | IsCheckAccount bool 102 | } 103 | 104 | var ErrManagerAlreadyExists = errors.New("manager already exits") 105 | 106 | func (db *managers) Create(ctx context.Context, opts CreateManagerOptions) (*Manager, error) { 107 | var manager Manager 108 | if err := db.WithContext(ctx).Model(&Manager{}).Where("name = ?", opts.Name).First(&manager).Error; err == nil { 109 | return nil, ErrManagerAlreadyExists 110 | } else if err != gorm.ErrRecordNotFound { 111 | return nil, errors.Wrap(err, "get") 112 | } 113 | 114 | m := &Manager{ 115 | Name: opts.Name, 116 | Password: opts.Password, 117 | Salt: getManagerSalt(), 118 | IsCheckAccount: opts.IsCheckAccount, 119 | } 120 | m.EncodePassword() 121 | 122 | return m, db.WithContext(ctx).Create(m).Error 123 | } 124 | 125 | func (db *managers) Get(ctx context.Context) ([]*Manager, error) { 126 | var managers []*Manager 127 | return managers, db.WithContext(ctx).Model(&Manager{}).Order("id ASC").Find(&managers).Error 128 | } 129 | 130 | var ErrManagerNotExists = errors.New("manager dose not exist") 131 | 132 | func (db *managers) GetByID(ctx context.Context, id uint) (*Manager, error) { 133 | var manager Manager 134 | if err := db.WithContext(ctx).Model(&Manager{}).Where("id = ?", id).First(&manager).Error; err != nil { 135 | if err == gorm.ErrRecordNotFound { 136 | return nil, ErrManagerNotExists 137 | } 138 | return nil, err 139 | } 140 | 141 | return &manager, nil 142 | } 143 | 144 | func (db *managers) ChangePassword(ctx context.Context, id uint, newPassword string) error { 145 | var newManager Manager 146 | newManager.Password = newPassword 147 | newManager.EncodePassword() 148 | 149 | return db.WithContext(ctx).Model(&Manager{}).Where("id = ?", id).Update("password", newManager.Password).Error 150 | } 151 | 152 | type UpdateManagerOptions struct { 153 | IsCheckAccount bool 154 | } 155 | 156 | func (db *managers) Update(ctx context.Context, id uint, opts UpdateManagerOptions) error { 157 | return db.WithContext(ctx).Model(&Manager{}).Where("id = ?", id).Update("is_check_account", opts.IsCheckAccount).Error 158 | } 159 | 160 | func (db *managers) DeleteByID(ctx context.Context, id uint) error { 161 | return db.WithContext(ctx).Delete(&Manager{}, "id = ?", id).Error 162 | } 163 | 164 | func (db *managers) DeleteAll(ctx context.Context) error { 165 | return db.WithContext(ctx).Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&Manager{}).Error 166 | } 167 | -------------------------------------------------------------------------------- /internal/db/rank.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "sort" 10 | 11 | "github.com/pkg/errors" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | var _ RanksStore = (*ranks)(nil) 16 | 17 | // Ranks is the default instance of the RanksStore. 18 | var Ranks RanksStore 19 | 20 | // RanksStore is the persistent interface for ranks. 21 | type RanksStore interface { 22 | // List returns the ranking list. 23 | List(ctx context.Context) ([]*RankItem, error) 24 | // VisibleChallengeTitle returns the titles of the visible challenges. 25 | VisibleChallengeTitle(ctx context.Context) ([]string, error) 26 | } 27 | 28 | // NewRanksStore returns a RanksStore instance with the given database connection. 29 | func NewRanksStore(db *gorm.DB) RanksStore { 30 | return &ranks{DB: db} 31 | } 32 | 33 | type ranks struct { 34 | *gorm.DB 35 | } 36 | 37 | // RankItem represents a single row of the ranking list. 38 | type RankItem struct { 39 | TeamID uint 40 | TeamName string 41 | TeamLogo string 42 | Rank uint 43 | Score float64 44 | GameBoxes GameBoxInfoList // Ordered by challenge ID. 45 | } 46 | 47 | type GameBoxInfoList []*GameBoxInfo 48 | 49 | // GameBoxInfo contains the game box info. 50 | type GameBoxInfo struct { 51 | ChallengeID uint 52 | IsCaptured bool 53 | IsDown bool 54 | Score float64 `json:",omitempty"` // Manager only 55 | } 56 | 57 | func (g GameBoxInfoList) Len() int { return len(g) } 58 | func (g GameBoxInfoList) Less(i, j int) bool { return g[i].ChallengeID < g[j].ChallengeID } 59 | func (g GameBoxInfoList) Swap(i, j int) { g[i], g[j] = g[j], g[i] } 60 | 61 | type RankListOptions struct { 62 | ShowGameBoxScore bool 63 | } 64 | 65 | func (db *ranks) List(ctx context.Context) ([]*RankItem, error) { 66 | teamsStore := NewTeamsStore(db.DB) 67 | teams, err := teamsStore.Get(ctx, GetTeamsOptions{ 68 | OrderBy: "score", 69 | Order: "DESC", 70 | }) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "get teams") 73 | } 74 | 75 | rankItems := make([]*RankItem, 0, len(teams)) 76 | 77 | gameBoxesStore := NewGameBoxesStore(db.DB) 78 | for _, team := range teams { 79 | gameBoxes, err := gameBoxesStore.Get(ctx, GetGameBoxesOption{ 80 | TeamID: team.ID, 81 | Visible: true, 82 | }) 83 | if err != nil { 84 | return nil, errors.Wrap(err, "get team game box") 85 | } 86 | 87 | gameBoxInfo := make(GameBoxInfoList, 0, len(gameBoxes)) 88 | for _, gameBox := range gameBoxes { 89 | gameBoxInfo = append(gameBoxInfo, &GameBoxInfo{ 90 | ChallengeID: gameBox.ChallengeID, 91 | IsCaptured: gameBox.IsCaptured, 92 | IsDown: gameBox.IsDown, 93 | Score: gameBox.Score, 94 | }) 95 | } 96 | 97 | // Game box should be ordered by the challenge ID, 98 | // to make sure the ranking list table header can match with the score correctly. 99 | sort.Sort(gameBoxInfo) 100 | 101 | rankItems = append(rankItems, &RankItem{ 102 | TeamID: team.ID, 103 | TeamName: team.Name, 104 | TeamLogo: team.Logo, 105 | Rank: team.Rank, 106 | Score: team.Score, 107 | GameBoxes: gameBoxInfo, 108 | }) 109 | } 110 | return rankItems, nil 111 | } 112 | 113 | func (db *ranks) VisibleChallengeTitle(ctx context.Context) ([]string, error) { 114 | var challenges []*Challenge 115 | if err := db.WithContext(ctx).Raw("SELECT * FROM challenges WHERE id IN " + 116 | "(SELECT DISTINCT game_boxes.challenge_id FROM game_boxes WHERE game_boxes.visible = TRUE AND game_boxes.deleted_at IS NULL) " + 117 | "AND deleted_at IS NULL ORDER BY id").Scan(&challenges).Error; err != nil { 118 | return nil, err 119 | } 120 | 121 | titles := make([]string, 0, len(challenges)) 122 | for _, c := range challenges { 123 | titles = append(titles, c.Title) 124 | } 125 | return titles, nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/db/rank_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | 7 | import ( 8 | "context" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestRanks(t *testing.T) { 15 | if testing.Short() { 16 | t.Skip() 17 | } 18 | 19 | t.Parallel() 20 | 21 | db, cleanup := newTestDB(t) 22 | ranksStore := NewRanksStore(db) 23 | 24 | for _, tc := range []struct { 25 | name string 26 | test func(t *testing.T, ctx context.Context, db *ranks) 27 | }{ 28 | {"List", testRanksList}, 29 | {"VisibleChallengeTitle", testRanksVisibleChallengeTitle}, 30 | } { 31 | t.Run(tc.name, func(t *testing.T) { 32 | t.Cleanup(func() { 33 | err := cleanup("teams", "challenges", "game_boxes") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | }) 38 | 39 | ctx := context.Background() 40 | // Create three teams. 41 | teamsStore := NewTeamsStore(db) 42 | _, err := teamsStore.Create(ctx, CreateTeamOptions{Name: "Vidar"}) 43 | assert.Nil(t, err) 44 | _, err = teamsStore.Create(ctx, CreateTeamOptions{Name: "E99p1ant"}) 45 | assert.Nil(t, err) 46 | _, err = teamsStore.Create(ctx, CreateTeamOptions{Name: "Cosmos"}) 47 | assert.Nil(t, err) 48 | 49 | // Create two challenges. 50 | challengesStore := NewChallengesStore(db) 51 | _, err = challengesStore.Create(ctx, CreateChallengeOptions{Title: "Web1", BaseScore: 1000}) 52 | assert.Nil(t, err) 53 | _, err = challengesStore.Create(ctx, CreateChallengeOptions{Title: "Pwn1", BaseScore: 1000}) 54 | assert.Nil(t, err) 55 | 56 | // Create game boxes for each team and challenge. 57 | gameBoxesStore := NewGameBoxesStore(db) 58 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 1, ChallengeID: 1, IPAddress: "192.168.1.1", Port: 80, Description: "Web1 For Vidar"}) 59 | assert.Nil(t, err) 60 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 1, ChallengeID: 2, IPAddress: "192.168.2.1", Port: 8080, Description: "Pwn1 For Vidar"}) 61 | assert.Nil(t, err) 62 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 2, ChallengeID: 1, IPAddress: "192.168.1.2", Port: 80, Description: "Web1 For E99p1ant"}) 63 | assert.Nil(t, err) 64 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 2, ChallengeID: 2, IPAddress: "192.168.2.2", Port: 8080, Description: "Pwn1 For E99p1ant"}) 65 | assert.Nil(t, err) 66 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 3, ChallengeID: 1, IPAddress: "192.168.1.3", Port: 80, Description: "Web1 For Cosmos"}) 67 | assert.Nil(t, err) 68 | _, err = gameBoxesStore.Create(ctx, CreateGameBoxOptions{TeamID: 3, ChallengeID: 2, IPAddress: "192.168.2.3", Port: 8080, Description: "Pwn1 For Cosmos"}) 69 | assert.Nil(t, err) 70 | 71 | tc.test(t, context.Background(), ranksStore.(*ranks)) 72 | }) 73 | } 74 | } 75 | 76 | func testRanksList(t *testing.T, ctx context.Context, db *ranks) { 77 | gameBoxesStore := NewGameBoxesStore(db.DB) 78 | for i := uint(1); i <= 6; i++ { 79 | err := gameBoxesStore.SetVisible(ctx, i, true) 80 | assert.Nil(t, err) 81 | } 82 | 83 | // Cosmos 1800 1000 84 | // Vidar 1000 700 85 | // E99p1ant 700 500 86 | err := gameBoxesStore.SetScore(ctx, 1, 1000) 87 | assert.Nil(t, err) 88 | err = gameBoxesStore.SetScore(ctx, 2, 700) 89 | assert.Nil(t, err) 90 | err = gameBoxesStore.SetScore(ctx, 3, 700) 91 | assert.Nil(t, err) 92 | err = gameBoxesStore.SetScore(ctx, 4, 500) 93 | assert.Nil(t, err) 94 | err = gameBoxesStore.SetScore(ctx, 5, 1800) 95 | assert.Nil(t, err) 96 | err = gameBoxesStore.SetScore(ctx, 6, 1000) 97 | assert.Nil(t, err) 98 | 99 | scoreStore := NewScoresStore(db.DB) 100 | err = scoreStore.RefreshTeamScore(ctx) 101 | assert.Nil(t, err) 102 | 103 | got, err := db.List(ctx) 104 | assert.Nil(t, err) 105 | 106 | want := []*RankItem{ 107 | { 108 | TeamID: 3, 109 | TeamName: "Cosmos", 110 | TeamLogo: "", 111 | Rank: 1, 112 | Score: 2800, 113 | GameBoxes: []*GameBoxInfo{ 114 | {ChallengeID: 1, Score: 1800}, 115 | {ChallengeID: 2, Score: 1000}, 116 | }, 117 | }, 118 | { 119 | TeamID: 1, 120 | TeamName: "Vidar", 121 | Rank: 2, 122 | Score: 1700, 123 | GameBoxes: []*GameBoxInfo{ 124 | {ChallengeID: 1, Score: 1000}, 125 | {ChallengeID: 2, Score: 700}, 126 | }, 127 | }, 128 | { 129 | TeamID: 2, 130 | TeamName: "E99p1ant", 131 | Rank: 3, 132 | Score: 1200, 133 | GameBoxes: []*GameBoxInfo{ 134 | {ChallengeID: 1, Score: 700}, 135 | {ChallengeID: 2, Score: 500}, 136 | }, 137 | }, 138 | } 139 | assert.Equal(t, want, got) 140 | } 141 | 142 | func testRanksVisibleChallengeTitle(t *testing.T, ctx context.Context, db *ranks) { 143 | // All the game boxes are invisible, so the ranking list title is nil. 144 | got, err := db.VisibleChallengeTitle(ctx) 145 | assert.Nil(t, err) 146 | want := []string{} 147 | assert.Equal(t, want, got) 148 | 149 | gameBoxesStore := NewGameBoxesStore(db.DB) 150 | for i := uint(1); i <= 6; i++ { 151 | err := gameBoxesStore.SetVisible(ctx, i, true) 152 | assert.Nil(t, err) 153 | } 154 | 155 | got, err = db.VisibleChallengeTitle(ctx) 156 | assert.Nil(t, err) 157 | want = []string{"Web1", "Pwn1"} 158 | assert.Equal(t, want, got) 159 | } 160 | -------------------------------------------------------------------------------- /internal/db/score_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package db 6 | -------------------------------------------------------------------------------- /internal/dbold/model.go: -------------------------------------------------------------------------------- 1 | package dbold 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // If the version is different from the database record, it will ask user for cleaning the database. 8 | const VERSION = "20201127" 9 | 10 | // Manager is a gorm model for database table `managers`. 11 | type Manager struct { 12 | gorm.Model 13 | 14 | Name string 15 | Password string `json:"-"` 16 | IsCheck bool 17 | Token string // For single sign-on 18 | } 19 | 20 | // Team is a gorm model for database table `teams`. 21 | type Team struct { 22 | gorm.Model 23 | 24 | Name string 25 | Password string `json:"-"` 26 | Logo string 27 | Score float64 28 | SecretKey string 29 | } 30 | 31 | // Token is a gorm model for database table `tokens`. 32 | // It used to store team token. 33 | type Token struct { 34 | gorm.Model 35 | 36 | TeamID uint 37 | Token string 38 | } 39 | 40 | // Challenge is a gorm model for database table `challenges`, used to store the challenges like Web1, Pwn1. 41 | type Challenge struct { 42 | gorm.Model 43 | Title string 44 | BaseScore int 45 | AutoRefreshFlag bool 46 | Command string 47 | } 48 | 49 | // DownAction is a gorm model for database table `down_actions`. 50 | type DownAction struct { 51 | gorm.Model 52 | 53 | TeamID uint 54 | ChallengeID uint 55 | GameBoxID uint 56 | Round int 57 | } 58 | 59 | // AttackAction is a gorm model for database table `attack_actions`. 60 | // Used to store the flag submitted record. 61 | type AttackAction struct { 62 | gorm.Model 63 | 64 | TeamID uint // Victim's team ID 65 | GameBoxID uint // Victim's gamebox ID 66 | ChallengeID uint // Victim's challenge ID 67 | AttackerTeamID uint // Attacker's Team ID 68 | Round int 69 | } 70 | 71 | // Flag is a gorm model for database table `flags`. 72 | // All the flags will be generated before the competition start and save in this table. 73 | type Flag struct { 74 | gorm.Model 75 | 76 | TeamID uint 77 | GameBoxID uint 78 | ChallengeID uint 79 | Round int 80 | Flag string 81 | } 82 | 83 | // GameBox is a gorm model for database table `gameboxes`. 84 | type GameBox struct { 85 | gorm.Model 86 | ChallengeID uint 87 | TeamID uint 88 | 89 | IP string 90 | Port string 91 | SSHPort string 92 | SSHUser string 93 | SSHPassword string 94 | Description string 95 | Visible bool 96 | Score float64 // The score can be negative. 97 | IsDown bool 98 | IsAttacked bool 99 | } 100 | 101 | // Score is a gorm model for database table `scores`. 102 | // Every action (checkdown, attacked...) will be created a score record, and the total score will be calculated by SUM(`score`). 103 | type Score struct { 104 | gorm.Model 105 | 106 | TeamID uint 107 | GameBoxID uint 108 | Round int 109 | Reason string 110 | Score float64 `gorm:"index"` 111 | } 112 | 113 | // Bulletin is a gorm model for database table `bulletins`. 114 | type Bulletin struct { 115 | gorm.Model 116 | 117 | Title string 118 | Content string 119 | } 120 | 121 | // BulletinRead gorm model, used to store the bulletin is read by a team. 122 | type BulletinRead struct { 123 | gorm.Model 124 | 125 | TeamID uint 126 | BulletinID uint 127 | } 128 | 129 | // Log is a gorm model for database table `logs`. 130 | type Log struct { 131 | gorm.Model 132 | 133 | Level int // 0 - Normal, 1 - Warning, 2 - Important 134 | Kind string 135 | Content string 136 | } 137 | 138 | // WebHook is a gorm model for database table `webhook`, used to store the webhook. 139 | type WebHook struct { 140 | gorm.Model 141 | 142 | URL string 143 | Type string 144 | Token string 145 | 146 | Retry int 147 | Timeout int 148 | } 149 | 150 | // DynamicConfig is the config which is stored in database. 151 | // So it's a GORM model for users can edit it anytime. 152 | type DynamicConfig struct { 153 | gorm.Model `json:"-"` 154 | 155 | Key string 156 | Value string 157 | Kind int8 158 | Options string 159 | } 160 | -------------------------------------------------------------------------------- /internal/dbold/mysql.go: -------------------------------------------------------------------------------- 1 | package dbold 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jinzhu/gorm" 7 | _ "github.com/jinzhu/gorm/dialects/mysql" 8 | log "unknwon.dev/clog/v2" 9 | 10 | "github.com/vidar-team/Cardinal/internal/conf" 11 | "github.com/vidar-team/Cardinal/internal/locales" 12 | ) 13 | 14 | var MySQL *gorm.DB 15 | 16 | func InitMySQL() { 17 | db, err := gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true&loc=Local&charset=utf8mb4,utf8", 18 | conf.Database.User, 19 | conf.Database.Password, 20 | conf.Database.Host, 21 | conf.Database.Name, 22 | )) 23 | 24 | if err != nil { 25 | log.Fatal("Failed to connect to mysql database: %v", err) 26 | } 27 | 28 | db.DB().SetMaxIdleConns(conf.Database.MaxIdleConns) 29 | db.DB().SetMaxOpenConns(conf.Database.MaxOpenConns) 30 | 31 | // Create tables. 32 | db.AutoMigrate( 33 | &Manager{}, 34 | &Challenge{}, 35 | &Token{}, 36 | &Team{}, 37 | &Bulletin{}, 38 | &BulletinRead{}, // Not used 39 | 40 | &AttackAction{}, 41 | &DownAction{}, 42 | &Score{}, 43 | &Flag{}, 44 | &GameBox{}, 45 | 46 | &Log{}, 47 | &WebHook{}, 48 | 49 | &DynamicConfig{}, 50 | ) 51 | 52 | MySQL = db 53 | 54 | // Test the database charset. 55 | if MySQL.Exec("SELECT * FROM `logs` WHERE `Content` = '中文测试';").Error != nil { 56 | log.Fatal(locales.T("general.database_charset_error")) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/dbutil/clock.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dbutil 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | func Now() time.Time { 12 | return time.Now().Truncate(time.Microsecond) 13 | } 14 | -------------------------------------------------------------------------------- /internal/dbutil/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dbutil 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "math/rand" 11 | "net/url" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "testing" 17 | "time" 18 | 19 | "gorm.io/driver/postgres" 20 | "gorm.io/gorm" 21 | ) 22 | 23 | var flagParseOnce sync.Once 24 | 25 | func NewTestDB(t *testing.T, migrationTables ...interface{}) (testDB *gorm.DB, cleanup func(...string) error) { 26 | dsn := os.ExpandEnv("postgres://$PGUSER:$PGPASSWORD@$PGHOST:$PGPORT/$PGDATABASE?sslmode=$PGSSLMODE") 27 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ 28 | NowFunc: func() time.Time { 29 | return Now() 30 | }, 31 | }) 32 | if err != nil { 33 | t.Fatalf("Failed to open connection: %v", err) 34 | } 35 | 36 | ctx := context.Background() 37 | rng := rand.New(rand.NewSource(time.Now().UnixNano())) 38 | dbname := "cardinal-test-" + strconv.FormatUint(rng.Uint64(), 10) 39 | 40 | err = db.WithContext(ctx).Exec(`CREATE DATABASE ` + QuoteIdentifier(dbname)).Error 41 | if err != nil { 42 | t.Fatalf("Failed to create test database: %v", err) 43 | } 44 | 45 | cfg, err := url.Parse(dsn) 46 | if err != nil { 47 | t.Fatalf("Failed to parse DSN: %v", err) 48 | } 49 | cfg.Path = "/" + dbname 50 | 51 | flagParseOnce.Do(flag.Parse) 52 | 53 | testDB, err = gorm.Open(postgres.Open(cfg.String()), &gorm.Config{ 54 | NowFunc: func() time.Time { 55 | return Now() 56 | }, 57 | }) 58 | if err != nil { 59 | t.Fatalf("Failed to open test connection: %v", err) 60 | } 61 | 62 | err = testDB.AutoMigrate(migrationTables...) 63 | if err != nil { 64 | t.Fatalf("Failed to auto migrate tables: %v", err) 65 | } 66 | 67 | t.Cleanup(func() { 68 | defer func() { 69 | if database, err := db.DB(); err == nil { 70 | _ = database.Close() 71 | } 72 | }() 73 | 74 | if t.Failed() { 75 | t.Logf("DATABASE %s left intact for inspection", dbname) 76 | return 77 | } 78 | 79 | database, err := testDB.DB() 80 | if err != nil { 81 | t.Fatalf("Failed to get currently open database: %v", err) 82 | } 83 | 84 | err = database.Close() 85 | if err != nil { 86 | t.Fatalf("Failed to close currently open database: %v", err) 87 | } 88 | 89 | err = db.WithContext(ctx).Exec(`DROP DATABASE ` + QuoteIdentifier(dbname)).Error 90 | if err != nil { 91 | t.Fatalf("Failed to drop test database: %v", err) 92 | } 93 | }) 94 | 95 | return testDB, func(tables ...string) error { 96 | if t.Failed() { 97 | return nil 98 | } 99 | 100 | for _, table := range tables { 101 | err := testDB.WithContext(ctx).Exec(`TRUNCATE TABLE ` + QuoteIdentifier(table) + ` RESTART IDENTITY CASCADE`).Error 102 | if err != nil { 103 | return err 104 | } 105 | } 106 | return nil 107 | } 108 | } 109 | 110 | // QuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to be 111 | // used as part of an SQL statement. 112 | func QuoteIdentifier(s string) string { 113 | return `"` + strings.ReplaceAll(s, `"`, `""`) + `"` 114 | } 115 | -------------------------------------------------------------------------------- /internal/dynamic_config/dynamic_config.go: -------------------------------------------------------------------------------- 1 | package dynamic_config 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/vidar-team/Cardinal/internal/dbold" 7 | "github.com/vidar-team/Cardinal/internal/locales" 8 | "github.com/vidar-team/Cardinal/internal/utils" 9 | ) 10 | 11 | func Init() { 12 | dbold.MySQL.Model(&dbold.DynamicConfig{}) 13 | 14 | initConfig(utils.DATBASE_VERSION, dbold.VERSION, utils.STRING) 15 | initConfig(utils.TITLE_CONF, "HCTF", utils.STRING) 16 | initConfig(utils.FLAG_PREFIX_CONF, "hctf{", utils.STRING) 17 | initConfig(utils.FLAG_SUFFIX_CONF, "}", utils.STRING) 18 | initConfig(utils.ANIMATE_ASTEROID, utils.BOOLEAN_FALSE, utils.BOOLEAN) 19 | initConfig(utils.SHOW_OTHERS_GAMEBOX, utils.BOOLEAN_FALSE, utils.BOOLEAN) 20 | initConfig(utils.DEFAULT_LANGUAGE, "zh-CN", utils.SELECT, "zh-CN|en-US") 21 | } 22 | 23 | // initConfig set the default value of the given key. 24 | // Always used in installation. 25 | func initConfig(key string, value string, kind int8, option ...string) { 26 | var opt string 27 | if len(option) != 0 { 28 | opt = option[0] 29 | } 30 | 31 | dbold.MySQL.Model(&dbold.DynamicConfig{}).FirstOrCreate(&dbold.DynamicConfig{ 32 | Key: key, 33 | Value: value, 34 | Kind: kind, 35 | Options: opt, 36 | }, "`key` = ?", key) 37 | } 38 | 39 | // Set update the config by insert a new record into database, for we can make a config version control soon. 40 | // Then refresh the config in struct. 41 | func Set(key string, value string) { 42 | if key == utils.DATBASE_VERSION { 43 | return 44 | } 45 | 46 | dbold.MySQL.Model(&dbold.DynamicConfig{}).Where("`key` = ?", key).Update(&dbold.DynamicConfig{ 47 | Key: key, 48 | Value: value, 49 | }) 50 | } 51 | 52 | // Get returns the config value. 53 | func Get(key string) string { 54 | var config dbold.DynamicConfig 55 | dbold.MySQL.Model(&dbold.DynamicConfig{}).Where("`key` = ?", key).Find(&config) 56 | return config.Value 57 | } 58 | 59 | // SetConfig is the HTTP handler used to set the config value. 60 | func SetConfig(c *gin.Context) (int, interface{}) { 61 | var inputForm []struct { 62 | Key string `binding:"required"` 63 | Value string `binding:"required"` 64 | } 65 | 66 | if err := c.BindJSON(&inputForm); err != nil { 67 | return utils.MakeErrJSON(400, 40046, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 68 | } 69 | 70 | for _, config := range inputForm { 71 | Set(config.Key, config.Value) 72 | } 73 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "config.update_success")) 74 | } 75 | 76 | // GetConfig is the HTTP handler used to return the config value of the given key. 77 | func GetConfig(c *gin.Context) (int, interface{}) { 78 | var inputForm struct { 79 | Key string `binding:"required"` 80 | } 81 | 82 | if err := c.BindJSON(&inputForm); err != nil { 83 | return utils.MakeErrJSON(400, 40046, locales.I18n.T(c.GetString("lang"), "general.error_payload")) 84 | } 85 | value := Get(inputForm.Key) 86 | return utils.MakeSuccessJSON(value) 87 | } 88 | 89 | // GetAllConfig is the HTTP handler used to return the all the configs. 90 | func GetAllConfig(c *gin.Context) (int, interface{}) { 91 | var config []dbold.DynamicConfig 92 | dbold.MySQL.Model(&dbold.DynamicConfig{}).Where("`key` != ?", utils.DATBASE_VERSION).Find(&config) 93 | return utils.MakeSuccessJSON(config) 94 | } 95 | -------------------------------------------------------------------------------- /internal/form/bulletin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type NewBulletin struct { 8 | Title string `validate:"required,lt=255"` 9 | Body string `validate:"required,lt=1000"` 10 | } 11 | 12 | type UpdateBulletin struct { 13 | ID uint `validate:"required"` 14 | Title string `validate:"required,lt=255"` 15 | Body string `validate:"required,lt=1000"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/form/challenge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type NewChallenge struct { 8 | Title string `validate:"required,lt=255"` 9 | BaseScore float64 `validate:"required,gte=0,lte=10000"` 10 | AutoRenewFlag bool 11 | RenewFlagCommand string 12 | } 13 | 14 | type UpdateChallenge struct { 15 | ID uint `validate:"required"` 16 | Title string `validate:"required,lt=255"` 17 | BaseScore float64 `validate:"required,gte=0,lte=10000"` 18 | AutoRenewFlag bool 19 | RenewFlagCommand string 20 | } 21 | 22 | type SetChallengeVisible struct { 23 | ID uint `binding:"Required"` 24 | Visible bool 25 | } 26 | -------------------------------------------------------------------------------- /internal/form/form.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/flamego/binding" 11 | "github.com/flamego/flamego" 12 | "github.com/flamego/validator" 13 | jsoniter "github.com/json-iterator/go" 14 | log "unknwon.dev/clog/v2" 15 | 16 | "github.com/vidar-team/Cardinal/internal/i18n" 17 | ) 18 | 19 | func Bind(model interface{}) flamego.Handler { 20 | validate := validator.New() 21 | 22 | return binding.JSON(model, binding.Options{ 23 | ErrorHandler: errorHandler(validate), 24 | Validator: validate, 25 | }) 26 | } 27 | 28 | func errorHandler(validate *validator.Validate) flamego.Handler { 29 | return func(c flamego.Context, errors binding.Errors, l *i18n.Locale) { 30 | c.ResponseWriter().WriteHeader(http.StatusBadRequest) 31 | c.ResponseWriter().Header().Set("Content-Type", "application/json") 32 | 33 | var errorCode int 34 | var msg string 35 | if errors[0].Category == binding.ErrorCategoryDeserialization { 36 | errorCode = 40000 37 | msg = l.T("general.error_payload") 38 | } else { 39 | errorCode = 40001 40 | switch v := errors[0].Err.(type) { 41 | case *validator.InvalidValidationError: 42 | // TODO 43 | log.Error(v.Error()) 44 | case validator.ValidationErrors: 45 | errs := errors[0].Err.(validator.ValidationErrors) 46 | err := errs[0] 47 | 48 | fieldName := l.T("form." + err.Namespace()) 49 | 50 | switch err.Tag() { 51 | case "required": 52 | msg = l.T("form.required_error", fieldName) 53 | case "len": 54 | msg = l.T("form.len_error", fieldName) 55 | default: 56 | msg = err.Error() 57 | } 58 | } 59 | 60 | } 61 | 62 | body := map[string]interface{}{ 63 | "error": errorCode, 64 | "msg": msg, 65 | } 66 | err := jsoniter.NewEncoder(c.ResponseWriter()).Encode(body) 67 | if err != nil { 68 | log.Error("Failed to encode response body: %v", err) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/form/game_box.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type NewGameBox []struct { 8 | ChallengeID uint `validate:"required,lt=255"` 9 | TeamID uint `validate:"required,lt=255"` 10 | IPAddress string `validate:"required,lt=255"` 11 | Port uint 12 | Description string 13 | InternalSSHPort uint 14 | InternalSSHUser string 15 | InternalSSHPassword string 16 | } 17 | 18 | type UpdateGameBox struct { 19 | ID uint `validate:"required,lt=255"` 20 | IPAddress string `validate:"required,lt=255"` 21 | Port uint 22 | Description string 23 | InternalSSHPort uint 24 | InternalSSHUser string 25 | InternalSSHPassword string 26 | } 27 | -------------------------------------------------------------------------------- /internal/form/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type ManagerLogin struct { 8 | Name string `validate:"required,lt=255"` 9 | Password string `validate:"required,lt=255"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/form/team.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package form 6 | 7 | type TeamLogin struct { 8 | Name string `validate:"required,lt=255"` 9 | Password string `validate:"required,lt=255"` 10 | } 11 | 12 | type NewTeam []struct { 13 | Name string `validate:"required,lt=255"` 14 | Logo string 15 | } 16 | 17 | type UpdateTeam struct { 18 | ID uint `validate:"required,lt=255"` 19 | Name string `validate:"required,lt=255"` 20 | Logo string 21 | } 22 | 23 | type SubmitFlag struct { 24 | Flag string `validate:"required"` 25 | } 26 | -------------------------------------------------------------------------------- /internal/game/bridge.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/vidar-team/Cardinal/internal/asteroid" 5 | "github.com/vidar-team/Cardinal/internal/dbold" 6 | "github.com/vidar-team/Cardinal/internal/dynamic_config" 7 | "github.com/vidar-team/Cardinal/internal/timer" 8 | "github.com/vidar-team/Cardinal/internal/utils" 9 | ) 10 | 11 | func AsteroidGreetData() (result asteroid.Greet) { 12 | var asteroidTeam []asteroid.Team 13 | var teams []dbold.Team 14 | dbold.MySQL.Model(&dbold.Team{}).Order("score DESC").Find(&teams) 15 | for rank, team := range teams { 16 | asteroidTeam = append(asteroidTeam, asteroid.Team{ 17 | Id: int(team.ID), 18 | Name: team.Name, 19 | Rank: rank + 1, 20 | Image: team.Logo, 21 | Score: int(team.Score), 22 | }) 23 | } 24 | 25 | result.Title = dynamic_config.Get(utils.TITLE_CONF) 26 | result.Team = asteroidTeam 27 | result.Time = timer.Get().RoundRemainTime 28 | result.Round = timer.Get().NowRound 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /internal/game/check.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/jinzhu/gorm" 6 | 7 | "github.com/vidar-team/Cardinal/internal/asteroid" 8 | "github.com/vidar-team/Cardinal/internal/dbold" 9 | "github.com/vidar-team/Cardinal/internal/livelog" 10 | "github.com/vidar-team/Cardinal/internal/locales" 11 | "github.com/vidar-team/Cardinal/internal/misc/webhook" 12 | "github.com/vidar-team/Cardinal/internal/timer" 13 | "github.com/vidar-team/Cardinal/internal/utils" 14 | ) 15 | 16 | // CheckDown is the gamebox check down handler for bots. 17 | func CheckDown(c *gin.Context) (int, interface{}) { 18 | // Check down is forbidden if the competition isn't start. 19 | if timer.Get().Status != "on" { 20 | return utils.MakeErrJSON(403, 40310, 21 | locales.I18n.T(c.GetString("lang"), "general.not_begin"), 22 | ) 23 | } 24 | 25 | type InputForm struct { 26 | GameBoxID uint `binding:"required"` 27 | } 28 | var inputForm InputForm 29 | err := c.BindJSON(&inputForm) 30 | if err != nil { 31 | return utils.MakeErrJSON(400, 40026, 32 | locales.I18n.T(c.GetString("lang"), "general.error_payload"), 33 | ) 34 | } 35 | 36 | // Does it check down one gamebox repeatedly in one round? 37 | var repeatCheck dbold.DownAction 38 | dbold.MySQL.Model(&dbold.DownAction{}).Where(&dbold.DownAction{ 39 | GameBoxID: inputForm.GameBoxID, 40 | Round: timer.Get().NowRound, 41 | }).Find(&repeatCheck) 42 | if repeatCheck.ID != 0 { 43 | return utils.MakeErrJSON(403, 40311, 44 | locales.I18n.T(c.GetString("lang"), "check.repeat"), 45 | ) 46 | } 47 | 48 | // Check the gamebox is existed or not. 49 | var gameBox dbold.GameBox 50 | dbold.MySQL.Model(&dbold.GameBox{}).Where(&dbold.GameBox{Model: gorm.Model{ID: inputForm.GameBoxID}}).Find(&gameBox) 51 | if gameBox.ID == 0 { 52 | return utils.MakeErrJSON(403, 40312, 53 | locales.I18n.T(c.GetString("lang"), "gamebox.not_found"), 54 | ) 55 | } 56 | if !gameBox.Visible { 57 | return utils.MakeErrJSON(403, 40314, 58 | locales.I18n.T(c.GetString("lang"), "check.not_visible"), 59 | ) 60 | } 61 | 62 | // No problem! Update the gamebox status to down. 63 | dbold.MySQL.Model(&dbold.GameBox{}).Where(&dbold.GameBox{Model: gorm.Model{ID: gameBox.ID}}).Update(&dbold.GameBox{IsDown: true}) 64 | 65 | tx := dbold.MySQL.Begin() 66 | if tx.Create(&dbold.DownAction{ 67 | TeamID: gameBox.TeamID, 68 | ChallengeID: gameBox.ChallengeID, 69 | GameBoxID: inputForm.GameBoxID, 70 | Round: timer.Get().NowRound, 71 | }).RowsAffected != 1 { 72 | tx.Rollback() 73 | return utils.MakeErrJSON(500, 50015, 74 | locales.I18n.T(c.GetString("lang"), "general.server_error"), 75 | ) 76 | } 77 | tx.Commit() 78 | 79 | // Check down hook 80 | go webhook.Add(webhook.CHECK_DOWN_HOOK, gin.H{"team": gameBox.TeamID, "gamebox": gameBox.ID}) 81 | 82 | // Update the gamebox status in ranking list. 83 | SetRankList() 84 | 85 | // Asteroid Unity3D 86 | asteroid.SendStatus(int(gameBox.TeamID), "down") 87 | 88 | var t dbold.Team 89 | dbold.MySQL.Model(&dbold.Team{}).Where(&dbold.Team{Model: gorm.Model{ID: gameBox.TeamID}}).Find(&t) 90 | var challenge dbold.Challenge 91 | dbold.MySQL.Model(&dbold.Challenge{}).Where(&dbold.Challenge{Model: gorm.Model{ID: gameBox.ChallengeID}}).Find(&challenge) 92 | // Live log 93 | _ = livelog.Stream.Write(livelog.GlobalStream, livelog.NewLine("check_down", 94 | gin.H{"Team": t.Name, "Challenge": challenge.Title})) 95 | 96 | return utils.MakeSuccessJSON(locales.I18n.T(c.GetString("lang"), "general.success")) 97 | } 98 | -------------------------------------------------------------------------------- /internal/game/rank.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/patrickmn/go-cache" 5 | 6 | "github.com/vidar-team/Cardinal/internal/dbold" 7 | "github.com/vidar-team/Cardinal/internal/locales" 8 | "github.com/vidar-team/Cardinal/internal/logger" 9 | "github.com/vidar-team/Cardinal/internal/store" 10 | ) 11 | 12 | // RankItem is used to create the ranking list. 13 | type RankItem struct { 14 | TeamID uint 15 | TeamName string 16 | TeamLogo string 17 | Score float64 18 | GameBoxStatus interface{} // Ordered by challenge ID. 19 | } 20 | 21 | // GameBoxInfo contains the gamebox info which for manager. 22 | // Manager can get the gamebox's score. 23 | type GameBoxInfo struct { 24 | Score float64 25 | IsAttacked bool 26 | IsDown bool 27 | } 28 | 29 | // GameBoxStatus contains the gamebox info which for team. 30 | type GameBoxStatus struct { 31 | IsAttacked bool 32 | IsDown bool 33 | } 34 | 35 | // GetRankList returns the ranking list data for team from the cache. 36 | func GetRankList() []*RankItem { 37 | rankList, ok := store.Get("rankList") 38 | if !ok { 39 | return []*RankItem{} 40 | } 41 | return rankList.([]*RankItem) 42 | } 43 | 44 | // GetManagerRankList returns the ranking list data for manager from the cache. 45 | func GetManagerRankList() []*RankItem { 46 | rankList, ok := store.Get("rankManagerList") 47 | if !ok { 48 | return []*RankItem{} 49 | } 50 | return rankList.([]*RankItem) 51 | } 52 | 53 | // GetRankListTitle returns the ranking list table header from the cache. 54 | func GetRankListTitle() []string { 55 | rankListTitle, ok := store.Get("rankListTitle") 56 | if !ok { 57 | return []string{} 58 | } 59 | return rankListTitle.([]string) 60 | } 61 | 62 | // SetRankListTitle will save the visible challenges' headers into cache. 63 | func SetRankListTitle() { 64 | var result []struct { 65 | Title string `gorm:"Column:Title"` 66 | } 67 | dbold.MySQL.Raw("SELECT `challenges`.`Title` FROM `challenges` WHERE `challenges`.`id` IN " + 68 | "(SELECT DISTINCT challenge_id FROM `game_boxes` WHERE `visible` = 1 AND `deleted_at` IS NULL) " + // DISTINCT get all the visible challenge IDs and remove duplicate data 69 | "AND `deleted_at` IS NULL ORDER BY `challenges`.`id`").Scan(&result) 70 | 71 | visibleChallengeTitle := make([]string, len(result)) 72 | for index, res := range result { 73 | visibleChallengeTitle[index] = res.Title 74 | } 75 | store.Set("rankListTitle", visibleChallengeTitle, cache.NoExpiration) // Save challenge title into cache. 76 | 77 | logger.New(logger.NORMAL, "system", locales.T("log.rank_list_success")) 78 | } 79 | 80 | // SetRankList will calculate the ranking list. 81 | func SetRankList() { 82 | var rankList []*RankItem 83 | var managerRankList []*RankItem 84 | 85 | var teams []dbold.Team 86 | dbold.MySQL.Model(&dbold.Team{}).Order("score DESC").Find(&teams) // Ordered by the team score. 87 | for _, team := range teams { 88 | var gameboxes []dbold.GameBox 89 | // Get the challenge data ordered by the challenge ID, to make sure the table header can match with the score correctly. 90 | dbold.MySQL.Model(&dbold.GameBox{}).Where(&dbold.GameBox{TeamID: team.ID, Visible: true}).Order("challenge_id").Find(&gameboxes) 91 | var gameBoxInfo []*GameBoxInfo // Gamebox info for manager. 92 | var gameBoxStatuses []*GameBoxStatus // Gamebox info for users and public. 93 | 94 | for _, gamebox := range gameboxes { 95 | gameBoxStatuses = append(gameBoxStatuses, &GameBoxStatus{ 96 | IsAttacked: gamebox.IsAttacked, 97 | IsDown: gamebox.IsDown, 98 | }) 99 | 100 | gameBoxInfo = append(gameBoxInfo, &GameBoxInfo{ 101 | Score: gamebox.Score, 102 | IsAttacked: gamebox.IsAttacked, 103 | IsDown: gamebox.IsDown, 104 | }) 105 | } 106 | 107 | rankList = append(rankList, &RankItem{ 108 | TeamID: team.ID, 109 | TeamName: team.Name, 110 | TeamLogo: team.Logo, 111 | Score: team.Score, 112 | GameBoxStatus: gameBoxStatuses, 113 | }) 114 | managerRankList = append(managerRankList, &RankItem{ 115 | TeamID: team.ID, 116 | TeamName: team.Name, 117 | TeamLogo: team.Logo, 118 | Score: team.Score, 119 | GameBoxStatus: gameBoxInfo, 120 | }) 121 | } 122 | 123 | // Save the ranking list for public into cache. 124 | store.Set("rankList", rankList, cache.NoExpiration) 125 | // Save the ranking list for manager into cache. 126 | store.Set("rankManagerList", managerRankList, cache.NoExpiration) 127 | } 128 | -------------------------------------------------------------------------------- /internal/healthy/healthy.go: -------------------------------------------------------------------------------- 1 | package healthy 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | 8 | "github.com/vidar-team/Cardinal/internal/conf" 9 | "github.com/vidar-team/Cardinal/internal/dbold" 10 | "github.com/vidar-team/Cardinal/internal/locales" 11 | "github.com/vidar-team/Cardinal/internal/logger" 12 | "github.com/vidar-team/Cardinal/internal/timer" 13 | ) 14 | 15 | // HealthyCheck will be used to check whether Cardinal runs normally. 16 | func HealthyCheck() { 17 | var teamCount int 18 | dbold.MySQL.Model(&dbold.Team{}).Count(&teamCount) 19 | 20 | previousRoundScore := PreviousRoundScore() 21 | if math.Abs(previousRoundScore) != 0 { 22 | // If the previous round total score is not equal zero, maybe all the teams were checked down. 23 | if previousRoundScore != float64(-conf.Game.CheckDownScore*teamCount) { 24 | // Maybe there are some mistakes in previous round score. 25 | logger.New(logger.IMPORTANT, "healthy_check", locales.T("healthy.previous_round_non_zero_error")) 26 | } 27 | } 28 | 29 | totalScore := TotalScore() 30 | if math.Abs(totalScore) != 0 { 31 | // If sum all the scores but it is not equal zero, maybe all the teams were checked down in some rounds. 32 | if int(totalScore)%(conf.Game.CheckDownScore*teamCount) != 0 { 33 | // Maybes there are some mistakes. 34 | logger.New(logger.IMPORTANT, "healthy_check", locales.T("healthy.total_score_non_zero_error")) 35 | } 36 | } 37 | } 38 | 39 | // PreviousRoundScore returns the previous round's score count. 40 | func PreviousRoundScore() float64 { 41 | var score []float64 42 | // Pay attention if there is no action in the previous round, the SUM(`score`) will be NULL. 43 | dbold.MySQL.Model(&dbold.Score{}).Where(&dbold.Score{Round: timer.Get().NowRound}).Pluck("IFNULL(SUM(`score`), 0)", &score) 44 | value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", score[0]), 64) 45 | return value 46 | } 47 | 48 | // TotalScore returns all the rounds' score count. 49 | func TotalScore() float64 { 50 | var score []float64 51 | // Pay attention in the first round, the SUM(`score`) is NULL. 52 | dbold.MySQL.Model(&dbold.Score{}).Pluck("IFNULL(SUM(`score`), 0)", &score) 53 | value, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", score[0]), 64) 54 | return value 55 | } 56 | -------------------------------------------------------------------------------- /internal/healthy/panel.go: -------------------------------------------------------------------------------- 1 | package healthy 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/vidar-team/Cardinal/internal/dbold" 8 | "github.com/vidar-team/Cardinal/internal/utils" 9 | ) 10 | 11 | // Panel returns the system runtime status, which is used in backstage data panel. 12 | func Panel(c *gin.Context) (int, interface{}) { 13 | var submitFlag int 14 | dbold.MySQL.Model(&dbold.AttackAction{}).Count(&submitFlag) 15 | 16 | var checkDown int 17 | dbold.MySQL.Model(&dbold.DownAction{}).Count(&checkDown) 18 | 19 | m := new(runtime.MemStats) 20 | runtime.ReadMemStats(m) 21 | return utils.MakeSuccessJSON(gin.H{ 22 | "SubmitFlag": submitFlag, 23 | "CheckDown": checkDown, 24 | "NumGoroutine": runtime.NumGoroutine(), // Goroutine number 25 | "MemAllocated": utils.FileSize(int64(m.Alloc)), // Allocated memory 26 | "TotalScore": TotalScore(), 27 | "PreviousRoundScore": PreviousRoundScore(), 28 | "Version": utils.VERSION, 29 | "CommitSHA": utils.COMMIT_SHA, 30 | "BuildTime": utils.BUILD_TIME, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/i18n/i18n.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package i18n 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/flamego/flamego" 11 | "github.com/qor/i18n" 12 | "github.com/qor/i18n/backends/yaml" 13 | "golang.org/x/text/language" 14 | 15 | "github.com/vidar-team/Cardinal/internal/context" 16 | "github.com/vidar-team/Cardinal/locales" 17 | ) 18 | 19 | type Locale struct { 20 | Tag language.Tag 21 | *i18n.I18n 22 | } 23 | 24 | func (l *Locale) T(key string, args ...interface{}) string { 25 | return string(l.I18n.T(l.Tag.String(), key, args...)) 26 | } 27 | 28 | func I18n() flamego.Handler { 29 | yamlBackend := yaml.NewWithFilesystem(http.FS(locales.FS)) 30 | translations := yamlBackend.LoadTranslations() 31 | i18n := i18n.New(yamlBackend) 32 | 33 | tags := make([]language.Tag, 0) 34 | for _, tr := range translations { 35 | tags = append(tags, language.Raw.Make(tr.Locale)) 36 | } 37 | matcher := language.NewMatcher(tags) 38 | 39 | return func(ctx context.Context) { 40 | acceptLanguages := ctx.Request().Header.Get("Accept-Language") 41 | tags, _, _ := language.ParseAcceptLanguage(acceptLanguages) 42 | tag, _, _ := matcher.Match(tags...) 43 | 44 | locale := &Locale{ 45 | Tag: tag, 46 | I18n: i18n, 47 | } 48 | ctx.Map(locale) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/install/install_util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package install 6 | 7 | import ( 8 | "strconv" 9 | "time" 10 | 11 | "github.com/manifoldco/promptui" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | var DateTimeLayout = "2006-01-02 15:04:05" 16 | 17 | func inputDateTime(label string, validateFunc ...func(time.Time) error) (time.Time, error) { 18 | validate := func(time.Time) error { 19 | return nil 20 | } 21 | if len(validateFunc) != 0 { 22 | validate = validateFunc[0] 23 | } 24 | 25 | prompt := promptui.Prompt{ 26 | Label: label, 27 | Validate: func(s string) error { 28 | t, err := time.Parse(DateTimeLayout, s) 29 | if err != nil { 30 | return errors.Wrap(err, "parse time") 31 | } 32 | return validate(t) 33 | }, 34 | Default: time.Now().Format(DateTimeLayout), 35 | } 36 | 37 | dateResult, err := prompt.Run() 38 | if err != nil { 39 | return time.Time{}, errors.Wrap(err, "run prompt") 40 | } 41 | 42 | t, err := time.Parse(DateTimeLayout, dateResult) 43 | if err != nil { 44 | return time.Time{}, errors.Wrap(err, "parse time") 45 | } 46 | return t, nil 47 | } 48 | 49 | func inputInt(label string, defaultValue int, validateFunc ...func(int) error) (int, error) { 50 | validate := func(int) error { 51 | return nil 52 | } 53 | if len(validateFunc) != 0 { 54 | validate = validateFunc[0] 55 | } 56 | 57 | prompt := promptui.Prompt{ 58 | Label: label, 59 | Validate: func(s string) error { 60 | val, err := strconv.Atoi(s) 61 | if err != nil { 62 | return errors.Wrap(err, "convert") 63 | } 64 | return validate(val) 65 | }, 66 | Default: strconv.Itoa(defaultValue), 67 | } 68 | 69 | intResult, err := prompt.Run() 70 | if err != nil { 71 | return 0, errors.Wrap(err, "run prompt") 72 | } 73 | 74 | val, err := strconv.Atoi(intResult) 75 | if err != nil { 76 | return 0, errors.Wrap(err, "convert") 77 | } 78 | 79 | return val, nil 80 | } 81 | 82 | func inputConfirm(label string, defaultVal ...bool) (bool, error) { 83 | var defaultValue bool 84 | if len(defaultVal) != 0 { 85 | defaultValue = defaultVal[0] 86 | } 87 | 88 | prompt := promptui.Prompt{ 89 | Label: label, 90 | IsConfirm: true, 91 | Default: func(val bool) string { 92 | if val { 93 | return "y" 94 | } 95 | return "N" 96 | }(defaultValue), 97 | } 98 | 99 | _, err := prompt.Run() 100 | if err != nil { 101 | // FYI: https://github.com/manifoldco/promptui/issues/81 102 | if err == promptui.ErrAbort { 103 | return false, nil 104 | } 105 | return false, errors.Wrap(err, "run prompt") 106 | } 107 | return true, nil 108 | } 109 | 110 | func inputString(label, defaultValue string, validateFunc ...func(string) error) (string, error) { 111 | validate := func(string) error { 112 | return nil 113 | } 114 | if len(validateFunc) != 0 { 115 | validate = validateFunc[0] 116 | } 117 | 118 | prompt := promptui.Prompt{ 119 | Label: label, 120 | Validate: validate, 121 | Default: defaultValue, 122 | } 123 | 124 | result, err := prompt.Run() 125 | if err != nil { 126 | return "", errors.Wrap(err, "run prompt") 127 | } 128 | return result, nil 129 | } 130 | 131 | func inputPassword(label string, validateFunc ...func(string) error) (string, error) { 132 | validate := func(string) error { 133 | return nil 134 | } 135 | if len(validateFunc) != 0 { 136 | validate = validateFunc[0] 137 | } 138 | 139 | prompt := promptui.Prompt{ 140 | Label: label, 141 | Validate: validate, 142 | Mask: '*', 143 | } 144 | 145 | result, err := prompt.Run() 146 | if err != nil { 147 | return "", errors.Wrap(err, "run prompt") 148 | } 149 | return result, nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/livelog/handler.go: -------------------------------------------------------------------------------- 1 | package livelog 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | const GlobalStream = 0 14 | const pingInterval = time.Second * 30 15 | 16 | var Stream *Streamer 17 | 18 | func Init() { 19 | Stream = newStreamer() 20 | 21 | // Create global stream. 22 | _ = Stream.Create(GlobalStream) 23 | } 24 | 25 | func GlobalStreamHandler(c *gin.Context) { 26 | c.Header("Content-Type", "text/event-stream") 27 | c.Header("Cache-Control", "no-cache") 28 | c.Header("Connection", "keep-alive") 29 | c.Header("X-Accel-Buffering", "no") 30 | 31 | f, ok := c.Writer.(http.Flusher) 32 | if !ok { 33 | return 34 | } 35 | 36 | _, _ = io.WriteString(c.Writer, ": ping\n\n") 37 | f.Flush() 38 | 39 | ctx, cancel := context.WithCancel(c) 40 | defer cancel() 41 | events, errC := Stream.Tail(ctx, GlobalStream) 42 | _, _ = io.WriteString(c.Writer, "events: stream opened\n\n") 43 | f.Flush() 44 | 45 | L: 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | _, _ = io.WriteString(c.Writer, "events: stream cancelled\n\n") 50 | f.Flush() 51 | break L 52 | case <-errC: 53 | _, _ = io.WriteString(c.Writer, "events: stream error\n\n") 54 | f.Flush() 55 | break L 56 | case <-time.After(time.Hour): 57 | _, _ = io.WriteString(c.Writer, "events: stream timeout\n\n") 58 | f.Flush() 59 | break L 60 | case <-time.After(pingInterval): 61 | _, _ = io.WriteString(c.Writer, ": ping\n\n") 62 | f.Flush() 63 | case event := <-events: 64 | _, _ = io.WriteString(c.Writer, "data: ") 65 | evt, _ := json.Marshal(event) 66 | _, _ = c.Writer.Write(evt) 67 | _, _ = io.WriteString(c.Writer, "\n\n") 68 | f.Flush() 69 | } 70 | } 71 | 72 | _, _ = io.WriteString(c.Writer, "event: error\ndata: eof\n\n") 73 | f.Flush() 74 | _, _ = io.WriteString(c.Writer, "events: stream closed") 75 | f.Flush() 76 | } 77 | -------------------------------------------------------------------------------- /internal/livelog/livelog.go: -------------------------------------------------------------------------------- 1 | package livelog 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | ) 8 | 9 | type Streamer struct { 10 | sync.Mutex 11 | 12 | streams map[int64]*stream 13 | } 14 | 15 | var errStreamNotFound = errors.New("stream: not found") 16 | 17 | // newStreamer returns a new in-memory log streamer. 18 | func newStreamer() *Streamer { 19 | return &Streamer{ 20 | streams: make(map[int64]*stream), 21 | } 22 | } 23 | 24 | // Create adds a new log stream. 25 | func (s *Streamer) Create(id int64) error { 26 | s.Lock() 27 | s.streams[id] = newStream() 28 | s.Unlock() 29 | return nil 30 | } 31 | 32 | // Delete removes a log by id. 33 | func (s *Streamer) Delete(id int64) error { 34 | s.Lock() 35 | stream, ok := s.streams[id] 36 | if ok { 37 | delete(s.streams, id) 38 | } 39 | s.Unlock() 40 | if !ok { 41 | return errStreamNotFound 42 | } 43 | return stream.close() 44 | } 45 | 46 | // Write adds a new line into stream. 47 | func (s *Streamer) Write(id int64, line *Line) error { 48 | s.Lock() 49 | stream, ok := s.streams[id] 50 | s.Unlock() 51 | if !ok { 52 | return errStreamNotFound 53 | } 54 | return stream.write(line) 55 | } 56 | 57 | // Tail returns the end signal. 58 | func (s *Streamer) Tail(ctx context.Context, id int64) (<-chan *Line, <-chan error) { 59 | s.Lock() 60 | stream, ok := s.streams[id] 61 | s.Unlock() 62 | if !ok { 63 | return nil, nil 64 | } 65 | return stream.subscribe(ctx) 66 | } 67 | 68 | // Info returns the count of subscribers in each stream. 69 | func (s *Streamer) Info() map[int64]int { 70 | s.Lock() 71 | defer s.Unlock() 72 | info := map[int64]int{} 73 | for id, stream := range s.streams { 74 | stream.Lock() 75 | info[id] = len(stream.sub) 76 | stream.Unlock() 77 | } 78 | return info 79 | } 80 | -------------------------------------------------------------------------------- /internal/livelog/stream.go: -------------------------------------------------------------------------------- 1 | package livelog 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // The max size that the content can be. 10 | const bufferSize = 5000 11 | 12 | // Line is a single line of the log. 13 | type Line struct { 14 | Type string `json:"Type"` 15 | Message interface{} `json:"Message"` 16 | Timestamp int64 `json:"Time"` 17 | } 18 | 19 | // NewLine creates a line. 20 | func NewLine(messageType string, message interface{}) *Line { 21 | return &Line{ 22 | Type: messageType, 23 | Message: message, 24 | Timestamp: time.Now().Unix(), 25 | } 26 | } 27 | 28 | type stream struct { 29 | sync.Mutex 30 | 31 | content []*Line 32 | sub map[*subscriber]struct{} 33 | } 34 | 35 | func newStream() *stream { 36 | return &stream{ 37 | sub: map[*subscriber]struct{}{}, 38 | } 39 | } 40 | 41 | func (s *stream) write(line *Line) error { 42 | s.Lock() 43 | defer s.Unlock() 44 | for su := range s.sub { 45 | su.send(line) 46 | } 47 | 48 | if size := len(s.content); size >= bufferSize { 49 | s.content = s.content[size-bufferSize:] 50 | } 51 | return nil 52 | } 53 | 54 | func (s *stream) subscribe(ctx context.Context) (<-chan *Line, <-chan error) { 55 | sub := &subscriber{ 56 | handler: make(chan *Line, bufferSize), 57 | closeChannel: make(chan struct{}), 58 | } 59 | err := make(chan error) 60 | 61 | s.Lock() 62 | // Send history data. 63 | for _, line := range s.content { 64 | sub.send(line) 65 | } 66 | s.sub[sub] = struct{}{} 67 | s.Unlock() 68 | 69 | go func() { 70 | defer close(err) 71 | select { 72 | case <-sub.closeChannel: 73 | case <-ctx.Done(): 74 | sub.close() 75 | } 76 | }() 77 | return sub.handler, err 78 | } 79 | 80 | func (s *stream) close() error { 81 | s.Lock() 82 | defer s.Unlock() 83 | for sub := range s.sub { 84 | delete(s.sub, sub) 85 | sub.close() 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/livelog/subscriber.go: -------------------------------------------------------------------------------- 1 | package livelog 2 | 3 | import "sync" 4 | 5 | type subscriber struct { 6 | sync.Mutex 7 | 8 | handler chan *Line 9 | closeChannel chan struct{} 10 | closed bool 11 | } 12 | 13 | func (s *subscriber) send(line *Line) { 14 | select { 15 | case <-s.closeChannel: 16 | case s.handler <- line: 17 | default: 18 | 19 | } 20 | } 21 | 22 | func (s *subscriber) close() { 23 | s.Lock() 24 | if !s.closed { 25 | close(s.closeChannel) 26 | s.closed = true 27 | } 28 | s.Unlock() 29 | } 30 | -------------------------------------------------------------------------------- /internal/locales/i18n.go: -------------------------------------------------------------------------------- 1 | package locales 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/qor/i18n" 6 | "github.com/qor/i18n/backends/yaml" 7 | "golang.org/x/text/language" 8 | 9 | "github.com/vidar-team/Cardinal/internal/conf" 10 | ) 11 | 12 | // I18n is the i18n constant. 13 | var I18n *i18n.I18n 14 | 15 | func init() { 16 | I18n = i18n.New( 17 | yaml.New("./locales"), 18 | ) 19 | } 20 | 21 | // T returns the translation of the given key in the default language. 22 | func T(key string, args ...interface{}) string { 23 | return string(I18n.T(conf.App.Language, key, args...)) 24 | } 25 | 26 | // Middleware is an i18n middleware. Get client language from Accept-Language header. 27 | func Middleware() gin.HandlerFunc { 28 | return func(c *gin.Context) { 29 | acceptLanguages := c.GetHeader("Accept-Language") 30 | languages, _, err := language.ParseAcceptLanguage(acceptLanguages) 31 | if err != nil || len(languages) == 0 { 32 | // Set en-US as default language. 33 | c.Set("lang", "en-US") 34 | c.Next() 35 | return 36 | } 37 | 38 | // Only get the first language, ignore the rest. 39 | c.Set("lang", languages[0].String()) 40 | c.Next() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /internal/logger/log.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/vidar-team/Cardinal/internal/dbold" 6 | "github.com/vidar-team/Cardinal/internal/utils" 7 | ) 8 | 9 | // Log levels 10 | const ( 11 | NORMAL = iota 12 | WARNING 13 | IMPORTANT 14 | ) 15 | 16 | // New create a new log record in database. 17 | func New(level int, kind string, content string) { 18 | dbold.MySQL.Create(&dbold.Log{ 19 | Level: level, 20 | Kind: kind, 21 | Content: content, 22 | }) 23 | } 24 | 25 | // GetLogs returns the latest 30 logs. 26 | func GetLogs(c *gin.Context) (int, interface{}) { 27 | var logs []dbold.Log 28 | dbold.MySQL.Model(&dbold.Log{}).Order("`id` DESC").Limit(30).Find(&logs) 29 | return utils.MakeSuccessJSON(logs) 30 | } 31 | -------------------------------------------------------------------------------- /internal/misc/sentry.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/getsentry/sentry-go" 7 | 8 | "github.com/vidar-team/Cardinal/internal/conf" 9 | "github.com/vidar-team/Cardinal/internal/utils" 10 | ) 11 | 12 | const sentryDSN = "https://08a91604e4c9434ab6fdc6369ee577d7@o424435.ingest.sentry.io/5356242" 13 | 14 | func Sentry() { 15 | cardinalVersion := utils.VERSION 16 | cardinalCommitSHA := utils.COMMIT_SHA 17 | 18 | if !conf.App.EnableSentry { 19 | return 20 | } 21 | 22 | sentry.ConfigureScope(func(scope *sentry.Scope) { 23 | scope.SetUser(sentry.User{IPAddress: "{{auto}}"}) 24 | }) 25 | 26 | if err := sentry.Init(sentry.ClientOptions{ 27 | Dsn: sentryDSN, 28 | BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { 29 | event.Tags["cardinal_version"] = cardinalVersion 30 | event.Release = cardinalCommitSHA 31 | return event 32 | }, 33 | }); err != nil { 34 | fmt.Printf("Sentry initialization failed: %v\n", err) 35 | } 36 | 37 | // greeting 38 | sentry.CaptureMessage("Hello " + cardinalVersion) 39 | } 40 | -------------------------------------------------------------------------------- /internal/misc/version.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | log "unknwon.dev/clog/v2" 8 | 9 | "github.com/vidar-team/Cardinal/internal/dbold" 10 | "github.com/vidar-team/Cardinal/internal/dynamic_config" 11 | "github.com/vidar-team/Cardinal/internal/locales" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/parnurzeal/gorequest" 15 | 16 | "github.com/vidar-team/Cardinal/internal/utils" 17 | ) 18 | 19 | const GITHUB_RELEASE_API = "https://api.github.com/repos/vidar-team/Cardinal/releases/latest" 20 | 21 | func CheckVersion() { 22 | // Check Cardinal version. 23 | resp, body, _ := gorequest.New().Get(GITHUB_RELEASE_API).Timeout(5 * time.Second).End() 24 | if resp != nil && resp.StatusCode == 200 { 25 | type releaseApiJson struct { 26 | Name string `json:"name"` 27 | NodeID string `json:"node_id"` 28 | PublishedAt string `json:"published_at"` 29 | TagName string `json:"tag_name"` 30 | } 31 | 32 | var releaseData releaseApiJson 33 | err := json.Unmarshal([]byte(body), &releaseData) 34 | if err == nil { 35 | // Compare version. 36 | if !utils.CompareVersion(utils.VERSION, releaseData.TagName) { 37 | log.Info(locales.T("misc.version_out_of_date", gin.H{ 38 | "currentVersion": utils.VERSION, 39 | "latestVersion": releaseData.TagName, 40 | })) 41 | } 42 | } 43 | } 44 | } 45 | 46 | // CheckDatabaseVersion compares the database version in the dynamic_config with now version. 47 | // It will show a alert if database need update. 48 | func CheckDatabaseVersion() { 49 | databaseVersion := dynamic_config.Get(utils.DATBASE_VERSION) 50 | if databaseVersion != dbold.VERSION { 51 | log.Warn(locales.T("misc.database_version_out_of_date")) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/rank/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rank 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/vidar-team/Cardinal/internal/db" 13 | "github.com/vidar-team/Cardinal/internal/store" 14 | ) 15 | 16 | const ( 17 | CacheKeyRankForTeam = "rankForTeam" 18 | CacheKeyRankForManager = "rankForManager" 19 | CacheKeyRankTitle = "rankTitle" 20 | ) 21 | 22 | // ForTeam returns ranking list for team account from the cache. 23 | // It only contains the challenges which are visible to teams. 24 | func ForTeam() []*db.RankItem { 25 | rankList, ok := store.Get(CacheKeyRankForTeam) 26 | if !ok { 27 | return []*db.RankItem{} 28 | } 29 | return rankList.([]*db.RankItem) 30 | } 31 | 32 | // ForManager returns ranking list for team account from the cache. 33 | // It contains all the challenges. 34 | func ForManager() []*db.RankItem { 35 | rankList, ok := store.Get(CacheKeyRankForManager) 36 | if !ok { 37 | return []*db.RankItem{} 38 | } 39 | return rankList.([]*db.RankItem) 40 | } 41 | 42 | // Title returns the ranking list table header from the cache. 43 | func Title() []string { 44 | title, ok := store.Get(CacheKeyRankTitle) 45 | if !ok { 46 | return []string{} 47 | } 48 | return title.([]string) 49 | } 50 | 51 | // SetTitle saves the visible challenges' headers into cache. 52 | func SetTitle(ctx context.Context) error { 53 | titles, err := db.Ranks.VisibleChallengeTitle(ctx) 54 | if err != nil { 55 | return errors.Wrap(err, "get visible challenge title") 56 | } 57 | store.Set(CacheKeyRankTitle, titles) 58 | return nil 59 | } 60 | 61 | // SetRankList calculates the ranking list for teams and managers. 62 | func SetRankList(ctx context.Context) error { 63 | rankList, err := db.Ranks.List(ctx) 64 | if err != nil { 65 | return errors.Wrap(err, "get rank list") 66 | } 67 | store.Set(CacheKeyRankForManager, rankList) 68 | 69 | // Team accounts can't get the score of the game boxes. 70 | for _, rankItem := range rankList { 71 | for _, gameBox := range rankItem.GameBoxes { 72 | gameBox.Score = 0 73 | } 74 | } 75 | store.Set(CacheKeyRankForTeam, rankList) 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/route/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "github.com/flamego/session" 9 | log "unknwon.dev/clog/v2" 10 | 11 | "github.com/vidar-team/Cardinal/internal/context" 12 | "github.com/vidar-team/Cardinal/internal/db" 13 | "github.com/vidar-team/Cardinal/internal/form" 14 | ) 15 | 16 | // AuthHandler is the authenticate request handler. 17 | type AuthHandler struct{} 18 | 19 | // NewAuthHandler creates and returns a new authenticate Handler. 20 | func NewAuthHandler() *AuthHandler { 21 | return &AuthHandler{} 22 | } 23 | 24 | const teamIDSessionKey = "TeamID" 25 | const managerIDSessionKey = "ManagerID" 26 | 27 | func (*AuthHandler) TeamAuthenticator(ctx context.Context, session session.Session) error { 28 | teamID, ok := session.Get(teamIDSessionKey).(uint) 29 | if !ok { 30 | return ctx.Error(40300, "team authenticate error") 31 | } 32 | 33 | team, err := db.Teams.GetByID(ctx.Request().Context(), teamID) 34 | if err != nil { 35 | if err == db.ErrTeamNotExists { 36 | return ctx.Error(40300, "") 37 | } 38 | 39 | log.Error("Failed to get team by ID: %v", err) 40 | return ctx.ServerError() 41 | } 42 | 43 | ctx.Map(team) 44 | return nil 45 | } 46 | 47 | func (*AuthHandler) TeamTokenAuthenticator(ctx context.Context) error { 48 | token := ctx.Query("token") 49 | team, err := db.Teams.GetByToken(ctx.Request().Context(), token) 50 | if err != nil { 51 | if err == db.ErrTeamNotExists { 52 | return ctx.Error(40300, "") 53 | } 54 | 55 | log.Error("Failed to get team by token: %v", err) 56 | return ctx.ServerError() 57 | } 58 | 59 | ctx.Map(team) 60 | return nil 61 | } 62 | 63 | func (*AuthHandler) TeamLogin(ctx context.Context, session session.Session, f form.TeamLogin) error { 64 | team, err := db.Teams.Authenticate(ctx.Request().Context(), f.Name, f.Password) 65 | if err == db.ErrBadCredentials { 66 | return ctx.Error(40300, "bad credentials") 67 | } else if err != nil { 68 | log.Error("Failed to authenticate team: %v", err) 69 | return ctx.Error(50000, "") 70 | } 71 | 72 | session.Set(teamIDSessionKey, team.ID) 73 | return ctx.Success(session.ID()) 74 | } 75 | 76 | func (*AuthHandler) TeamLogout(ctx context.Context, session session.Session) error { 77 | session.Delete(teamIDSessionKey) 78 | return ctx.Success() 79 | } 80 | 81 | func (*AuthHandler) ManagerAuthenticator(ctx context.Context, session session.Session) error { 82 | managerID, ok := session.Get(managerIDSessionKey).(uint) 83 | if !ok { 84 | return ctx.Error(40300, "manager authenticate error") 85 | } 86 | 87 | manager, err := db.Managers.GetByID(ctx.Request().Context(), managerID) 88 | if err != nil { 89 | if err == db.ErrManagerNotExists { 90 | return ctx.Error(40300, "") 91 | } 92 | 93 | log.Error("Failed to get manager: %v", err) 94 | return ctx.ServerError() 95 | } 96 | 97 | ctx.Map(manager) 98 | return nil 99 | } 100 | 101 | func (*AuthHandler) ManagerLogin(ctx context.Context, session session.Session, f form.ManagerLogin) error { 102 | manager, err := db.Managers.Authenticate(ctx.Request().Context(), f.Name, f.Password) 103 | if err == db.ErrBadCredentials { 104 | return ctx.Error(40300, "bad credentials") 105 | } else if err != nil { 106 | log.Error("Failed to authenticate manager: %v", err) 107 | return ctx.Error(50000, "") 108 | } 109 | 110 | session.Set(managerIDSessionKey, manager.ID) 111 | return ctx.Success(session.ID()) 112 | } 113 | 114 | func (*AuthHandler) ManagerLogout(ctx context.Context, session session.Session) error { 115 | session.Delete(managerIDSessionKey) 116 | return ctx.Success() 117 | } 118 | -------------------------------------------------------------------------------- /internal/route/bulletin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | log "unknwon.dev/clog/v2" 9 | 10 | "github.com/vidar-team/Cardinal/internal/context" 11 | "github.com/vidar-team/Cardinal/internal/db" 12 | "github.com/vidar-team/Cardinal/internal/form" 13 | "github.com/vidar-team/Cardinal/internal/i18n" 14 | ) 15 | 16 | // BulletinHandler is the bulletin request handler. 17 | type BulletinHandler struct{} 18 | 19 | // NewBulletinHandler creates and returns a new bulletin Handler. 20 | func NewBulletinHandler() *BulletinHandler { 21 | return &BulletinHandler{} 22 | } 23 | 24 | // List returns all the bulletins. 25 | func (*BulletinHandler) List(ctx context.Context) error { 26 | bulletins, err := db.Bulletins.Get(ctx.Request().Context()) 27 | if err != nil { 28 | log.Error("Failed to get bulletins list: %v", err) 29 | return ctx.ServerError() 30 | } 31 | 32 | return ctx.Success(bulletins) 33 | } 34 | 35 | // New creates a new bulletin with the given options. 36 | func (*BulletinHandler) New(ctx context.Context, f form.NewBulletin) error { 37 | _, err := db.Bulletins.Create(ctx.Request().Context(), db.CreateBulletinOptions{ 38 | Title: f.Title, 39 | Body: f.Body, 40 | }) 41 | if err != nil { 42 | log.Error("Failed to create new bulletin: %v", err) 43 | return ctx.ServerError() 44 | } 45 | 46 | return ctx.Success() 47 | } 48 | 49 | // Update updates the bulletin with the given options. 50 | func (*BulletinHandler) Update(ctx context.Context, f form.UpdateBulletin, l *i18n.Locale) error { 51 | // Check the bulletin exists or not. 52 | bulletin, err := db.Bulletins.GetByID(ctx.Request().Context(), f.ID) 53 | if err != nil { 54 | if err == db.ErrBulletinNotExists { 55 | return ctx.Error(40400, l.T("bulletin.not_found")) 56 | } 57 | log.Error("Failed to get bulletin: %v", err) 58 | return ctx.ServerError() 59 | } 60 | 61 | err = db.Bulletins.Update(ctx.Request().Context(), bulletin.ID, db.UpdateBulletinOptions{ 62 | Title: f.Title, 63 | Body: f.Body, 64 | }) 65 | if err != nil { 66 | log.Error("Failed to update bulletin: %v", err) 67 | return ctx.ServerError() 68 | } 69 | 70 | return ctx.Success() 71 | } 72 | 73 | // Delete deletes the bulletin with the given id. 74 | func (*BulletinHandler) Delete(ctx context.Context, l *i18n.Locale) error { 75 | id := uint(ctx.QueryInt("id")) 76 | 77 | // Check the bulletin exists or not. 78 | bulletin, err := db.Bulletins.GetByID(ctx.Request().Context(), id) 79 | if err != nil { 80 | if err == db.ErrBulletinNotExists { 81 | return ctx.Error(40400, l.T("bulletin.not_found")) 82 | } 83 | log.Error("Failed to get bulletin: %v", err) 84 | return ctx.ServerError() 85 | } 86 | 87 | err = db.Bulletins.DeleteByID(ctx.Request().Context(), bulletin.ID) 88 | if err != nil { 89 | log.Error("Failed to delete bulletin: %v", err) 90 | return ctx.ServerError() 91 | } 92 | 93 | return ctx.Success() 94 | } 95 | -------------------------------------------------------------------------------- /internal/route/flag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "fmt" 9 | 10 | log "unknwon.dev/clog/v2" 11 | 12 | "github.com/vidar-team/Cardinal/internal/clock" 13 | "github.com/vidar-team/Cardinal/internal/conf" 14 | "github.com/vidar-team/Cardinal/internal/context" 15 | "github.com/vidar-team/Cardinal/internal/db" 16 | "github.com/vidar-team/Cardinal/internal/utils" 17 | ) 18 | 19 | type FlagHandler struct{} 20 | 21 | func NewFlagHandler() *FlagHandler { 22 | return &FlagHandler{} 23 | } 24 | 25 | func (*FlagHandler) Get(ctx context.Context) error { 26 | page := ctx.QueryInt("page") 27 | pageSize := ctx.QueryInt("pageSize") 28 | teamID := ctx.QueryInt("teamID") 29 | challengeID := ctx.QueryInt("challengeID") 30 | gameBoxID := ctx.QueryInt("gameBoxID") 31 | round := ctx.QueryInt("round") 32 | 33 | flags, totalCount, err := db.Flags.Get(ctx.Request().Context(), db.GetFlagOptions{ 34 | Page: page, 35 | PageSize: pageSize, 36 | TeamID: uint(teamID), 37 | ChallengeID: uint(challengeID), 38 | GameBoxID: uint(gameBoxID), 39 | Round: uint(round), 40 | }) 41 | if err != nil { 42 | log.Error("Failed to get flags: %v", err) 43 | return ctx.ServerError() 44 | } 45 | 46 | return ctx.Success(map[string]interface{}{ 47 | "List": flags, 48 | "Count": totalCount, 49 | }) 50 | } 51 | 52 | func (*FlagHandler) BatchCreate(ctx context.Context) error { 53 | // TODO time analytic 54 | // TODO delete all the flag and regenerate in transaction. 55 | 56 | gameBoxes, err := db.GameBoxes.Get(ctx.Request().Context(), db.GetGameBoxesOption{}) 57 | if err != nil { 58 | log.Error("Failed to get game boxes: %v", err) 59 | return ctx.ServerError() 60 | } 61 | 62 | flagPrefix := conf.Game.FlagPrefix 63 | flagSuffix := conf.Game.FlagSuffix 64 | salt := utils.Sha1Encode(conf.App.SecuritySalt) 65 | totalRound := clock.T.TotalRound 66 | log.Trace("Total Round: %d", totalRound) 67 | 68 | flagMetadatas := make([]db.FlagMetadata, 0, int(totalRound)*len(gameBoxes)) 69 | for round := uint(1); round <= totalRound; round++ { 70 | // Flag = FlagPrefix + hmacSha1(TeamID + | + GameBoxID + | + Round, sha1(salt)) + FlagSuffix 71 | for _, gameBox := range gameBoxes { 72 | flag := flagPrefix + utils.HmacSha1Encode(fmt.Sprintf("%d|%d|%d", gameBox.TeamID, gameBox.ID, round), salt) + flagSuffix 73 | flagMetadatas = append(flagMetadatas, db.FlagMetadata{ 74 | GameBoxID: gameBox.ID, 75 | Round: round, 76 | Value: flag, 77 | }) 78 | } 79 | } 80 | 81 | if err := db.Flags.BatchCreate(ctx.Request().Context(), db.CreateFlagOptions{ 82 | Flags: flagMetadatas, 83 | }); err != nil { 84 | log.Error("Failed to batch create flags: %v", err) 85 | return ctx.ServerError() 86 | } 87 | 88 | return ctx.Success() 89 | } 90 | -------------------------------------------------------------------------------- /internal/route/flag_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/Cardinal-Platform/testify/assert" 13 | "github.com/flamego/flamego" 14 | 15 | "github.com/vidar-team/Cardinal/internal/clock" 16 | "github.com/vidar-team/Cardinal/internal/conf" 17 | "github.com/vidar-team/Cardinal/internal/form" 18 | ) 19 | 20 | func TestFlag(t *testing.T) { 21 | router, managerToken, cleanup := NewTestRoute(t) 22 | 23 | // Create two teams. 24 | createTeam(t, managerToken, router, form.NewTeam{ 25 | { 26 | Name: "Vidar", 27 | Logo: "https://vidar.club/logo.png", 28 | }, 29 | { 30 | Name: "E99p1ant", 31 | Logo: "https://github.red/logo.png", 32 | }, 33 | }) 34 | 35 | // Create two basic challenges. 36 | createChallenge(t, managerToken, router, form.NewChallenge{ 37 | Title: "Web1", 38 | BaseScore: 1000, 39 | }) 40 | createChallenge(t, managerToken, router, form.NewChallenge{ 41 | Title: "Web2", 42 | BaseScore: 1500, 43 | }) 44 | 45 | // Create four game boxes. 46 | createGameBox(t, managerToken, router, form.NewGameBox{ 47 | {ChallengeID: 1, TeamID: 1, IPAddress: "192.168.1.1", Port: 80, Description: "Web1 For Vidar"}, 48 | {ChallengeID: 1, TeamID: 2, IPAddress: "192.168.1.2", Port: 8080, Description: "Web1 For E99p1ant"}, 49 | {ChallengeID: 2, TeamID: 1, IPAddress: "192.168.2.1", Port: 80, Description: "Web2 For Vidar"}, 50 | {ChallengeID: 2, TeamID: 2, IPAddress: "192.168.2.2", Port: 8080, Description: "Web2 For E99p1ant"}, 51 | }) 52 | 53 | conf.Game.FlagPrefix = "d3ctf{" 54 | conf.Game.FlagSuffix = "}" 55 | 56 | // Mock total round. 57 | totalRound := clock.T.TotalRound 58 | t.Cleanup(func() { 59 | clock.T.TotalRound = totalRound 60 | }) 61 | clock.T.TotalRound = 2 62 | 63 | for _, tc := range []struct { 64 | name string 65 | test func(t *testing.T, router *flamego.Flame, managerToken string) 66 | }{ 67 | {"Get", testFlagGet}, 68 | {"BatchCreate", testFlagBatchCreate}, 69 | } { 70 | t.Run(tc.name, func(t *testing.T) { 71 | t.Cleanup(func() { 72 | err := cleanup("flags") 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | }) 77 | 78 | tc.test(t, router, managerToken) 79 | }) 80 | } 81 | } 82 | 83 | func testFlagGet(t *testing.T, router *flamego.Flame, managerToken string) { 84 | // Empty flags. 85 | req, err := http.NewRequest(http.MethodGet, "/api/manager/flags", nil) 86 | assert.Nil(t, err) 87 | 88 | req.Header.Set("Authorization", managerToken) 89 | w := httptest.NewRecorder() 90 | router.ServeHTTP(w, req) 91 | assert.Equal(t, http.StatusOK, w.Code) 92 | want := `{"error":0,"data": {"Count": 0, "List": []}}` 93 | assert.JSONEq(t, want, w.Body.String()) 94 | 95 | // Create flags. 96 | createFlag(t, router, managerToken) 97 | req, err = http.NewRequest(http.MethodGet, "/api/manager/flags", nil) 98 | assert.Nil(t, err) 99 | 100 | req.Header.Set("Authorization", managerToken) 101 | w = httptest.NewRecorder() 102 | router.ServeHTTP(w, req) 103 | assert.Equal(t, http.StatusOK, w.Code) 104 | 105 | want = ` 106 | { 107 | "data": { 108 | "Count": 8, 109 | "List": [ 110 | { 111 | "ChallengeID": 1, 112 | "GameBoxID": 1, 113 | "ID": 1, 114 | "Round": 1, 115 | "TeamID": 1, 116 | "Value": "d3ctf{7046a8da1dbbb0b70d9843f1e19e1eee4679f727}" 117 | }, 118 | { 119 | "ChallengeID": 1, 120 | "GameBoxID": 2, 121 | "ID": 2, 122 | "Round": 1, 123 | "TeamID": 2, 124 | "Value": "d3ctf{782049c46936c0e01897ca35f11611b3878b8990}" 125 | }, 126 | { 127 | "ChallengeID": 2, 128 | "GameBoxID": 3, 129 | "ID": 3, 130 | "Round": 1, 131 | "TeamID": 1, 132 | "Value": "d3ctf{15b15829651d2551b0904040dfb3ebbe5519e5e7}" 133 | }, 134 | { 135 | "ChallengeID": 2, 136 | "GameBoxID": 4, 137 | "ID": 4, 138 | "Round": 1, 139 | "TeamID": 2, 140 | "Value": "d3ctf{241f46eecf5d15cd0571cb0f6b6acc3de7d1b277}" 141 | }, 142 | { 143 | "ChallengeID": 1, 144 | "GameBoxID": 1, 145 | "ID": 5, 146 | "Round": 2, 147 | "TeamID": 1, 148 | "Value": "d3ctf{e11756ef65f34c46782fead3babd0726624f3d2d}" 149 | }, 150 | { 151 | "ChallengeID": 1, 152 | "GameBoxID": 2, 153 | "ID": 6, 154 | "Round": 2, 155 | "TeamID": 2, 156 | "Value": "d3ctf{40091ccbbc5e4f516250b9b8125c3ebfa7e20515}" 157 | }, 158 | { 159 | "ChallengeID": 2, 160 | "GameBoxID": 3, 161 | "ID": 7, 162 | "Round": 2, 163 | "TeamID": 1, 164 | "Value": "d3ctf{f7941b09ee3ad7b078052958dd4b55e62acb9da7}" 165 | }, 166 | { 167 | "ChallengeID": 2, 168 | "GameBoxID": 4, 169 | "ID": 8, 170 | "Round": 2, 171 | "TeamID": 2, 172 | "Value": "d3ctf{3263048c9b68ff1f7759df13269fdf250ac84f66}" 173 | } 174 | ] 175 | }, 176 | "error": 0 177 | }` 178 | assert.JSONPartialEq(t, want, w.Body.String()) 179 | } 180 | 181 | func testFlagBatchCreate(t *testing.T, router *flamego.Flame, managerToken string) { 182 | req, err := http.NewRequest(http.MethodGet, "/api/manager/flags", nil) 183 | assert.Nil(t, err) 184 | req.Header.Set("Authorization", managerToken) 185 | w := httptest.NewRecorder() 186 | router.ServeHTTP(w, req) 187 | 188 | assert.Equal(t, http.StatusOK, w.Code) 189 | } 190 | 191 | func createFlag(t *testing.T, router *flamego.Flame, managerToken string) { 192 | req, err := http.NewRequest(http.MethodPost, "/api/manager/flags", nil) 193 | assert.Nil(t, err) 194 | req.Header.Set("Authorization", managerToken) 195 | w := httptest.NewRecorder() 196 | router.ServeHTTP(w, req) 197 | 198 | assert.Equal(t, http.StatusOK, w.Code) 199 | } 200 | -------------------------------------------------------------------------------- /internal/route/game_box.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | log "unknwon.dev/clog/v2" 9 | 10 | "github.com/vidar-team/Cardinal/internal/context" 11 | "github.com/vidar-team/Cardinal/internal/db" 12 | "github.com/vidar-team/Cardinal/internal/form" 13 | "github.com/vidar-team/Cardinal/internal/i18n" 14 | ) 15 | 16 | // GameBoxHandler is the game box request handler. 17 | type GameBoxHandler struct{} 18 | 19 | // NewGameBoxHandler creates and returns a new game box handler. 20 | func NewGameBoxHandler() *GameBoxHandler { 21 | return &GameBoxHandler{} 22 | } 23 | 24 | // List returns all the game boxes. 25 | func (*GameBoxHandler) List(ctx context.Context) error { 26 | gameBoxes, err := db.GameBoxes.Get(ctx.Request().Context(), db.GetGameBoxesOption{}) 27 | if err != nil { 28 | log.Error("Failed to get game box list: %v", err) 29 | return ctx.ServerError() 30 | } 31 | 32 | count, err := db.GameBoxes.Count(ctx.Request().Context()) 33 | if err != nil { 34 | log.Error("Failed to get game box count: %v", err) 35 | return ctx.ServerError() 36 | } 37 | 38 | return ctx.Success(map[string]interface{}{ 39 | "Data": gameBoxes, 40 | "Count": count, 41 | }) 42 | } 43 | 44 | // New creates game boxes with the given options. 45 | func (*GameBoxHandler) New(ctx context.Context, f form.NewGameBox, l *i18n.Locale) error { 46 | if len(f) == 0 { 47 | return ctx.Error(40000, "empty game box list") 48 | } 49 | 50 | gameBoxOptions := make([]db.CreateGameBoxOptions, 0, len(f)) 51 | for _, option := range f { 52 | gameBoxOptions = append(gameBoxOptions, db.CreateGameBoxOptions{ 53 | TeamID: option.TeamID, 54 | ChallengeID: option.ChallengeID, 55 | IPAddress: option.IPAddress, 56 | Port: option.Port, 57 | Description: option.Description, 58 | InternalSSH: db.SSHConfig{ 59 | Port: option.InternalSSHPort, 60 | User: option.InternalSSHUser, 61 | Password: option.InternalSSHPassword, 62 | }, 63 | }) 64 | } 65 | 66 | _, err := db.GameBoxes.BatchCreate(ctx.Request().Context(), gameBoxOptions) 67 | if err != nil { 68 | if err == db.ErrGameBoxAlreadyExists { 69 | // TODO show which game box has existed. 70 | return ctx.Error(40000, l.T("gamebox.repeat")) 71 | } 72 | log.Error("Failed to create game boxes in batch: %v", err) 73 | return ctx.ServerError() 74 | } 75 | 76 | return ctx.Success(gameBoxOptions) 77 | } 78 | 79 | // Update updates the game box. 80 | func (*GameBoxHandler) Update(ctx context.Context, f form.UpdateGameBox, l *i18n.Locale) error { 81 | err := db.GameBoxes.Update(ctx.Request().Context(), f.ID, db.UpdateGameBoxOptions{ 82 | IPAddress: f.IPAddress, 83 | Port: f.Port, 84 | Description: f.Description, 85 | InternalSSH: db.SSHConfig{ 86 | Port: f.InternalSSHPort, 87 | User: f.InternalSSHUser, 88 | Password: f.InternalSSHPassword, 89 | }, 90 | }) 91 | if err == db.ErrGameBoxNotExists { 92 | return ctx.Error(40400, "gamebox.not_found") 93 | } 94 | return ctx.Success() 95 | } 96 | 97 | // Delete removes the game box. 98 | func (*GameBoxHandler) Delete(ctx context.Context, l *i18n.Locale) error { 99 | id := ctx.QueryInt("id") 100 | err := db.GameBoxes.DeleteByIDs(ctx.Request().Context(), uint(id)) 101 | if err == db.ErrGameBoxNotExists { 102 | return ctx.Error(40400, "gamebox.not_found") 103 | } 104 | return ctx.Success() 105 | } 106 | 107 | // ResetAll resets all the game boxes. 108 | // It deletes all the game boxes score record and refresh the ranking list. 109 | func (*GameBoxHandler) ResetAll(ctx context.Context) error { 110 | // TODO 111 | return nil 112 | } 113 | 114 | // SSHTest tests the game box SSH configuration, 115 | // which try to connect to the game box instance within SSH. 116 | func (*GameBoxHandler) SSHTest(ctx context.Context) error { 117 | // TODO 118 | return nil 119 | } 120 | 121 | // RefreshFlag refreshes the game box flag if the `RenewFlagCommand` was set in challenge. 122 | // It will connect to the game box instance and run the command to refresh the flag. 123 | func (*GameBoxHandler) RefreshFlag(ctx context.Context) error { 124 | // TODO 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /internal/route/general.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/vidar-team/Cardinal/internal/clock" 11 | "github.com/vidar-team/Cardinal/internal/conf" 12 | "github.com/vidar-team/Cardinal/internal/context" 13 | ) 14 | 15 | // GeneralHandler is the general request handler. 16 | type GeneralHandler struct{} 17 | 18 | // NewGeneralHandler creates and returns a new GeneralHandler. 19 | func NewGeneralHandler() *GeneralHandler { 20 | return &GeneralHandler{} 21 | } 22 | 23 | func (*GeneralHandler) Hello(c context.Context) error { 24 | return c.Success(map[string]interface{}{ 25 | "Version": conf.Version, 26 | "BuildTime": conf.BuildTime, 27 | "BuildCommit": conf.BuildCommit, 28 | }) 29 | } 30 | 31 | func (*GeneralHandler) Init(c context.Context) error { 32 | return c.Success(map[string]interface{}{ 33 | "Name": conf.App.Name, 34 | }) 35 | } 36 | 37 | func (*GeneralHandler) Time(c context.Context) error { 38 | return c.Success(map[string]interface{}{ 39 | "CurrentTime": time.Now().Unix(), 40 | "StartAt": clock.T.StartAt.Unix(), 41 | "EndAt": clock.T.EndAt.Unix(), 42 | "RoundDuration": clock.T.RoundDuration.Seconds(), 43 | "CurrentRound": clock.T.CurrentRound, 44 | "RoundRemainDuration": int(clock.T.RoundRemainDuration.Seconds()), 45 | "Status": clock.T.Status, 46 | "TotalRound": clock.T.TotalRound, 47 | }) 48 | } 49 | 50 | func (*GeneralHandler) NotFound(c context.Context) error { 51 | // TODO: i18n support. 52 | return c.Error(40400, "not found") 53 | } 54 | -------------------------------------------------------------------------------- /internal/route/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "github.com/vidar-team/Cardinal/internal/context" 9 | "github.com/vidar-team/Cardinal/internal/rank" 10 | ) 11 | 12 | // ManagerHandler is the manager request handler. 13 | type ManagerHandler struct{} 14 | 15 | // NewManagerHandler creates and returns a new manager Handler. 16 | func NewManagerHandler() *ManagerHandler { 17 | return &ManagerHandler{} 18 | } 19 | 20 | func (*ManagerHandler) Panel() { 21 | 22 | } 23 | 24 | func (*ManagerHandler) Logs() { 25 | 26 | } 27 | 28 | func (*ManagerHandler) Rank(ctx context.Context) error { 29 | return ctx.Success(rank.ForManager()) 30 | } 31 | -------------------------------------------------------------------------------- /internal/route/route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/flamego/cors" 11 | "github.com/flamego/flamego" 12 | "github.com/flamego/session" 13 | 14 | "github.com/vidar-team/Cardinal/internal/context" 15 | "github.com/vidar-team/Cardinal/internal/form" 16 | "github.com/vidar-team/Cardinal/internal/i18n" 17 | ) 18 | 19 | // NewRouter returns the router. 20 | func NewRouter() *flamego.Flame { 21 | f := flamego.Classic() 22 | 23 | f.Use( 24 | session.Sessioner(session.Options{ 25 | ReadIDFunc: func(r *http.Request) string { return r.Header.Get("Authorization") }, 26 | WriteIDFunc: func(w http.ResponseWriter, r *http.Request, sid string, created bool) {}, 27 | }), 28 | context.Contexter(), 29 | i18n.I18n(), 30 | flamego.Static(flamego.StaticOptions{ 31 | Directory: "uploads", 32 | Prefix: "uploads", 33 | }), 34 | ) 35 | 36 | f.Use(cors.CORS( 37 | cors.Options{ 38 | Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions}, 39 | }, 40 | )) 41 | 42 | general := NewGeneralHandler() 43 | auth := NewAuthHandler() 44 | bulletin := NewBulletinHandler() 45 | challenge := NewChallengeHandler() 46 | flag := NewFlagHandler() 47 | gameBox := NewGameBoxHandler() 48 | team := NewTeamHandler() 49 | manager := NewManagerHandler() 50 | 51 | f.Group("/api", func() { 52 | f.Any("/", general.Hello) 53 | f.Get("/init", general.Init) 54 | f.Get("/time", general.Time) 55 | f.Get("/asteroid") 56 | 57 | f.Post("/submitFlag", form.Bind(form.SubmitFlag{}), auth.TeamTokenAuthenticator, team.SubmitFlag) 58 | 59 | f.Group("/team", func() { 60 | f.Post("/login", form.Bind(form.TeamLogin{}), auth.TeamLogin) 61 | f.Get("/logout", auth.TeamLogout) 62 | 63 | f.Group("", func() { 64 | f.Get("/info", team.Info) 65 | f.Get("/gameBoxes", func() { 66 | f.Get("/", team.GameBoxes) 67 | f.Get("/all") 68 | }) 69 | f.Get("/bulletins", team.Bulletins) 70 | f.Get("/rank", team.Rank) 71 | f.Get("/liveLog") 72 | }, auth.TeamAuthenticator) 73 | }) 74 | 75 | f.Group("/manager", func() { 76 | f.Post("/login", form.Bind(form.ManagerLogin{}), auth.ManagerLogin) 77 | f.Get("/logout", auth.ManagerLogout) 78 | 79 | f.Group("", func() { 80 | f.Get("/panel") 81 | f.Get("/logs") 82 | f.Get("/rank", manager.Rank) 83 | 84 | // Challenge 85 | f.Get("/challenges", challenge.List) 86 | f.Post("/challenge", form.Bind(form.NewChallenge{}), challenge.New) 87 | f.Put("/challenge", form.Bind(form.UpdateChallenge{}), challenge.Update) 88 | f.Delete("/challenge", challenge.Delete) 89 | f.Post("/challenge/visible", form.Bind(form.SetChallengeVisible{}), challenge.SetVisible) 90 | 91 | // Team 92 | f.Get("/teams", team.List) 93 | f.Post("/teams", form.Bind(form.NewTeam{}), team.New) 94 | f.Put("/team", form.Bind(form.UpdateTeam{}), team.Update) 95 | f.Delete("/team", team.Delete) 96 | f.Post("/team/resetPassword", team.ResetPassword) 97 | 98 | // Game Box 99 | f.Get("/gameBoxes", gameBox.List) 100 | f.Post("/gameBoxes/reset") 101 | f.Post("/gameBoxes", form.Bind(form.NewGameBox{}), gameBox.New) 102 | f.Put("/gameBox", form.Bind(form.UpdateGameBox{}), gameBox.Update) 103 | f.Delete("/gameBox", gameBox.Delete) 104 | f.Post("/gameBox/sshTest") 105 | f.Post("/gameBox/refreshFlag") 106 | 107 | // Flag 108 | f.Get("/flags", flag.Get) 109 | f.Post("/flags", flag.BatchCreate) 110 | 111 | // Bulletins 112 | f.Get("/bulletins", bulletin.List) 113 | f.Post("/bulletin", form.Bind(form.NewBulletin{}), bulletin.New) 114 | f.Put("/bulletin", form.Bind(form.UpdateBulletin{}), bulletin.Update) 115 | f.Delete("/bulletin", bulletin.Delete) 116 | 117 | // Asteroid 118 | f.Group("/asteroid", func() { 119 | f.Get("/status") 120 | f.Post("/attack") 121 | f.Post("/rank") 122 | f.Post("/status") 123 | f.Post("/round") 124 | f.Post("/easterEgg") 125 | f.Post("/time") 126 | f.Post("/clear") 127 | }) 128 | 129 | // Account 130 | f.Group("/account", func() { 131 | f.Get("") 132 | f.Post("") 133 | f.Put("") 134 | f.Delete("") 135 | }) 136 | 137 | // Check 138 | f.Get("/checkDown") 139 | }, auth.ManagerAuthenticator) 140 | }) 141 | }) 142 | 143 | f.NotFound(general.NotFound) 144 | 145 | return f 146 | } 147 | -------------------------------------------------------------------------------- /internal/route/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package route 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | 14 | "bou.ke/monkey" 15 | "github.com/flamego/flamego" 16 | jsoniter "github.com/json-iterator/go" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/thanhpk/randstr" 19 | 20 | "github.com/vidar-team/Cardinal/internal/db" 21 | "github.com/vidar-team/Cardinal/internal/dbutil" 22 | "github.com/vidar-team/Cardinal/internal/form" 23 | "github.com/vidar-team/Cardinal/internal/utils" 24 | ) 25 | 26 | const ( 27 | TestRouteAdminName = "cardinal_admin" 28 | TestRouteAdminPassword = "supersecurepassword" 29 | ) 30 | 31 | // NewTestRoute returns the route used for test. 32 | func NewTestRoute(t *testing.T) (*flamego.Flame, string, func(tables ...string) error) { 33 | f := NewRouter() 34 | testDB, dbCleanup := dbutil.NewTestDB(t, db.AllTables...) 35 | // Set the global database store to test database. 36 | db.SetDatabaseStore(testDB) 37 | 38 | // Mock utils.GenerateToken() 39 | tokenPatch := monkey.Patch(utils.GenerateToken, func() string { return "mocked_token" }) 40 | t.Cleanup(func() { 41 | tokenPatch.Unpatch() 42 | }) 43 | 44 | // Mock github.com/thanhpk/randstr package. 45 | randstrPatch := monkey.Patch(randstr.Hex, func(int) string { return "mocked_randstr_hex" }) 46 | t.Cleanup(func() { 47 | randstrPatch.Unpatch() 48 | }) 49 | 50 | // Create manager account for testing. 51 | ctx := context.Background() 52 | _, err := db.Managers.Create(ctx, db.CreateManagerOptions{ 53 | Name: TestRouteAdminName, 54 | Password: TestRouteAdminPassword, 55 | IsCheckAccount: false, 56 | }) 57 | assert.Nil(t, err) 58 | 59 | // Login as manager to get the manager token. 60 | loginBody, err := jsoniter.Marshal(form.ManagerLogin{ 61 | Name: TestRouteAdminName, 62 | Password: TestRouteAdminPassword, 63 | }) 64 | assert.Nil(t, err) 65 | 66 | w := httptest.NewRecorder() 67 | req, err := http.NewRequest(http.MethodPost, "/api/manager/login", bytes.NewBuffer(loginBody)) 68 | assert.Nil(t, err) 69 | f.ServeHTTP(w, req) 70 | assert.Equal(t, http.StatusOK, w.Code) 71 | 72 | respBody := struct { 73 | Data string `json:"data"` 74 | }{} 75 | err = jsoniter.NewDecoder(w.Body).Decode(&respBody) 76 | assert.Nil(t, err) 77 | managerToken := respBody.Data 78 | 79 | return f, managerToken, func(tables ...string) error { 80 | if t.Failed() { 81 | return nil 82 | } 83 | 84 | // Reset database table. 85 | if err := dbCleanup(tables...); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/route/wrapper.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // __ is the wrap of the Gin handler. 6 | func __(handler func(*gin.Context) (int, interface{})) func(*gin.Context) { 7 | return func(c *gin.Context) { 8 | c.JSON(handler(c)) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/patrickmn/go-cache" 7 | ) 8 | 9 | var store *cache.Cache 10 | 11 | func Init() { 12 | c := cache.New(cache.NoExpiration, cache.DefaultExpiration) 13 | store = c 14 | } 15 | 16 | func Get(k string) (interface{}, bool) { 17 | return store.Get(k) 18 | } 19 | 20 | func Set(k string, x interface{}, d ...time.Duration) { 21 | duration := cache.NoExpiration 22 | if len(d) == 1 { 23 | duration = d[0] 24 | } 25 | store.Set(k, x, duration) 26 | } 27 | -------------------------------------------------------------------------------- /internal/timer/bridge.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | var SetRankListTitle func() 4 | var SetRankList func() 5 | var CleanGameBoxStatus func() 6 | var GetLatestScoreRound func() int 7 | var RefreshFlag func() 8 | var CalculateRoundScore func(int) 9 | -------------------------------------------------------------------------------- /internal/upload/file.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/thanhpk/randstr" 12 | "github.com/vidar-team/Cardinal/internal/locales" 13 | "github.com/vidar-team/Cardinal/internal/utils" 14 | ) 15 | 16 | func GetDir(c *gin.Context) (int, interface{}) { 17 | basePath := c.Query("path") 18 | folder := c.Query("folder") 19 | hidden, _ := strconv.ParseBool(c.Query("hidden")) 20 | folderOnly, _ := strconv.ParseBool(c.Query("folderOnly")) 21 | 22 | if basePath == "" { 23 | nowPath, err := os.Getwd() 24 | basePath = nowPath 25 | if err != nil { 26 | return utils.MakeErrJSON(500, 50025, "获取当前目录信息失败") 27 | } 28 | } 29 | path := filepath.Join(basePath, folder) 30 | 31 | f, err := os.Stat(path) 32 | if err != nil { 33 | return utils.MakeErrJSON(500, 50026, fmt.Sprintf("打开文件 %s 失败", path)) 34 | } 35 | if !f.IsDir() { 36 | return utils.MakeErrJSON(500, 50027, fmt.Sprintf("%s 不是目录", path)) 37 | } 38 | fileInfo, err := ioutil.ReadDir(path) 39 | if err != nil { 40 | return utils.MakeErrJSON(500, 50026, fmt.Sprintf("打开文件 %s 失败", path)) 41 | } 42 | 43 | type fileItem struct { 44 | Name string 45 | IsDir bool 46 | Size string 47 | ModTime int64 48 | } 49 | 50 | files := make([]fileItem, 0, len(fileInfo)) 51 | for _, f := range fileInfo { 52 | if f.Name()[0] == '.' && !hidden { 53 | // skip hidden file. 54 | continue 55 | } 56 | 57 | if !f.IsDir() && folderOnly { 58 | // skip file. 59 | continue 60 | } 61 | 62 | files = append(files, fileItem{ 63 | Name: f.Name(), 64 | IsDir: f.IsDir(), 65 | Size: utils.FileSize(f.Size()), 66 | ModTime: f.ModTime().Unix(), 67 | }) 68 | } 69 | 70 | return utils.MakeSuccessJSON(gin.H{ 71 | "path": path, 72 | "files": files, 73 | }) 74 | } 75 | 76 | // UploadPicture is upload team logo handler for manager. 77 | func UploadPicture(c *gin.Context) (int, interface{}) { 78 | file, err := c.FormFile("picture") 79 | if err != nil { 80 | return utils.MakeErrJSON(400, 40025, 81 | locales.I18n.T(c.GetString("lang"), "file.select_picture"), 82 | ) 83 | } 84 | fileExt := map[string]string{ 85 | "image/png": ".png", 86 | "image/gif": ".gif", 87 | "image/jpeg": ".jpg", 88 | } 89 | ext, ok := fileExt[c.GetHeader("Content-Type")] 90 | if !ok { 91 | ext = ".png" 92 | } 93 | fileName := randstr.Hex(16) + ext 94 | 95 | err = c.SaveUploadedFile(file, "./uploads/"+fileName) 96 | if err != nil { 97 | return utils.MakeErrJSON(500, 50014, 98 | locales.I18n.T(c.GetString("lang"), "general.server_error"), 99 | ) 100 | } 101 | return utils.MakeSuccessJSON(fileName) 102 | } 103 | -------------------------------------------------------------------------------- /internal/utils/const.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // These variable will be assigned in CI. 4 | var ( 5 | VERSION string 6 | COMMIT_SHA string 7 | BUILD_TIME string 8 | ) 9 | 10 | const ( 11 | // Config 12 | DATBASE_VERSION = "database_version" 13 | TITLE_CONF = "title" 14 | FLAG_PREFIX_CONF = "flag_prefix" 15 | FLAG_SUFFIX_CONF = "flag_suffix" 16 | ANIMATE_ASTEROID = "animate_asteroid" 17 | SHOW_OTHERS_GAMEBOX = "show_others_gamebox" 18 | DEFAULT_LANGUAGE = "default_language" 19 | 20 | BOOLEAN_TRUE = "true" 21 | BOOLEAN_FALSE = "false" 22 | ) 23 | 24 | const ( 25 | // Config type 26 | STRING = iota 27 | BOOLEAN 28 | SELECT 29 | ) 30 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/hmac" 7 | "crypto/sha1" 8 | "fmt" 9 | "io" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/dustin/go-humanize" 16 | "github.com/gin-gonic/gin" 17 | "github.com/satori/go.uuid" 18 | "golang.org/x/crypto/ssh" 19 | 20 | "github.com/vidar-team/Cardinal/internal/conf" 21 | ) 22 | 23 | // MakeErrJSON makes the error response JSON for gin. 24 | func MakeErrJSON(httpStatusCode int, errCode int, msg interface{}) (int, interface{}) { 25 | return httpStatusCode, gin.H{"error": errCode, "msg": fmt.Sprint(msg)} 26 | } 27 | 28 | // MakeSuccessJSON makes the successful response JSON for gin. 29 | func MakeSuccessJSON(data interface{}) (int, interface{}) { 30 | return 200, gin.H{"error": 0, "msg": "success", "data": data} 31 | } 32 | 33 | // CheckPassword adds salt and check the password. 34 | func CheckPassword(inputPassword string, realPassword string) bool { 35 | // sha1( sha1(password) + salt ) 36 | return HmacSha1Encode(inputPassword, conf.App.SecuritySalt) == realPassword 37 | } 38 | 39 | // Sha1Encode Sha1 encode input string. 40 | func Sha1Encode(input string) string { 41 | h := sha1.New() 42 | _, _ = h.Write([]byte(input)) 43 | bs := h.Sum(nil) 44 | return fmt.Sprintf("%x", bs) 45 | } 46 | 47 | // AddSalt uses the config salt as key to HmacSha1Encode. 48 | func AddSalt(input string) string { 49 | return HmacSha1Encode(input, conf.App.SecuritySalt) 50 | } 51 | 52 | // HmacSha1Encode HMAC SHA1 encode 53 | func HmacSha1Encode(input string, key string) string { 54 | h := hmac.New(sha1.New, []byte(key)) 55 | _, _ = io.WriteString(h, input) 56 | return fmt.Sprintf("%x", h.Sum(nil)) 57 | } 58 | 59 | // GenerateToken returns UUID v4 string. 60 | func GenerateToken() string { 61 | return uuid.NewV4().String() 62 | } 63 | 64 | // FileSize returns the formatter text of the giving size. 65 | func FileSize(size int64) string { 66 | return humanize.IBytes(uint64(size)) 67 | } 68 | 69 | // FileIsExist check the file or folder existed. 70 | func FileIsExist(path string) bool { 71 | _, err := os.Stat(path) 72 | return err == nil || os.IsExist(err) 73 | } 74 | 75 | // InputString used in the install.go for the config file guide. 76 | func InputString(str *string, hint string) { 77 | var err error 78 | var input string 79 | for input == "" { 80 | fmt.Println(">", hint) 81 | 82 | stdin := bufio.NewReader(os.Stdin) 83 | input, err = stdin.ReadString('\n') 84 | input = strings.Trim(input, "\r\n") 85 | if err != nil || input == "" { 86 | if *str != "" { 87 | break 88 | } 89 | } 90 | *str = input 91 | } 92 | } 93 | 94 | func SSHExecute(ip string, port string, user string, password string, command string) (string, error) { 95 | client, err := ssh.Dial("tcp", ip+":"+port, &ssh.ClientConfig{ 96 | User: user, 97 | Auth: []ssh.AuthMethod{ssh.Password(password)}, 98 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 99 | Timeout: 5 * time.Second, 100 | }) 101 | if err != nil { 102 | return "", err 103 | } 104 | defer client.Close() 105 | 106 | session, err := client.NewSession() 107 | if err != nil { 108 | return "", err 109 | } 110 | defer session.Close() 111 | 112 | var output bytes.Buffer 113 | session.Stdout = &output 114 | err = session.Run(command) 115 | if err != nil { 116 | return "", err 117 | } 118 | 119 | return output.String(), nil 120 | } 121 | 122 | // CompareVersion used to compare the cardinal version. 123 | func CompareVersion(v1 string, v2 string) bool { 124 | // The version of Cardinal is v.x.x.x, 125 | // we split the string by `.` and compare the number. 126 | // if v1 >= v2 return true 127 | // v1 < v2 return false 128 | // 129 | // It will always return false if the version format is wrong. 130 | 131 | // Empty string 132 | if v1 == "" || v2 == "" { 133 | return false 134 | } 135 | 136 | // Check format 137 | if v1[0] != 'v' || v2[0] != 'v' { 138 | return false 139 | } 140 | v1, v2 = v1[1:], v2[1:] 141 | 142 | v1Segment := strings.Split(v1, ".") 143 | v2Segment := strings.Split(v2, ".") 144 | if len(v1Segment) != 3 || len(v2Segment) != 3 { 145 | return false 146 | } 147 | 148 | if v1 == v2 { 149 | return true 150 | } 151 | 152 | // Compare each part. 153 | for segIndex := 0; segIndex < 3; segIndex++ { 154 | v1Number, err := strconv.Atoi(v1Segment[segIndex]) 155 | if err != nil { 156 | return false 157 | } 158 | v2Number, err := strconv.Atoi(v2Segment[segIndex]) 159 | if err != nil { 160 | return false 161 | } 162 | if v1Number == v2Number { 163 | continue 164 | } 165 | return v1Number > v2Number 166 | } 167 | // They are the same. 168 | return true 169 | } 170 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_CompareVersion(t *testing.T) { 11 | examples := map[[2]string]bool{ 12 | [2]string{"", ""}: false, 13 | [2]string{"v0.1.1", ""}: false, 14 | [2]string{"0.1.1", "0.1.1"}: false, 15 | [2]string{"0.1.2", "0.1.1"}: false, 16 | [2]string{"v1.0.0", "v2.0.0"}: false, 17 | [2]string{"v2.0.0", "v1.0.0"}: true, 18 | [2]string{"v1.2.3", "v3.2.1"}: false, 19 | [2]string{"v3.2.1", "v1.2.3"}: true, 20 | [2]string{"v0.0.7", "v0.0.7"}: true, 21 | [2]string{"v0.1.7", "v0.2.7"}: false, 22 | [2]string{"v1.3.7", "v1.2.5"}: true, 23 | [2]string{"v11.3.7", "v12.2.5"}: false, 24 | [2]string{"v11.3.7", "v11.2.5"}: true, 25 | [2]string{"v45.21.67", "v23.21.59"}: true, 26 | } 27 | for k, v := range examples { 28 | assert.Equal(t, v, CompareVersion(k[0], k[1]), fmt.Sprintf("v1: %s, v2: %s", k[0], k[1])) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /locales/embed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 E99p1ant. All rights reserved. 2 | // Use of this source code is governed by an AGPL-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package locales 6 | 7 | import ( 8 | "embed" 9 | ) 10 | 11 | //go:embed *.yml 12 | var FS embed.FS 13 | -------------------------------------------------------------------------------- /locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | zh-CN: 2 | general: 3 | success: "成功" 4 | error_payload: "请求体格式错误!" 5 | error_query: "请求参数不正确!" 6 | must_be_number: "参数 {{.key}} 必须为数字!" 7 | post_repeat: "重复添加!" 8 | not_begin: "比赛未开始" 9 | server_error: "后端程序错误!" 10 | invalid_token: "Token 无效" 11 | no_auth: "未授权访问" 12 | not_found: "资源不存在" 13 | method_not_allow: "请求方法不允许" 14 | database_charset_error: "数据库编码格式错误。可能导致无法正确处理中文字符,请删除并重新创建数据库,设置编码格式为 utf8mb4。示例:CREATE DATABASE `cardinal` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" 15 | 16 | bulletin: 17 | post_error: "添加公告失败!" 18 | post_success: "添加公告成功!" 19 | put_error: "修改公告失败!" 20 | put_success: "修改公告成功!" 21 | delete_error: "删除公告失败!" 22 | delete_success: "删除公告成功!" 23 | not_found: "公告不存在!" 24 | 25 | challenge: 26 | post_error: "添加题目失败!" 27 | post_success: "添加题目成功!" 28 | put_error: "修改题目信息失败!" 29 | put_success: "修改题目信息成功!" 30 | delete_error: "删除题目失败!" 31 | delete_success: "删除题目成功!" 32 | not_found: "题目不存在!" 33 | empty_command: "命令不能为空!" 34 | repeat: "题目重复!" 35 | 36 | check: 37 | repeat: "重复 Check,已忽略" 38 | not_visible: "题目未开题,CheckDown 失败" 39 | 40 | config: 41 | load_success: "加载配置文件成功" 42 | update_success: "更新设置成功!" 43 | 44 | file: 45 | select_picture: "请选择图片文件!" 46 | 47 | flag: 48 | wrong: "Flag 错误!" 49 | submit_error: "提交失败!" 50 | submit_success: "提交成功!" 51 | repeat: "请勿重复提交 Flag" 52 | generate_success: "生成 Flag 成功!" 53 | 54 | gamebox: 55 | post_error: "添加靶机失败!" 56 | post_success: "添加靶机成功!" 57 | put_error: "修改靶机信息失败!" 58 | put_success: "修改靶机信息成功!" 59 | delete_error: "删除靶机失败!" 60 | delete_success: "删除靶机成功!" 61 | visibility_success: "修改靶机可见状态成功!" 62 | repeat: "存在重复添加的靶机,请检查" 63 | not_found: "靶机不存在!" 64 | auto_refresh_flag_error: "靶机 SSH 设置为空" 65 | reset_success: "重置靶机信息成功" 66 | 67 | team: 68 | post_error: "添加队伍失败!" 69 | put_error: "修改队伍信息失败!" 70 | put_success: "修改队伍信息成功!" 71 | delete_error: "删除队伍失败!" 72 | delete_success: "删除队伍成功!" 73 | login_error: "账号或密码错误!" 74 | logout_success: "登出成功!" 75 | repeat: "存在重复添加的队伍,请检查" 76 | team_name_empty: "队伍名不能为空" 77 | reset_password_error: "重置密码失败!" 78 | not_found: "队伍不存在!" 79 | 80 | timer: 81 | total_round: "比赛总轮数:{{.round}}" 82 | total_time: "比赛总时长:{{.time}} 分钟" 83 | end: "比赛已结束" 84 | start_time_error: "比赛结束时间应大于开始时间!" 85 | single_rest_time_error: "RestTime 单个时间周期配置错误!" 86 | rest_time_start_error: "RestTime 配置错误!前一时间应在后一时间点之前。[ {{.from}} - {{.to}} ]" 87 | rest_time_overflow_error: "RestTime 配置错误!不能在比赛开始时间之前或比赛结束时间之后。[ {{.from}} - {{.to}} ]" 88 | rest_time_order_error: "RestTime 需要按开始时间顺序输入![ {{.from}} - {{.to}} ]" 89 | 90 | healthy: 91 | previous_round_non_zero_error: "上一轮分数非零和,请检查!" 92 | total_score_non_zero_error: "总分数非零和,请检查!" 93 | 94 | install: 95 | greet: "Cardinal.toml 配置文件不存在,安装向导将带领您进行配置。" 96 | input_title: "请输入比赛名称" 97 | begin_time: "请输入比赛开始时间(格式 2020-02-17 12:00:00)" 98 | end_time: "请输入比赛结束时间(格式 2020-02-17 12:00:00)" 99 | duration: "请输入每轮长度(单位:分钟,默认值:2)" 100 | port: "请输入后端服务器端口号(默认值:19999)" 101 | flag_prefix: "请输入 Flag 前缀(默认值:hctf{)" 102 | flag_suffix: "请输入 Flag 后缀(默认值:})" 103 | checkdown_score: "请输入每次 Checkdown 扣分(默认值:50)" 104 | attack_score: "请输入每次攻击得分(默认值:50)" 105 | separate_frontend: "是否自行另外部署前端?(true / false,默认值:false)" 106 | sentry: "发送您的统计数据,帮助我们使 Cardinal 变得更好?(true / false,默认值:true)" 107 | db_host: "请输入数据库地址(默认值:localhost:3306)" 108 | db_username: "请输入数据库账号:" 109 | db_password: "请输入数据库密码:" 110 | db_name: "请输入数据库表名(默认值:cardinal)" 111 | manager_name: "请输入管理员账号:" 112 | manager_password: "请输入管理员密码:" 113 | manager_success: "添加管理员账号成功,请妥善保管您的账号密码信息!" 114 | create_config_success: "创建 Cardinal.toml 配置文件成功!" 115 | 116 | manager: 117 | login_error: "账号或密码错误!" 118 | logout_success: "登出成功!" 119 | post_error: "添加管理员账号失败!" 120 | post_success: "添加管理员成功!" 121 | delete_error: "删除管理员失败!" 122 | delete_success: "删除管理员成功!" 123 | repeat: "管理员名称重复" 124 | update_token_fail: "更新管理员 Token 失败!" 125 | update_password_fail: "修改管理员密码失败!" 126 | manager_required: "需要管理员权限账号" 127 | 128 | log: 129 | new_challenge: "新的题目 [{{.title}}] 被创建" 130 | delete_challenge: "题目 [{{.title}}] 被删除" 131 | new_gamebox: "共 {{.count}} 个靶机被创建" 132 | set_challenge_visible: "设置题目 [{{.challenge}}] 状态为可见" 133 | set_challenge_invisible: "设置题目 [{{.challenge}}] 状态为不可见" 134 | generate_flag: "生成 Flag 完成!共 {{.total}} 个。耗时 {{.time}} s。" 135 | score_success: "第 {{.round}} 轮分数结算完成!耗时 {{.time}} s。" 136 | new_manager: "新的管理员账号 [ {{.name}} ] 被添加" 137 | delete_manager: "管理员 [ ID: {{.id}} ] 已删除" 138 | manager_token: "管理员 [ ID: {{.id}} ] Token 已刷新" 139 | manager_password: "管理员 [ ID: {{.id}} ] 密码已修改" 140 | new_team: "{{.count}} 个新的队伍 [ {{.teamName}} ] 被创建" 141 | delete_team: "Team [ {{.teamName}} ] 被删除" 142 | team_reset_password: "队伍 [ {{.teamName}} ] 登录密码已重置" 143 | rank_list_success: "更新排行榜标题成功" 144 | 145 | misc: 146 | version_out_of_date: "当前 Cardinal 并非最新版本,请考虑升级到最新版本。当前版本: {{.currentVersion}} / 最新版本: {{.latestVersion}}" 147 | database_version_out_of_date: "数据库结构已升级,请考虑清除当前数据库中的数据并重新生成。" 148 | -------------------------------------------------------------------------------- /test/bulletin_test.go: -------------------------------------------------------------------------------- 1 | package cardinal_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_GetAllBulletins(t *testing.T) { 14 | w := httptest.NewRecorder() 15 | req, _ := http.NewRequest("GET", "/api/manager/bulletins", nil) 16 | req.Header.Set("Authorization", managerToken) 17 | router.ServeHTTP(w, req) 18 | assert.Equal(t, 200, w.Code) 19 | } 20 | 21 | func Test_NewBulletin(t *testing.T) { 22 | // JSON bind fail 23 | w := httptest.NewRecorder() 24 | jsonData, _ := json.Marshal(map[string]interface{}{ 25 | "Title": "this is a bulletin", 26 | }) 27 | req, _ := http.NewRequest("POST", "/api/manager/bulletin", bytes.NewBuffer(jsonData)) 28 | req.Header.Set("Authorization", managerToken) 29 | router.ServeHTTP(w, req) 30 | assert.Equal(t, 400, w.Code) 31 | 32 | // success 33 | w = httptest.NewRecorder() 34 | jsonData, _ = json.Marshal(map[string]interface{}{ 35 | "Title": "this is a bulletin", 36 | "Content": "test test test", 37 | }) 38 | req, _ = http.NewRequest("POST", "/api/manager/bulletin", bytes.NewBuffer(jsonData)) 39 | req.Header.Set("Authorization", managerToken) 40 | router.ServeHTTP(w, req) 41 | assert.Equal(t, 200, w.Code) 42 | } 43 | 44 | func Test_EditBulletin(t *testing.T) { 45 | // JSON bind fail 46 | w := httptest.NewRecorder() 47 | jsonData, _ := json.Marshal(map[string]interface{}{ 48 | "Title": "this is a bulletin", 49 | "Content": "new content", 50 | }) 51 | req, _ := http.NewRequest("PUT", "/api/manager/bulletin", bytes.NewBuffer(jsonData)) 52 | req.Header.Set("Authorization", managerToken) 53 | router.ServeHTTP(w, req) 54 | assert.Equal(t, 400, w.Code) 55 | 56 | // not found 57 | w = httptest.NewRecorder() 58 | jsonData, _ = json.Marshal(map[string]interface{}{ 59 | "ID": 2, 60 | "Title": "this is a bulletin", 61 | "Content": "new content", 62 | }) 63 | req, _ = http.NewRequest("PUT", "/api/manager/bulletin", bytes.NewBuffer(jsonData)) 64 | req.Header.Set("Authorization", managerToken) 65 | router.ServeHTTP(w, req) 66 | assert.Equal(t, 404, w.Code) 67 | 68 | // success 69 | w = httptest.NewRecorder() 70 | jsonData, _ = json.Marshal(map[string]interface{}{ 71 | "ID": 1, 72 | "Title": "this is a bulletin", 73 | "Content": "new content", 74 | }) 75 | req, _ = http.NewRequest("PUT", "/api/manager/bulletin", bytes.NewBuffer(jsonData)) 76 | req.Header.Set("Authorization", managerToken) 77 | router.ServeHTTP(w, req) 78 | assert.Equal(t, 200, w.Code) 79 | } 80 | 81 | func Test_DeleteBulletin(t *testing.T) { 82 | // error id 83 | w := httptest.NewRecorder() 84 | req, _ := http.NewRequest("DELETE", "/api/manager/bulletin?id=asdfg", nil) 85 | req.Header.Set("Authorization", managerToken) 86 | router.ServeHTTP(w, req) 87 | assert.Equal(t, 400, w.Code) 88 | 89 | // id not exist 90 | w = httptest.NewRecorder() 91 | req, _ = http.NewRequest("DELETE", "/api/manager/bulletin?id=233", nil) 92 | req.Header.Set("Authorization", managerToken) 93 | router.ServeHTTP(w, req) 94 | assert.Equal(t, 404, w.Code) 95 | 96 | // success 97 | w = httptest.NewRecorder() 98 | req, _ = http.NewRequest("DELETE", "/api/manager/bulletin?id=1", nil) 99 | req.Header.Set("Authorization", managerToken) 100 | router.ServeHTTP(w, req) 101 | assert.Equal(t, 200, w.Code) 102 | } 103 | -------------------------------------------------------------------------------- /test/cardinal_test.go: -------------------------------------------------------------------------------- 1 | package cardinal_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "unknwon.dev/clog/v2" 9 | 10 | "github.com/vidar-team/Cardinal/internal/asteroid" 11 | "github.com/vidar-team/Cardinal/internal/bootstrap" 12 | "github.com/vidar-team/Cardinal/internal/conf" 13 | "github.com/vidar-team/Cardinal/internal/dbold" 14 | "github.com/vidar-team/Cardinal/internal/dynamic_config" 15 | "github.com/vidar-team/Cardinal/internal/game" 16 | "github.com/vidar-team/Cardinal/internal/livelog" 17 | "github.com/vidar-team/Cardinal/internal/misc/webhook" 18 | "github.com/vidar-team/Cardinal/internal/route" 19 | "github.com/vidar-team/Cardinal/internal/store" 20 | "github.com/vidar-team/Cardinal/internal/timer" 21 | "github.com/vidar-team/Cardinal/internal/utils" 22 | ) 23 | 24 | var managerToken = utils.GenerateToken() 25 | 26 | var checkToken string 27 | 28 | var team = make([]struct { 29 | Name string `json:"Name"` 30 | Password string `json:"Password"` 31 | Token string `json:"token"` 32 | AccessKey string `json:"access_key"` 33 | }, 0) 34 | 35 | var router *gin.Engine 36 | 37 | func TestMain(m *testing.M) { 38 | prepare() 39 | log.Trace("Cardinal Test ready...") 40 | m.Run() 41 | 42 | os.Exit(0) 43 | } 44 | 45 | func prepare() { 46 | _ = log.NewConsole(100) 47 | 48 | log.Trace("Prepare for Cardinal test environment...") 49 | 50 | gin.SetMode(gin.ReleaseMode) 51 | 52 | err := conf.TestInit() 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | // Init MySQL database. 58 | dbold.InitMySQL() 59 | 60 | // Test manager account e99:qwe1qwe2qwe3 61 | dbold.MySQL.Create(&dbold.Manager{ 62 | Name: "e99", 63 | Password: utils.AddSalt("qwe1qwe2qwe3"), 64 | Token: managerToken, 65 | IsCheck: false, 66 | }) 67 | 68 | // Refresh the dynamic config from the database. 69 | dynamic_config.Init() 70 | 71 | bootstrap.GameToTimerBridge() 72 | timer.Init() 73 | 74 | asteroid.Init(game.AsteroidGreetData) 75 | 76 | // Cache 77 | store.Init() 78 | webhook.RefreshWebHookStore() 79 | 80 | // Live log 81 | livelog.Init() 82 | 83 | // Web router. 84 | router = route.Init() 85 | } 86 | -------------------------------------------------------------------------------- /test/challenge_test.go: -------------------------------------------------------------------------------- 1 | package cardinal_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_NewChallenge(t *testing.T) { 14 | // JSON bind fail 15 | w := httptest.NewRecorder() 16 | jsonData, _ := json.Marshal(map[string]interface{}{ 17 | "Title": "Web1", 18 | }) 19 | req, _ := http.NewRequest("POST", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 20 | req.Header.Set("Authorization", managerToken) 21 | router.ServeHTTP(w, req) 22 | assert.Equal(t, 400, w.Code) 23 | 24 | // success 25 | w = httptest.NewRecorder() 26 | jsonData, _ = json.Marshal(map[string]interface{}{ 27 | "Title": "Web1", 28 | "BaseScore": 800, 29 | }) 30 | req, _ = http.NewRequest("POST", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 31 | req.Header.Set("Authorization", managerToken) 32 | router.ServeHTTP(w, req) 33 | assert.Equal(t, 200, w.Code) 34 | 35 | w = httptest.NewRecorder() 36 | jsonData, _ = json.Marshal(map[string]interface{}{ 37 | "Title": "Pwn2", 38 | "BaseScore": 1000, 39 | }) 40 | req, _ = http.NewRequest("POST", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 41 | req.Header.Set("Authorization", managerToken) 42 | router.ServeHTTP(w, req) 43 | assert.Equal(t, 200, w.Code) 44 | 45 | w = httptest.NewRecorder() 46 | jsonData, _ = json.Marshal(map[string]interface{}{ 47 | "Title": "Pwn1", 48 | "BaseScore": 1000, 49 | }) 50 | req, _ = http.NewRequest("POST", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 51 | req.Header.Set("Authorization", managerToken) 52 | router.ServeHTTP(w, req) 53 | assert.Equal(t, 200, w.Code) 54 | 55 | // repeat 56 | w = httptest.NewRecorder() 57 | jsonData, _ = json.Marshal(map[string]interface{}{ 58 | "Title": "Web1", 59 | "BaseScore": 800, 60 | }) 61 | req, _ = http.NewRequest("POST", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 62 | req.Header.Set("Authorization", managerToken) 63 | router.ServeHTTP(w, req) 64 | assert.Equal(t, 403, w.Code) 65 | } 66 | 67 | func Test_EditChallenge(t *testing.T) { 68 | // JSON bind fail 69 | w := httptest.NewRecorder() 70 | jsonData, _ := json.Marshal(map[string]interface{}{ 71 | "Title": "Web1", 72 | "BaseScore": 1000, 73 | }) 74 | req, _ := http.NewRequest("PUT", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 75 | req.Header.Set("Authorization", managerToken) 76 | router.ServeHTTP(w, req) 77 | assert.Equal(t, 400, w.Code) 78 | 79 | // not found 80 | w = httptest.NewRecorder() 81 | jsonData, _ = json.Marshal(map[string]interface{}{ 82 | "ID": 233, 83 | "Title": "Web1", 84 | "BaseScore": 1000, 85 | }) 86 | req, _ = http.NewRequest("PUT", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 87 | req.Header.Set("Authorization", managerToken) 88 | router.ServeHTTP(w, req) 89 | assert.Equal(t, 404, w.Code) 90 | 91 | // success 92 | w = httptest.NewRecorder() 93 | jsonData, _ = json.Marshal(map[string]interface{}{ 94 | "ID": 1, 95 | "Title": "Web233", 96 | "BaseScore": 800, 97 | }) 98 | req, _ = http.NewRequest("PUT", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 99 | req.Header.Set("Authorization", managerToken) 100 | router.ServeHTTP(w, req) 101 | assert.Equal(t, 200, w.Code) 102 | 103 | w = httptest.NewRecorder() 104 | jsonData, _ = json.Marshal(map[string]interface{}{ 105 | "ID": 1, 106 | "Title": "Web1", 107 | "BaseScore": 1000, 108 | }) 109 | req, _ = http.NewRequest("PUT", "/api/manager/challenge", bytes.NewBuffer(jsonData)) 110 | req.Header.Set("Authorization", managerToken) 111 | router.ServeHTTP(w, req) 112 | assert.Equal(t, 200, w.Code) 113 | } 114 | 115 | func Test_GetAllChallenges(t *testing.T) { 116 | w := httptest.NewRecorder() 117 | req, _ := http.NewRequest("GET", "/api/manager/challenges", nil) 118 | req.Header.Set("Authorization", managerToken) 119 | router.ServeHTTP(w, req) 120 | assert.Equal(t, 200, w.Code) 121 | } 122 | 123 | func Test_DeleteChallenge(t *testing.T) { 124 | // error id 125 | w := httptest.NewRecorder() 126 | req, _ := http.NewRequest("DELETE", "/api/manager/challenge?id=asdfg", nil) 127 | req.Header.Set("Authorization", managerToken) 128 | router.ServeHTTP(w, req) 129 | assert.Equal(t, 400, w.Code) 130 | 131 | // id not exist 132 | w = httptest.NewRecorder() 133 | req, _ = http.NewRequest("DELETE", "/api/manager/challenge?id=233", nil) 134 | req.Header.Set("Authorization", managerToken) 135 | router.ServeHTTP(w, req) 136 | assert.Equal(t, 404, w.Code) 137 | 138 | // success delete 2 139 | w = httptest.NewRecorder() 140 | req, _ = http.NewRequest("DELETE", "/api/manager/challenge?id=2", nil) 141 | req.Header.Set("Authorization", managerToken) 142 | router.ServeHTTP(w, req) 143 | assert.Equal(t, 200, w.Code) 144 | } 145 | 146 | func Test_SetVisible(t *testing.T) { 147 | // payload error 148 | w := httptest.NewRecorder() 149 | jsonData, _ := json.Marshal(map[string]interface{}{ 150 | "ID": 1, 151 | "Visible": "true", 152 | }) 153 | req, _ := http.NewRequest("POST", "/api/manager/challenge/visible", bytes.NewBuffer(jsonData)) 154 | req.Header.Set("Authorization", managerToken) 155 | router.ServeHTTP(w, req) 156 | assert.Equal(t, 400, w.Code) 157 | 158 | // challenge not found 159 | w = httptest.NewRecorder() 160 | jsonData, _ = json.Marshal(map[string]interface{}{ 161 | "ID": 2, 162 | "Visible": true, 163 | }) 164 | req, _ = http.NewRequest("POST", "/api/manager/challenge/visible", bytes.NewBuffer(jsonData)) 165 | req.Header.Set("Authorization", managerToken) 166 | router.ServeHTTP(w, req) 167 | assert.Equal(t, 404, w.Code) 168 | } 169 | -------------------------------------------------------------------------------- /test/log_test.go: -------------------------------------------------------------------------------- 1 | package cardinal_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_GetLogs(t *testing.T) { 12 | w := httptest.NewRecorder() 13 | req, _ := http.NewRequest("GET", "/api/manager/logs", nil) 14 | req.Header.Set("Authorization", managerToken) 15 | router.ServeHTTP(w, req) 16 | assert.Equal(t, 200, w.Code) 17 | } 18 | 19 | func Test_Panel(t *testing.T) { 20 | w := httptest.NewRecorder() 21 | req, _ := http.NewRequest("GET", "/api/manager/panel", nil) 22 | req.Header.Set("Authorization", managerToken) 23 | router.ServeHTTP(w, req) 24 | assert.Equal(t, 200, w.Code) 25 | } 26 | 27 | func Test_Panel2(t *testing.T) { 28 | // Test general router 29 | //var backJSON = struct { 30 | // Error int `json:"error"` 31 | // Msg string `json:"msg"` 32 | // Data string `json:"data"` 33 | //}{} 34 | 35 | w := httptest.NewRecorder() 36 | req, _ := http.NewRequest("GET", "/api/", nil) 37 | router.ServeHTTP(w, req) 38 | assert.Equal(t, 200, w.Code) 39 | 40 | w = httptest.NewRecorder() 41 | req, _ = http.NewRequest("GET", "/api/base", nil) 42 | router.ServeHTTP(w, req) 43 | assert.Equal(t, 200, w.Code) 44 | //err := json.Unmarshal(w.Body.Bytes(), &backJSON) 45 | //assert.Equal(t, nil, err) 46 | //assert.Equal(t, backJSON.Data, "HCTF") 47 | 48 | w = httptest.NewRecorder() 49 | req, _ = http.NewRequest("GET", "/api/time", nil) 50 | router.ServeHTTP(w, req) 51 | assert.Equal(t, 200, w.Code) 52 | 53 | w = httptest.NewRecorder() 54 | req, _ = http.NewRequest("GET", "/api/404_not_found_router", nil) 55 | router.ServeHTTP(w, req) 56 | assert.Equal(t, 404, w.Code) 57 | 58 | // no auth 59 | w = httptest.NewRecorder() 60 | req, _ = http.NewRequest("GET", "/api/manager/flags", nil) 61 | req.Header.Set("Authorization", "error_token") 62 | router.ServeHTTP(w, req) 63 | assert.Equal(t, 401, w.Code) 64 | 65 | w = httptest.NewRecorder() 66 | req, _ = http.NewRequest("GET", "/api/manager/flags", nil) 67 | req.Header.Set("Authorization", "") 68 | router.ServeHTTP(w, req) 69 | assert.Equal(t, 403, w.Code) 70 | 71 | w = httptest.NewRecorder() 72 | req, _ = http.NewRequest("GET", "/api/team/rank", nil) 73 | req.Header.Set("Authorization", "error_token") 74 | router.ServeHTTP(w, req) 75 | assert.Equal(t, 401, w.Code) 76 | 77 | w = httptest.NewRecorder() 78 | req, _ = http.NewRequest("GET", "/api/team/rank", nil) 79 | req.Header.Set("Authorization", "") 80 | router.ServeHTTP(w, req) 81 | assert.Equal(t, 403, w.Code) 82 | } 83 | --------------------------------------------------------------------------------