├── .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 | [](https://cardinal.ink)
2 |
3 | [](https://github.com/vidar-team/Cardinal/actions/workflows/go.yml)
4 | [](https://github.com/vidar-team/Cardinal/actions/workflows/codeql.yml)
5 | [](https://codecov.io/gh/vidar-team/Cardinal)
6 | [](https://goreportcard.com/report/github.com/vidar-team/Cardinal)
7 | [](https://crowdin.com/project/cardinal)
8 | [](https://sourcegraph.com/github.com/vidar-team/Cardinal)
9 | [](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 | 
27 |
28 |
29 | 更多图片
30 |
31 | 
32 |
33 | 
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 |
--------------------------------------------------------------------------------