├── web
├── admin
│ ├── public
│ │ ├── favicon.ico
│ │ ├── manifest.json
│ │ └── index.html
│ ├── src
│ │ ├── components
│ │ │ ├── markdown-editor
│ │ │ │ ├── MarkdownEditor.css
│ │ │ │ ├── MarkdownEditor.module.css
│ │ │ │ └── MarkdownEditor.js
│ │ │ ├── pagination
│ │ │ │ ├── Pagination.module.css
│ │ │ │ └── Pagination.js
│ │ │ ├── filter-icon
│ │ │ │ ├── FilterIcon.module.css
│ │ │ │ └── FilterIcon.js
│ │ │ ├── flag-icon
│ │ │ │ └── FlagIcon.js
│ │ │ ├── filter-dropdown
│ │ │ │ ├── FilterDropdown.module.css
│ │ │ │ └── FilterDropdown.js
│ │ │ ├── index.js
│ │ │ ├── with-fields
│ │ │ │ └── withFields.js
│ │ │ └── filters
│ │ │ │ └── withFilters.js
│ │ ├── scenes
│ │ │ ├── scenes
│ │ │ │ ├── login
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ └── login-form
│ │ │ │ │ │ │ └── LoginForm.module.css
│ │ │ │ │ ├── Login.module.css
│ │ │ │ │ └── Login.js
│ │ │ │ ├── index.js
│ │ │ │ └── main
│ │ │ │ │ ├── scenes
│ │ │ │ │ ├── user-create-edit
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── challenge-create-edit
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ ├── index.js
│ │ │ │ │ │ │ └── challenge-form
│ │ │ │ │ │ │ └── ChallengeForm.module.css
│ │ │ │ │ ├── announcement-create-edit
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ └── index.js
│ │ │ │ │ ├── challenges-table
│ │ │ │ │ │ └── ChallengesTable.module.css
│ │ │ │ │ ├── announcements-table
│ │ │ │ │ │ └── AnnouncementsTable.module.css
│ │ │ │ │ ├── dashboard
│ │ │ │ │ │ └── Dashboard.js
│ │ │ │ │ ├── users-table
│ │ │ │ │ │ └── UsersTable.module.css
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── files-tree
│ │ │ │ │ │ └── FilesTree.js
│ │ │ │ │ ├── components
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── sidebar
│ │ │ │ │ │ ├── Sidebar.module.css
│ │ │ │ │ │ └── Sidebar.js
│ │ │ │ │ └── nav
│ │ │ │ │ │ └── Nav.js
│ │ │ │ │ └── Main.module.css
│ │ │ └── App.js
│ │ ├── index.html
│ │ ├── models
│ │ │ ├── index.js
│ │ │ ├── game.js
│ │ │ ├── auth.js
│ │ │ ├── files.js
│ │ │ ├── announcements.js
│ │ │ ├── challenges.js
│ │ │ └── users.js
│ │ ├── utils
│ │ │ ├── api.js
│ │ │ ├── object.js
│ │ │ ├── form.js
│ │ │ └── rematch-api.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── images
│ │ │ ├── logo-small.svg
│ │ │ ├── logo-big.svg
│ │ │ └── logo-big-dark.svg
│ ├── .gitignore
│ ├── .eslintrc
│ └── package.json
└── public
│ ├── public
│ ├── favicon.ico
│ ├── manifest.json
│ └── index.html
│ ├── src
│ ├── utils
│ │ ├── object.js
│ │ ├── date.js
│ │ ├── api.js
│ │ ├── form.js
│ │ └── rematch-api.js
│ ├── scenes
│ │ ├── components
│ │ │ ├── index.js
│ │ │ ├── RequireAuth.js
│ │ │ ├── Nav.js
│ │ │ └── UserStats.js
│ │ └── scenes
│ │ │ ├── index.js
│ │ │ ├── auth
│ │ │ ├── Logout.js
│ │ │ ├── Activate.js
│ │ │ ├── Auth.js
│ │ │ ├── ResetPassword.js
│ │ │ ├── SendToken.js
│ │ │ ├── SendTokenForm.js
│ │ │ ├── ResetPasswordForm.js
│ │ │ ├── Login.js
│ │ │ ├── Signup.js
│ │ │ └── LoginForm.js
│ │ │ ├── rules
│ │ │ └── Rules.js
│ │ │ ├── challenge
│ │ │ └── FlagForm.js
│ │ │ ├── news
│ │ │ └── Countdown.js
│ │ │ ├── scoreboard
│ │ │ └── Scoreboard.js
│ │ │ └── challenges
│ │ │ └── Challenges.js
│ ├── components
│ │ ├── spinner
│ │ │ └── Spinner.js
│ │ ├── divider
│ │ │ └── Divider.js
│ │ ├── not-found
│ │ │ └── NotFound.js
│ │ ├── index.js
│ │ ├── window
│ │ │ └── Window.js
│ │ ├── loading
│ │ │ └── Loading.js
│ │ ├── page
│ │ │ └── Page.js
│ │ ├── button
│ │ │ └── Button.js
│ │ ├── form-item
│ │ │ └── FormItem.js
│ │ └── layout
│ │ │ └── Layout.js
│ ├── models
│ │ ├── index.js
│ │ ├── contest.js
│ │ ├── scores.js
│ │ ├── user.js
│ │ ├── challenges.js
│ │ ├── news.js
│ │ └── auth.js
│ └── index.js
│ ├── .gitignore
│ ├── package.json
│ └── README.md
├── .dockerignore
├── models
├── fixtures
│ ├── likes.yml
│ ├── announcements.yml
│ ├── tokens.yml
│ ├── solutions.yml
│ ├── challenges.yml
│ └── users.yml
├── utils.go
├── models.go
├── solutions.go
├── migrations
│ ├── migrations.go
│ └── 1489143364_initial_schema.down.sql
├── likes.go
├── solutions_test.go
├── likes_test.go
├── models_test.go
├── tokens.go
├── announcements_test.go
└── scores.go
├── main.go
├── controllers
├── schemas
│ ├── users
│ │ └── get.json
│ ├── Users.json
│ ├── Scores.json
│ ├── admin
│ │ ├── challenges
│ │ │ ├── getOne.json
│ │ │ ├── getAll.json
│ │ │ ├── update.json
│ │ │ └── create.json
│ │ ├── users
│ │ │ ├── getAll.json
│ │ │ ├── getOne.json
│ │ │ ├── update.json
│ │ │ └── create.json
│ │ ├── auth
│ │ │ └── login.json
│ │ └── announcements
│ │ │ ├── create.json
│ │ │ └── update.json
│ ├── Announcements.json
│ ├── Challenges.json
│ ├── Solution.json
│ ├── AdminAuthLogin.json
│ ├── UserSolutionsCreate.json
│ ├── AuthActivate.json
│ ├── ChallengeUser.json
│ ├── AuthLogin.json
│ ├── AuthResetPassword.json
│ ├── UserPublic.json
│ ├── ChallengeE.json
│ ├── ChallengeMeta.json
│ ├── AuthSendToken.json
│ ├── Score.json
│ ├── GameInfo.json
│ ├── AuthRegister.json
│ ├── AdminUsersCreate.json
│ ├── Error.json
│ ├── Announcement.json
│ ├── User.json
│ ├── ScoresCTFTime.json
│ └── Challenge.json
├── admin_game.go
├── game_test.go
├── announcements_test.go
├── params.go
├── validations.go
├── scores_test.go
├── pagination.go
├── file.go
├── utils.go
├── game.go
├── challenges_test.go
├── announcements.go
├── admin_files.go
├── admin_auth.go
├── challenges.go
├── scores.go
├── admin_announcements.go
├── middlewares.go
└── error.go
├── README.md
├── internal
├── crypto
│ ├── crypto.go
│ ├── password.go
│ └── flag.go
└── mailer
│ └── mock
│ └── Sender.go
├── middlewares
├── schemacheck
│ ├── error.go
│ ├── options.go
│ └── handler.go
├── csrf
│ ├── context.go
│ ├── token.go
│ ├── options.go
│ └── handler.go
├── cntcheck
│ ├── options.go
│ └── handler.go
├── timecheck
│ ├── options.go
│ └── handler.go
└── recaptcha
│ ├── handler.go
│ ├── options.go
│ └── check.go
├── .gitignore
├── .github
└── workflows
│ └── build.yml
├── Dockerfile
├── config
└── helpers.go
├── Makefile
├── templates
└── email
│ └── ext
│ ├── reset.tmpl
│ └── activate.tmpl
└── go.mod
/web/admin/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctf-zone/ctfzone/HEAD/web/admin/public/favicon.ico
--------------------------------------------------------------------------------
/web/public/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ctf-zone/ctfzone/HEAD/web/public/public/favicon.ico
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | files/
2 | web/admin/dist/
3 | web/public/dist/
4 | web/admin/node_modules/
5 | web/public/node_modules/
6 |
--------------------------------------------------------------------------------
/web/public/src/utils/object.js:
--------------------------------------------------------------------------------
1 | export const isEmpty = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object
2 |
--------------------------------------------------------------------------------
/models/fixtures/likes.yml:
--------------------------------------------------------------------------------
1 | - user_id: 1 # LC↯BC
2 | challenge_id: 1 # web-100
3 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
4 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ctf-zone/ctfzone/cmd"
5 | )
6 |
7 | func main() {
8 | cmd.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/web/admin/src/components/markdown-editor/MarkdownEditor.css:
--------------------------------------------------------------------------------
1 | @import '~codemirror/lib/codemirror.css';
2 | @import '~codemirror/theme/idea.css';
3 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/login/components/index.js:
--------------------------------------------------------------------------------
1 | import LoginForm from './login-form/LoginForm'
2 |
3 | export {
4 | LoginForm,
5 | }
6 |
--------------------------------------------------------------------------------
/web/admin/src/components/pagination/Pagination.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | display: flex;
3 | justify-content: center;
4 | padding: 20px;
5 | }
6 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/index.js:
--------------------------------------------------------------------------------
1 | import Login from './login/Login'
2 | import Main from './main/Main'
3 |
4 | export {
5 | Login,
6 | Main,
7 | }
8 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/user-create-edit/components/index.js:
--------------------------------------------------------------------------------
1 | import UserForm from "./user-form/UserForm";
2 |
3 | export { UserForm };
4 |
--------------------------------------------------------------------------------
/controllers/schemas/users/get.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Users",
3 | "description": "List of users",
4 | "type": "object",
5 | "$ref": "file:///User.json"
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CTFZone
2 |
3 | [](https://travis-ci.com/ctf-zone/ctfzone)
4 |
5 | Jeopardy CTF platform.
6 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/components/index.js:
--------------------------------------------------------------------------------
1 | import Nav from './nav/Nav'
2 | import Sidebar from './sidebar/Sidebar'
3 |
4 | export {
5 | Nav,
6 | Sidebar,
7 | }
8 |
--------------------------------------------------------------------------------
/controllers/schemas/Users.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Users",
3 | "description": "List of users",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///User.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/public/src/utils/date.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import relativeTime from 'dayjs/plugin/relativeTime';
3 |
4 | dayjs.extend(relativeTime);
5 |
6 | export default dayjs;
7 |
--------------------------------------------------------------------------------
/controllers/schemas/Scores.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Scores",
3 | "description": "List of scores",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///Score.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/challenges/getOne.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminChallengesGetOne",
3 | "description": "Get one challenge response",
4 | "$ref": "file:///ChallengeE.json"
5 | }
6 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/challenge-create-edit/components/index.js:
--------------------------------------------------------------------------------
1 | import ChallengeForm from './challenge-form/ChallengeForm'
2 |
3 | export {
4 | ChallengeForm,
5 | }
6 |
--------------------------------------------------------------------------------
/internal/crypto/crypto.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import "errors"
4 |
5 | var (
6 | ErrInvalidHash = errors.New("Invalid hash format")
7 | ErrMismatch = errors.New("Hash mismatch")
8 | )
9 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/announcement-create-edit/components/index.js:
--------------------------------------------------------------------------------
1 | import AnnouncementForm from './announcement-form/AnnouncementForm'
2 |
3 | export {
4 | AnnouncementForm,
5 | }
6 |
--------------------------------------------------------------------------------
/web/public/src/scenes/components/index.js:
--------------------------------------------------------------------------------
1 | import Nav from './Nav';
2 | import RequireAuth from './RequireAuth';
3 | import UserStats from './UserStats';
4 |
5 | export { Nav, RequireAuth, UserStats };
6 |
--------------------------------------------------------------------------------
/controllers/schemas/Announcements.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Announcements",
3 | "description": "Announcements",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///Announcement.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/admin/src/components/filter-icon/FilterIcon.module.css:
--------------------------------------------------------------------------------
1 | .active {
2 | color: var(--filter-color-active) !important;
3 | }
4 |
5 | .inactive {
6 | color: var(--filter-color-inactive) !important;
7 | }
8 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/users/getAll.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminsUsersGetAll",
3 | "description": "Get all users respones",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///User.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/users/getOne.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminsUsersGetOne",
3 | "description": "Get one user respones",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///User.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/controllers/schemas/Challenges.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "ChallengesExt",
3 | "description": "List of challenges with metadata",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///ChallengeE.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/challenges/getAll.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminChallengesGetAll",
3 | "description": "Get all challenges response",
4 | "type": "array",
5 | "items": {
6 | "$ref": "file:///ChallengeE.json"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/admin/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # testing
5 | /coverage
6 |
7 | # production
8 | /build
9 |
10 | # cache
11 | /.cache
12 | .eslintcache
13 |
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
--------------------------------------------------------------------------------
/web/public/src/components/spinner/Spinner.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | class Spinner extends Component {
4 | render() {
5 | return
;
6 | }
7 | }
8 |
9 | export default Spinner;
10 |
--------------------------------------------------------------------------------
/models/utils.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | func now() time.Time {
8 | // PostgreSQL currently can't store nanoseconds
9 | // https://github.com/lib/pq/issues/227
10 | return time.Now().UTC().Truncate(time.Microsecond)
11 | }
12 |
--------------------------------------------------------------------------------
/web/public/src/components/divider/Divider.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class Divider extends Component {
4 | render() {
5 | return (
6 |
7 | )
8 | }
9 | }
10 |
11 | export default Divider
12 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/login/components/login-form/LoginForm.module.css:
--------------------------------------------------------------------------------
1 | .logo {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | padding: 10px;
6 | }
7 |
8 | .logo img {
9 | height: 100px;
10 | }
11 |
12 | .submit {
13 | width: 100%;
14 | }
15 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/Main.module.css:
--------------------------------------------------------------------------------
1 | .content {
2 | padding: 24px;
3 | }
4 |
5 | .body {
6 | padding: 10px;
7 | background-color: #fff;
8 | }
9 |
10 | .spin {
11 | height: 100vh;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | }
16 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/challenges-table/ChallengesTable.module.css:
--------------------------------------------------------------------------------
1 | .controls {
2 | padding: 10px 0;
3 | }
4 |
5 | th, td {
6 | text-align: center !important;
7 | }
8 |
9 | .controlsLeft {
10 | text-align: left;
11 | }
12 |
13 | .controlsRight {
14 | text-align: right;
15 | }
16 |
--------------------------------------------------------------------------------
/web/public/src/components/not-found/NotFound.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class NotFound extends Component {
4 | render() {
5 | return (
6 |
7 |
404 Page Not Found
8 |
9 | )
10 | }
11 | }
12 |
13 | export default NotFound
14 |
--------------------------------------------------------------------------------
/web/admin/src/components/flag-icon/FlagIcon.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import FlagIconFactory, { functions } from 'react-flag-icon-css'
3 |
4 | const FlagIcon = FlagIconFactory(React, { useCssModules: false })
5 |
6 | export const countries = functions.countries.getCountries()
7 | export default FlagIcon
8 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/announcements-table/AnnouncementsTable.module.css:
--------------------------------------------------------------------------------
1 | .controls {
2 | padding: 10px 0;
3 | }
4 |
5 | th, td {
6 | text-align: center !important;
7 | }
8 |
9 | .controlsLeft {
10 | text-align: left;
11 | }
12 |
13 | .controlsRight {
14 | text-align: right;
15 | }
16 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/dashboard/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | class Dashboard extends Component {
4 | render() {
5 | return (
6 |
7 |
Dashboard
8 |
9 | )
10 | }
11 | }
12 |
13 | export default Dashboard
14 |
--------------------------------------------------------------------------------
/controllers/schemas/Solution.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Solution",
3 | "description": "Solution model",
4 | "type": "object",
5 | "properties": {
6 | "user": {
7 | "$ref": "file:///UserPublic.json"
8 | },
9 | "challenge": {
10 | "$ref": "file:///ChallengeExt.json"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/web/admin/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | M⭑CTF 2018
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/controllers/schemas/AdminAuthLogin.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminAuthLogin",
3 | "description": "AdminAuthLogin request schema",
4 | "type": "object",
5 | "properties": {
6 | "password": {
7 | "type": "string"
8 | }
9 | },
10 | "additionalProperties": false,
11 | "required": [
12 | "password"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/controllers/schemas/UserSolutionsCreate.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "UserSolutionsCreate",
3 | "description": "User solutions create request",
4 | "type": "object",
5 | "properties": {
6 | "flag": {
7 | "type": "string"
8 | }
9 | },
10 | "additionalProperties": false,
11 | "required": [
12 | "flag"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/auth/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminAuthLogin",
3 | "description": "AdminAuthLogin request schema",
4 | "type": "object",
5 | "properties": {
6 | "password": {
7 | "type": "string"
8 | }
9 | },
10 | "additionalProperties": false,
11 | "required": [
12 | "password"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/web/public/src/models/index.js:
--------------------------------------------------------------------------------
1 | import auth from './auth'
2 | import challenges from './challenges'
3 | import news from './news'
4 | import user from './user'
5 | import scores from './scores'
6 | import contest from './contest'
7 |
8 | export default {
9 | auth,
10 | challenges,
11 | news,
12 | user,
13 | scores,
14 | contest,
15 | }
16 |
--------------------------------------------------------------------------------
/controllers/schemas/AuthActivate.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AuthActivate",
3 | "description": "AuthActivate request schema",
4 | "type": "object",
5 | "properties": {
6 | "token": {
7 | "type": "string",
8 | "pattern": "^[0-9a-f]{64}$"
9 | }
10 | },
11 | "additionalProperties": false,
12 | "required": [
13 | "token"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/web/admin/src/components/markdown-editor/MarkdownEditor.module.css:
--------------------------------------------------------------------------------
1 | .edit {
2 | border: 1px solid var(--field-border-color);
3 | border-radius: 4px;
4 | line-height: 1.5 !important;
5 | }
6 |
7 | .preview {
8 | border: 1px solid var(--field-border-color);
9 | border-radius: 4px;
10 | height: 302px;
11 | overflow-y: scroll;
12 | padding: 24px;
13 | }
14 |
--------------------------------------------------------------------------------
/web/admin/src/models/index.js:
--------------------------------------------------------------------------------
1 | import auth from './auth'
2 | import challenges from './challenges'
3 | import users from './users'
4 | import game from './game'
5 | import files from './files'
6 | import announcements from './announcements'
7 |
8 | export default {
9 | auth,
10 | challenges,
11 | users,
12 | game,
13 | files,
14 | announcements,
15 | }
16 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/index.js:
--------------------------------------------------------------------------------
1 | import Auth from './auth/Auth';
2 | import Challenge from './challenge/Challenge';
3 | import Challenges from './challenges/Challenges';
4 | import News from './news/News';
5 | import Rules from './rules/Rules';
6 | import Scoreboard from './scoreboard/Scoreboard';
7 |
8 | export { Auth, Challenge, Challenges, News, Rules, Scoreboard };
9 |
--------------------------------------------------------------------------------
/middlewares/schemacheck/error.go:
--------------------------------------------------------------------------------
1 | package schemacheck
2 |
3 | type FieldError struct {
4 | Field string
5 | Msg string
6 | }
7 |
8 | type Error struct {
9 | Msg string
10 | Errs []*FieldError
11 | }
12 |
13 | func (e *Error) Error() string {
14 | s := e.Msg + "; "
15 | for _, e := range e.Errs {
16 | s += e.Field + ": " + e.Msg + ";"
17 | }
18 | return s
19 | }
20 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/login/Login.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | height: 100vh;
3 | background-color: var(--light-background);
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | }
8 |
9 | .form {
10 | width: 300px;
11 | padding: 20px;
12 | box-shadow: 0 0 100px rgba(0, 0, 0, .08);
13 | background-color: var(--dark-background);
14 | }
15 |
--------------------------------------------------------------------------------
/controllers/admin_game.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/ctf-zone/ctfzone/config"
7 | )
8 |
9 | func AdminGameInfo(c *config.Game) http.HandlerFunc {
10 | return func(w http.ResponseWriter, r *http.Request) {
11 | if err := responseJSON(w, c); err != nil {
12 | handleError(w, r, ErrInternal.SetError(err))
13 | return
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/web/admin/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "CTFZone Admin",
3 | "name": "CTFZOne Admin Panel",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/web/public/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/controllers/game_test.go:
--------------------------------------------------------------------------------
1 | package controllers_test
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestGameInfo(t *testing.T) {
8 | setup(t)
9 | defer teardown(t)
10 |
11 | e := heDefault(t)
12 | e = heHost(e, "ctfzone.test")
13 | e = heCSRF(e)
14 |
15 | res := e.GET("/api/game").
16 | Expect().
17 | Status(200)
18 |
19 | checkJSONSchema(t, "GameInfo.json", res.Body().Raw())
20 | }
21 |
--------------------------------------------------------------------------------
/web/admin/src/models/game.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 |
3 | export default {
4 | state: {},
5 | reducers: {
6 | set: (state, payload) => {
7 | return { ...state, ...payload }
8 | },
9 | },
10 | effects: {
11 | async get() {
12 | const response = await api.get('game')
13 | const game = response.data
14 | this.set(game)
15 | return game
16 | },
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/controllers/schemas/ChallengeUser.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "ChallengeUser",
3 | "description": "Challenge metadata for current user",
4 | "type": "object",
5 | "properties": {
6 | "isSolved": {
7 | "type": "boolean"
8 | },
9 | "isLiked": {
10 | "type": "boolean"
11 | }
12 | },
13 | "additionalProperties": false,
14 | "required": [
15 | "isSolved",
16 | "isLiked"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/controllers/schemas/AuthLogin.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AuthLogin",
3 | "description": "AuthLogin request schema",
4 | "type": "object",
5 | "properties": {
6 | "email": {
7 | "type": "string",
8 | "format": "email"
9 | },
10 | "password": {
11 | "type": "string"
12 | }
13 | },
14 | "additionalProperties": false,
15 | "required": [
16 | "email",
17 | "password"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/controllers/announcements_test.go:
--------------------------------------------------------------------------------
1 | package controllers_test
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestAnnouncementsList_Success(t *testing.T) {
8 | setup(t)
9 | defer teardown(t)
10 |
11 | e := heDefault(t)
12 | e = heHost(e, "ctfzone.test")
13 | e = heCSRF(e)
14 |
15 | res := e.GET("/api/announcements").
16 | Expect().
17 | Status(200)
18 |
19 | checkJSONSchema(t, "Announcements.json", res.Body().Raw())
20 | }
21 |
--------------------------------------------------------------------------------
/web/admin/src/components/filter-dropdown/FilterDropdown.module.css:
--------------------------------------------------------------------------------
1 | .dropdown {
2 | border-radius: 6px;
3 | background: #fff;
4 | box-shadow: 0 1px 6px rgba(0, 0, 0, .2);
5 | }
6 |
7 | .container {
8 | padding: 15px;
9 | }
10 |
11 | .buttons {
12 | padding: 7px 15px;
13 | overflow: hidden;
14 | border-top: 1px solid #e8e8e8;
15 | }
16 |
17 | .ok {
18 | float: left;
19 | }
20 |
21 | .reset {
22 | float: right;
23 | }
24 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/users-table/UsersTable.module.css:
--------------------------------------------------------------------------------
1 | .controls {
2 | padding: 10px 0;
3 | }
4 |
5 | th,
6 | td {
7 | text-align: center !important;
8 | }
9 |
10 | .upload {
11 | margin-left: 10px;
12 | }
13 |
14 | .controlsLeft {
15 | text-align: left;
16 | }
17 |
18 | .controlsRight {
19 | text-align: right;
20 | }
21 |
22 | .flag {
23 | border: 1px solid #d9d9d9;
24 | background-size: cover;
25 | }
26 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/announcements/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AnnouncementCreateRequest",
3 | "type": "object",
4 | "properties": {
5 | "title": {
6 | "type": "string"
7 | },
8 | "body": {
9 | "type": "string"
10 | },
11 | "challengeId": {
12 | "type": "integer",
13 | "minimum": 1
14 | }
15 | },
16 | "additionalProperties": false,
17 | "required": [
18 | "title"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/announcements/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AnnouncementUpdateRequest",
3 | "type": "object",
4 | "properties": {
5 | "title": {
6 | "type": "string"
7 | },
8 | "body": {
9 | "type": "string"
10 | },
11 | "challengeId": {
12 | "type": "integer",
13 | "minimum": 1
14 | }
15 | },
16 | "additionalProperties": false,
17 | "required": [
18 | "title"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/web/public/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | .eslintcache
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/web/public/src/models/contest.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api';
2 |
3 | export default {
4 | state: {
5 | status: {}
6 | },
7 | reducers: {
8 | set: (state, payload) => {
9 | return { ...state, ...payload };
10 | }
11 | },
12 | effects: {
13 | async getStatus() {
14 | const response = await api.get('/game');
15 | this.set({ status: response.data });
16 | return response.data;
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/controllers/schemas/AuthResetPassword.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AuthResetPassword",
3 | "description": "AuthResetPassword request schema",
4 | "type": "object",
5 | "properties": {
6 | "token": {
7 | "type": "string",
8 | "pattern": "^[0-9a-f]{64}$"
9 | },
10 | "password": {
11 | "type": "string"
12 | }
13 | },
14 | "additionalProperties": false,
15 | "required": [
16 | "token",
17 | "password"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | import { LoginForm } from './components'
4 |
5 | import styles from './Login.module.css'
6 |
7 | class Login extends Component {
8 | render() {
9 | return (
10 |
15 | )
16 | }
17 | }
18 |
19 | export default Login
20 |
--------------------------------------------------------------------------------
/web/public/src/models/scores.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api';
2 |
3 | export default {
4 | state: {
5 | items: []
6 | },
7 | reducers: {
8 | set: (state, payload) => {
9 | return { ...state, ...payload };
10 | }
11 | },
12 | effects: {
13 | async list() {
14 | const response = await api.get('/scores');
15 | const items = response.data;
16 | this.set({ items });
17 | return items;
18 | }
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/controllers/schemas/UserPublic.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "UserPublic",
3 | "description": "User public model",
4 | "type": "object",
5 | "properties": {
6 | "id": {
7 | "type": "integer",
8 | "minimum": 1
9 | },
10 | "name": {
11 | "type": "string"
12 | },
13 | "extra": {
14 | "type": "object"
15 | }
16 | },
17 | "additionalProperties": false,
18 | "required": [
19 | "id",
20 | "name",
21 | "extra"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/web/public/src/utils/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export default axios.create({
4 | baseURL: process.env.API_URL || '/api',
5 | xsrfCookieName: 'csrf-token',
6 | xsrfHeaderName: 'X-CSRF-Token',
7 | withCredentials: true
8 | });
9 |
10 | export class ApiError extends Error {
11 | constructor(status, message, errors) {
12 | super('ApiError');
13 |
14 | this.status = status;
15 | this.message = message;
16 | this.errors = errors;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/controllers/schemas/ChallengeE.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "ChallengeE",
3 | "description": "Challenge Extended model",
4 | "type": "object",
5 | "properties": {
6 | "challenge": {
7 | "$ref": "file:///Challenge.json"
8 | },
9 | "meta": {
10 | "$ref": "file:///ChallengeMeta.json"
11 | },
12 | "user": {
13 | "$ref": "file:///ChallengeUser.json"
14 | }
15 | },
16 | "additionalProperties": false,
17 | "required": [
18 | "challenge"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/controllers/schemas/ChallengeMeta.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "ChallengeMeta",
3 | "description": "Challenge metadata",
4 | "type": "object",
5 | "properties": {
6 | "solutionsCount": {
7 | "type": "number"
8 | },
9 | "likesCount": {
10 | "type": "number"
11 | },
12 | "hintsCount": {
13 | "type": "number"
14 | }
15 | },
16 | "additionalProperties": false,
17 | "required": [
18 | "solutionsCount",
19 | "likesCount",
20 | "hintsCount"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/middlewares/csrf/context.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "context"
5 | "encoding/base64"
6 | "net/http"
7 | )
8 |
9 | type contextKey int
10 |
11 | const (
12 | tokenKey contextKey = iota
13 | )
14 |
15 | func Token(r *http.Request) string {
16 | return r.Context().Value(tokenKey).(string)
17 | }
18 |
19 | func addTokenToRequest(r *http.Request, token []byte) *http.Request {
20 | return r.WithContext(context.WithValue(r.Context(), tokenKey, base64.URLEncoding.EncodeToString(token)))
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ==========
2 | # = Golang =
3 | # ==========
4 |
5 | # Test binary, build with `go test -c`
6 | *.test
7 |
8 | # Output of the go coverage tool, specifically when used with LiteIDE
9 | *.out
10 |
11 | # ========
12 | # = Misc =
13 | # ========
14 |
15 | *.DS_Store
16 |
17 | # =======
18 | # = App =
19 | # =======
20 |
21 | # Env
22 | .env
23 |
24 | # TLS test file
25 | server.key
26 | server.crt
27 |
28 | # Binary
29 | ctfzone
30 |
31 | # Image
32 | ctfzone.tgz
33 |
34 | # Files
35 | files/
36 |
--------------------------------------------------------------------------------
/controllers/schemas/AuthSendToken.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AuthSendToken",
3 | "description": "AuthSendToken request schema",
4 | "type": "object",
5 | "properties": {
6 | "email": {
7 | "type": "string",
8 | "format": "email"
9 | },
10 | "type": {
11 | "type": "string",
12 | "enum": [
13 | "activate",
14 | "reset"
15 | ]
16 | }
17 | },
18 | "additionalProperties": false,
19 | "required": [
20 | "email",
21 | "type"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/jmoiron/sqlx"
5 | _ "github.com/lib/pq"
6 | )
7 |
8 | type Repository struct {
9 | db *sqlx.DB
10 | }
11 |
12 | func New(dsn string) (*Repository, error) {
13 |
14 | db, err := sqlx.Connect("postgres", dsn)
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return &Repository{db}, nil
20 | }
21 |
22 | func (r *Repository) Close() error {
23 | return r.db.Close()
24 | }
25 |
26 | func (r *Repository) EnableLogging() {
27 | // TODO
28 | }
29 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/challenge-create-edit/components/challenge-form/ChallengeForm.module.css:
--------------------------------------------------------------------------------
1 | @import '~codemirror/lib/codemirror.css';
2 | @import '~codemirror/theme/idea.css';
3 |
4 | .description-edit {
5 | border: 1px solid var(--field-border-color);
6 | border-radius: 4px;
7 | line-height: 1.5 !important;
8 | }
9 |
10 | .description-preview {
11 | border: 1px solid var(--field-border-color);
12 | border-radius: 4px;
13 | height: 302px;
14 | overflow-y: scroll;
15 | padding: 24px;
16 | }
17 |
--------------------------------------------------------------------------------
/web/admin/src/utils/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | export default axios.create({
4 | baseURL: process.env.API_URL || '/api',
5 | xsrfCookieName: 'csrf-token',
6 | xsrfHeaderName: 'X-CSRF-Token',
7 | withCredentials: true,
8 | })
9 |
10 | export class ApiError extends Error {
11 | constructor(status, message, errors) {
12 | super('ApiError')
13 | Error.captureStackTrace(this, ApiError)
14 |
15 | this.status = status
16 | this.message = message
17 | this.errors = errors
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Switch, Route, withRouter } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 |
5 | import * as scenes from './scenes'
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
18 | export default withRouter(connect()(App))
19 |
--------------------------------------------------------------------------------
/controllers/schemas/Score.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Score",
3 | "description": "Score model",
4 | "type": "object",
5 | "properties": {
6 | "user": {
7 | "$ref": "file:///UserPublic.json"
8 | },
9 | "score": {
10 | "type": "number"
11 | },
12 | "rank": {
13 | "type": "integer"
14 | },
15 | "updatedAt": {
16 | "type": "string",
17 | "format": "date-time"
18 | }
19 | },
20 | "additionalProperties": false,
21 | "required": [
22 | "user",
23 | "score"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/middlewares/cntcheck/options.go:
--------------------------------------------------------------------------------
1 | package cntcheck
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | var defaultOptions = &options{
8 | errorFunc: defaultErrorFunc,
9 | }
10 |
11 | type options struct {
12 | errorFunc func(http.ResponseWriter, *http.Request, error)
13 | }
14 |
15 | // Option defines the functional arguments for configuring the middleware.
16 | type Option func(*options)
17 |
18 | func ErrorFunc(f func(http.ResponseWriter, *http.Request, error)) Option {
19 | return func(opts *options) {
20 | opts.errorFunc = f
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/web/admin/src/models/auth.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 |
3 | export default {
4 | state: false,
5 | reducers: {
6 | set: (state, payload) => {
7 | return payload
8 | },
9 | },
10 | effects: {
11 | async login(data) {
12 | await api.post('auth/login', data)
13 | this.set(true)
14 | },
15 | async check() {
16 | await api.get('auth/check')
17 | this.set(true)
18 | },
19 | async logout() {
20 | await api.post('auth/logout')
21 | this.set(false)
22 | },
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/controllers/params.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 |
7 | "github.com/gorilla/schema"
8 | )
9 |
10 | var decoder = schema.NewDecoder()
11 |
12 | func init() {
13 | decoder.RegisterConverter(map[string]interface{}{}, func(input string) reflect.Value {
14 | m := make(map[string]interface{})
15 | for _, pair := range strings.Split(input, ";") {
16 | parts := strings.Split(pair, ":")
17 | if len(parts) == 2 {
18 | m[parts[0]] = parts[1]
19 | }
20 | }
21 | return reflect.ValueOf(m)
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/controllers/schemas/GameInfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "GameInfo",
3 | "description": "Game information",
4 | "type": "object",
5 | "properties": {
6 | "status": {
7 | "type": "string",
8 | "enum": ["countdown", "started", "ended"]
9 | },
10 | "start": {
11 | "type": "string",
12 | "format": "date-time"
13 | },
14 | "end": {
15 | "type": "string",
16 | "format": "date-time"
17 | }
18 | },
19 | "additionalProperties": false,
20 | "required": [
21 | "status",
22 | "start",
23 | "end"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/web/public/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Button from './button/Button';
2 | import FormItem from './form-item/FormItem';
3 | import Layout from './layout/Layout';
4 | import Page from './page/Page';
5 | import Window from './window/Window';
6 | import Spinner from './spinner/Spinner';
7 | import Loading from './loading/Loading';
8 | import Divider from './divider/Divider';
9 | import NotFound from './not-found/NotFound';
10 |
11 | export {
12 | Button,
13 | FormItem,
14 | Layout,
15 | Page,
16 | Window,
17 | Spinner,
18 | Loading,
19 | Divider,
20 | NotFound
21 | };
22 |
--------------------------------------------------------------------------------
/web/public/src/components/window/Window.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Window extends Component {
5 | static propTypes = {
6 | title: PropTypes.string,
7 | type: PropTypes.string,
8 | children: PropTypes.node
9 | };
10 |
11 | render() {
12 | return (
13 |
14 |
{this.props.title}
15 | {this.props.children}
16 |
17 | );
18 | }
19 | }
20 |
21 | export default Window;
22 |
--------------------------------------------------------------------------------
/web/public/src/models/user.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api';
2 |
3 | export default {
4 | state: {
5 | stats: {}
6 | },
7 | reducers: {
8 | set: (state, payload) => {
9 | return { ...state, ...payload };
10 | }
11 | },
12 | effects: {
13 | async getStats() {
14 | const response = await api.get('/user/stats');
15 | const data = response.data;
16 | this.set({ stats: data });
17 | return data;
18 | },
19 | async createSolution({ id, flag }) {
20 | await api.post(`/user/solutions/${id}`, { flag });
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/models/fixtures/announcements.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | title: Test
3 | body: This is announcement
4 | challenge_id: null
5 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
6 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
7 |
8 | - id: 2
9 | title: Latest announcement
10 | body: This is latest announcement
11 | challenge_id: null
12 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
13 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
14 |
15 | - id: 3
16 | title: Hint
17 | body: This is hint
18 | challenge_id: 3
19 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
20 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
21 |
--------------------------------------------------------------------------------
/middlewares/timecheck/options.go:
--------------------------------------------------------------------------------
1 | package timecheck
2 |
3 | import (
4 | "net/http"
5 | "time"
6 | )
7 |
8 | var defaultOptions = &options{
9 | errorFunc: defaultErrorFunc,
10 | }
11 |
12 | type options struct {
13 | start time.Time
14 | end time.Time
15 | errorFunc func(http.ResponseWriter, *http.Request, error)
16 | }
17 |
18 | // Option defines the functional arguments for configuring the middleware.
19 | type Option func(*options)
20 |
21 | func ErrorFunc(f func(http.ResponseWriter, *http.Request, error)) Option {
22 | return func(opts *options) {
23 | opts.errorFunc = f
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/web/admin/src/utils/object.js:
--------------------------------------------------------------------------------
1 | const setPath = ([key, ...next], value, obj) => {
2 | if (next.length === 0) {
3 | return { ...obj, [key]: value }
4 | }
5 | return { ...obj, [key]: setPath(next, value, obj[key]) }
6 | }
7 |
8 | export const set = (path, value, obj) => setPath(path.split('.'), value, obj)
9 |
10 | const unsetPath = ([key, ...next], obj) => {
11 | const { [key]: value, ...rest } = obj
12 | if (next.length === 0) {
13 | return rest
14 | }
15 | return { ...rest, [key]: unsetPath(next, value) }
16 | }
17 |
18 | export const unset = (path, obj) => unsetPath(path.split('.'), obj)
19 |
--------------------------------------------------------------------------------
/internal/crypto/password.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import "golang.org/x/crypto/bcrypt"
4 |
5 | // HashPassword hashes given passwords.
6 | func HashPassword(password string) (string, error) {
7 |
8 | hash, err := bcrypt.GenerateFromPassword(
9 | []byte(password),
10 | bcrypt.DefaultCost,
11 | )
12 | if err != nil {
13 | return "", err
14 | }
15 |
16 | return string(hash), nil
17 | }
18 |
19 | // CheckPassword compares given password and hash.
20 | func CheckPassword(hash, password string) error {
21 | return bcrypt.CompareHashAndPassword(
22 | []byte(hash),
23 | []byte(password),
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/controllers/schemas/AuthRegister.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AuthRegister",
3 | "description": "AuthRegister request schema",
4 | "type": "object",
5 | "properties": {
6 | "name": {
7 | "type": "string",
8 | "minLength": 3
9 | },
10 | "email": {
11 | "type": "string",
12 | "format": "email"
13 | },
14 | "password": {
15 | "type": "string",
16 | "minLength": 8
17 | },
18 | "extra": {
19 | "type": "object"
20 | }
21 | },
22 | "additionalProperties": false,
23 | "required": [
24 | "name",
25 | "email",
26 | "password"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/controllers/validations.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import "github.com/ctf-zone/ctfzone/models"
4 |
5 | func userValidate(db *models.Repository, u *models.User) (bool, map[string][]string) {
6 |
7 | errs := make(map[string][]string)
8 |
9 | if _, err := db.UsersOneByName(u.Name); err == nil {
10 | errs["name"] = []string{"User with such name already exists"}
11 | }
12 |
13 | if _, err := db.UsersOneByEmail(u.Email); err == nil {
14 | errs["email"] = []string{"User with such email already exists"}
15 | }
16 |
17 | if len(errs) > 0 {
18 | return false, errs
19 | }
20 |
21 | return true, nil
22 | }
23 |
--------------------------------------------------------------------------------
/internal/crypto/flag.go:
--------------------------------------------------------------------------------
1 | package crypto
2 |
3 | import (
4 | "crypto/sha256"
5 | "crypto/subtle"
6 | "encoding/hex"
7 | "fmt"
8 | )
9 |
10 | func CheckFlag(hash, flag string) error {
11 | expectedHash, err := hex.DecodeString(hash)
12 | if err != nil {
13 | return ErrInvalidHash
14 | }
15 |
16 | actualHash := sha256.Sum256([]byte(flag))
17 |
18 | if subtle.ConstantTimeCompare(
19 | expectedHash,
20 | actualHash[:],
21 | ) != 1 {
22 | return ErrMismatch
23 | }
24 |
25 | return nil
26 | }
27 |
28 | func HashFlag(flag string) string {
29 | return fmt.Sprintf("%x", sha256.Sum256([]byte(flag)))
30 | }
31 |
--------------------------------------------------------------------------------
/web/public/src/components/loading/Loading.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Spinner from '../spinner/Spinner'
5 |
6 | class Loading extends Component {
7 |
8 | static propTypes = {
9 | children: PropTypes.node,
10 | loading: PropTypes.bool,
11 | }
12 |
13 | render() {
14 | const { loading, children } = this.props
15 |
16 | if (loading) {
17 | return (
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | return children
25 | }
26 | }
27 |
28 | export default Loading
29 |
--------------------------------------------------------------------------------
/internal/mailer/mock/Sender.go:
--------------------------------------------------------------------------------
1 | // Code generated by mockery v1.0.0
2 | package mailer_mock
3 |
4 | import mock "github.com/stretchr/testify/mock"
5 |
6 | // Sender is an autogenerated mock type for the Sender type
7 | type Sender struct {
8 | mock.Mock
9 | }
10 |
11 | // Send provides a mock function with given fields: template, to, data
12 | func (_m *Sender) Send(template string, to string, data interface{}) error {
13 | ret := _m.Called(template, to, data)
14 |
15 | var r0 error
16 | if rf, ok := ret.Get(0).(func(string, string, interface{}) error); ok {
17 | r0 = rf(template, to, data)
18 | } else {
19 | r0 = ret.Error(0)
20 | }
21 |
22 | return r0
23 | }
24 |
--------------------------------------------------------------------------------
/web/admin/src/components/filter-icon/FilterIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Icon } from 'antd'
4 |
5 | import styles from './FilterIcon.module.css'
6 |
7 | class FilterIcon extends Component {
8 | static propTypes = {
9 | isFiltered: PropTypes.bool,
10 | onClick: PropTypes.func,
11 | }
12 |
13 | render() {
14 | const { isFiltered, ...rest } = this.props
15 |
16 | return (
17 |
22 | )
23 | }
24 | }
25 |
26 | export default FilterIcon
27 |
--------------------------------------------------------------------------------
/web/public/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import { init } from '@rematch/core';
5 | import apiPlugin from './utils/rematch-api';
6 | import { Provider } from 'react-redux';
7 |
8 | import { BrowserRouter as Router } from 'react-router-dom';
9 |
10 | import models from './models';
11 | import Main from './scenes/Main';
12 |
13 | import './index.scss';
14 |
15 | const store = init({
16 | models,
17 | plugins: [apiPlugin({})]
18 | });
19 |
20 | ReactDOM.render(
21 |
22 |
23 |
24 |
25 | ,
26 | document.getElementById('root')
27 | );
28 |
--------------------------------------------------------------------------------
/web/public/src/components/page/Page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import Divider from '../divider/Divider'
5 |
6 | class Page extends Component {
7 |
8 | static propTypes = {
9 | children: PropTypes.node,
10 | type: PropTypes.string,
11 | title: PropTypes.string,
12 | }
13 |
14 | render() {
15 | const { type, title, children } = this.props
16 | return (
17 |
18 |
{title}
19 |
20 | {children}
21 |
22 | )
23 | }
24 | }
25 |
26 | export default Page
27 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/users/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminUserUpdate",
3 | "description": "User update request",
4 | "type": "object",
5 | "properties": {
6 | "name": {
7 | "type": "string"
8 | },
9 | "email": {
10 | "type": "string",
11 | "format": "email"
12 | },
13 | "password": {
14 | "type": "string",
15 | "minLength": 8
16 | },
17 | "isActivated": {
18 | "type": "boolean"
19 | },
20 | "extra": {
21 | "type": "object"
22 | }
23 | },
24 | "additionalProperties": false,
25 | "required": [
26 | "name",
27 | "email",
28 | "isActivated",
29 | "extra"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/controllers/schemas/AdminUsersCreate.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminUsersCreate",
3 | "description": "AdminUsersCreate request",
4 | "type": "object",
5 | "properties": {
6 | "name": {
7 | "type": "string"
8 | },
9 | "email": {
10 | "type": "string",
11 | "format": "email"
12 | },
13 | "password": {
14 | "type": "string",
15 | "minLength": 8
16 | },
17 | "isActivated": {
18 | "type": "boolean"
19 | },
20 | "extra": {
21 | "type": "object"
22 | }
23 | },
24 | "additionalProperties": false,
25 | "required": [
26 | "name",
27 | "email",
28 | "password",
29 | "isActivated"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/web/public/src/utils/form.js:
--------------------------------------------------------------------------------
1 | import { createFormField } from 'rc-form';
2 |
3 | export const addErrors = (fields, errors) => {
4 | return Object.assign(
5 | fields,
6 | ...Object.keys(errors).map(field => ({
7 | [field]: createFormField({
8 | ...fields[field],
9 | errors: errors[field].map(e => new Error(e))
10 | })
11 | }))
12 | );
13 | };
14 |
15 | export const mapPropsToFields = ({ fields }) => {
16 | return Object.assign(
17 | {},
18 | ...Object.keys(fields || {}).map(field => {
19 | return {
20 | [field]: createFormField({
21 | ...fields[field]
22 | })
23 | };
24 | })
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/controllers/scores_test.go:
--------------------------------------------------------------------------------
1 | package controllers_test
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestScoresList(t *testing.T) {
8 | setup(t)
9 | defer teardown(t)
10 |
11 | e := heDefault(t)
12 | e = heHost(e, "ctfzone.test")
13 |
14 | res := e.GET("/api/scores").
15 | Expect().
16 | Status(200)
17 |
18 | checkJSONSchema(t, "Scores.json", res.Body().Raw())
19 | }
20 |
21 | func TestScoresCtftimeList(t *testing.T) {
22 | setup(t)
23 | defer teardown(t)
24 |
25 | e := heDefault(t)
26 | e = heHost(e, "ctfzone.test")
27 |
28 | res := e.GET("/api/scores/ctftime").
29 | Expect().
30 | Status(200)
31 |
32 | checkJSONSchema(t, "ScoresCTFTime.json", res.Body().Raw())
33 | }
34 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/components/sidebar/Sidebar.module.css:
--------------------------------------------------------------------------------
1 | .sider {
2 | }
3 |
4 | .logo {
5 | height: 64px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | }
10 |
11 | .logo img {
12 | width: 60px;
13 | }
14 |
15 | .toggle {
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | background-color: var(--trigger-background);
20 | color: var(--text-color-secondary-dark);
21 | padding: 10px;
22 | font-size: 15px;
23 | cursor: pointer;
24 | }
25 |
26 | .toggle:hover {
27 | color: var(--text-color-dark);
28 | }
29 |
30 | .layout {
31 | height: 100%;
32 | background-color: var(--dark-background);
33 | }
34 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/users/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminUsersCreate",
3 | "description": "User create request",
4 | "type": "object",
5 | "properties": {
6 | "name": {
7 | "type": "string"
8 | },
9 | "email": {
10 | "type": "string",
11 | "format": "email"
12 | },
13 | "password": {
14 | "type": "string",
15 | "minLength": 8
16 | },
17 | "isActivated": {
18 | "type": "boolean"
19 | },
20 | "extra": {
21 | "type": "object"
22 | }
23 | },
24 | "additionalProperties": false,
25 | "required": [
26 | "name",
27 | "email",
28 | "password",
29 | "isActivated",
30 | "extra"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/controllers/schemas/Error.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Error",
3 | "description": "Error information",
4 | "type": "object",
5 | "properties": {
6 | "error": {
7 | "type": "string"
8 | },
9 | "fields": {
10 | "type": "array",
11 | "items": {
12 | "type": "object",
13 | "properties": {
14 | "field": {
15 | "type": "string"
16 | },
17 | "error": {
18 | "type": "string"
19 | }
20 | },
21 | "required": [
22 | "field",
23 | "error"
24 | ]
25 | }
26 | }
27 | },
28 | "additionalProperties": false,
29 | "required": [
30 | "error"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/web/admin/src/components/index.js:
--------------------------------------------------------------------------------
1 | import FilterDropdown from './filter-dropdown/FilterDropdown'
2 | import FilterIcon from './filter-icon/FilterIcon'
3 | // import FlagIcon from './flag-icon/FlagIcon'
4 | import Pagination from './pagination/Pagination'
5 | import withFilters from './filters/withFilters'
6 | import MarkdownEditor from './markdown-editor/MarkdownEditor'
7 | import {
8 | textFilter,
9 | radioFilter,
10 | selectFilter,
11 | dateRangeFilter,
12 | } from './filters/filters'
13 |
14 | export {
15 | FilterDropdown,
16 | FilterIcon,
17 | // FlagIcon,
18 | Pagination,
19 | withFilters,
20 | textFilter,
21 | radioFilter,
22 | selectFilter,
23 | dateRangeFilter,
24 | MarkdownEditor,
25 | }
26 |
--------------------------------------------------------------------------------
/web/public/src/components/button/Button.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | class Button extends Component {
6 | static propTypes = {
7 | children: PropTypes.node,
8 | disabled: PropTypes.bool,
9 | onClick: PropTypes.func,
10 | }
11 |
12 | render() {
13 | const { disabled, ...rest } = this.props
14 | const className = classNames({
15 | 'ctf-button': true,
16 | 'ctf-button--disabled': disabled,
17 | })
18 |
19 | return (
20 |
26 | )
27 | }
28 | }
29 |
30 | export default Button
31 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/index.js:
--------------------------------------------------------------------------------
1 | import Dashboard from './dashboard/Dashboard'
2 | import UserCreateEdit from './user-create-edit/UserCreateEdit'
3 | import UsersTable from './users-table/UsersTable'
4 | import ChallengesTable from './challenges-table/ChallengesTable'
5 | import ChallengeCreateEdit from './challenge-create-edit/ChallengeCreateEdit'
6 | import FilesTree from './files-tree/FilesTree'
7 | import AnnouncementsTable from './announcements-table/AnnouncementsTable'
8 | import AnnouncementCreateEdit from './announcement-create-edit/AnnouncementCreateEdit'
9 |
10 | export {
11 | ChallengesTable,
12 | Dashboard,
13 | UserCreateEdit,
14 | UsersTable,
15 | ChallengeCreateEdit,
16 | FilesTree,
17 | AnnouncementsTable,
18 | AnnouncementCreateEdit,
19 | }
20 |
--------------------------------------------------------------------------------
/controllers/schemas/Announcement.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Announcement",
3 | "description": "Announcement model",
4 | "type": "object",
5 | "properties": {
6 | "id": {
7 | "type": "integer",
8 | "minimum": 1
9 | },
10 | "title": {
11 | "type": "string"
12 | },
13 | "body": {
14 | "type": "string"
15 | },
16 | "createdAt": {
17 | "type": "string",
18 | "format": "date-time"
19 | },
20 | "updatedAt": {
21 | "type": "string",
22 | "format": "date-time"
23 | },
24 | "challengeId": {
25 | "type": "integer",
26 | "minimum": 1
27 | }
28 | },
29 | "additionalProperties": false,
30 | "required": [
31 | "id",
32 | "title",
33 | "body",
34 | "createdAt",
35 | "updatedAt"
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/web/admin/src/models/files.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 |
3 | export default {
4 | state: {
5 | files: [],
6 | },
7 | reducers: {
8 | set: (state, payload) => {
9 | return { ...state, ...payload }
10 | },
11 | },
12 | effects: {
13 | async list() {
14 | const response = await api.get('files')
15 | const files = response.data
16 | this.set({ files })
17 | return files
18 | },
19 | async upload(file) {
20 | let formData = new FormData()
21 |
22 | formData.append('file', file)
23 |
24 | const response = await api.post('files', formData, {
25 | headers: {
26 | 'Content-Type': 'multipart/form-data',
27 | },
28 | })
29 |
30 | const fileInfo = response.data
31 |
32 | return fileInfo
33 | },
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/middlewares/schemacheck/options.go:
--------------------------------------------------------------------------------
1 | package schemacheck
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/xeipuuv/gojsonschema"
7 | )
8 |
9 | var defaultOptions = &options{
10 | schema: nil,
11 | errorFunc: defaultErrorFunc,
12 | }
13 |
14 | type options struct {
15 | schema *gojsonschema.Schema
16 | errorFunc func(http.ResponseWriter, *http.Request, error)
17 | }
18 |
19 | // Option defines the functional arguments for configuring the middleware.
20 | type Option func(*options)
21 |
22 | // ErrorFunc allows you to control behavior when recaptcha check failed.
23 | // The default behavior is for a HTTP 403 status code to be written to the
24 | // ResponseWriter along with the plain-text error string.
25 | func ErrorFunc(f func(http.ResponseWriter, *http.Request, error)) Option {
26 | return func(opts *options) {
27 | opts.errorFunc = f
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/web/public/src/scenes/components/RequireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | const RequireAuth = Wrapped => {
7 | class Wrapper extends Component {
8 | static propTypes = {
9 | isLoggedIn: PropTypes.bool,
10 | history: PropTypes.object
11 | };
12 |
13 | componentDidMount() {
14 | const { isLoggedIn, history } = this.props;
15 |
16 | if (!isLoggedIn) {
17 | history.push('/auth/login');
18 | }
19 | }
20 |
21 | render() {
22 | return ;
23 | }
24 | }
25 |
26 | const mapStateToProps = state => ({
27 | isLoggedIn: state.auth
28 | });
29 |
30 | return withRouter(connect(mapStateToProps)(Wrapper));
31 | };
32 |
33 | export default RequireAuth;
34 |
--------------------------------------------------------------------------------
/controllers/pagination.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "strings"
8 |
9 | "github.com/ctf-zone/ctfzone/models"
10 | )
11 |
12 | func addLinkHeader(w http.ResponseWriter, baseURL string, pi *models.PagesInfo, params url.Values) {
13 | links := make([]string, 0)
14 |
15 | if !pi.Prev.IsZero() {
16 | v := params
17 | v.Del("before")
18 | v.Del("after")
19 | v.Set("before", fmt.Sprintf("%v", pi.Prev.Before))
20 | links = append(links, fmt.Sprintf(`<%s>; rel="prev"`, baseURL+"?"+v.Encode()))
21 | }
22 |
23 | if !pi.Next.IsZero() {
24 | v := params
25 | v.Del("before")
26 | v.Del("after")
27 | v.Set("after", fmt.Sprintf("%v", pi.Next.After))
28 | links = append(links, fmt.Sprintf(`<%s>; rel="next"`, baseURL+"?"+v.Encode()))
29 | }
30 |
31 | if len(links) > 0 {
32 | w.Header().Set("Link", strings.Join(links, ", "))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/challenges/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminChallengesUpdate",
3 | "description": "Challenge update request",
4 | "type": "object",
5 | "properties": {
6 | "title": {
7 | "type": "string"
8 | },
9 | "description": {
10 | "type": "string"
11 | },
12 | "categories": {
13 | "type": "array",
14 | "items": {
15 | "type": "string"
16 | }
17 | },
18 | "points": {
19 | "type": "number"
20 | },
21 | "difficulty": {
22 | "type": "string",
23 | "enum": ["easy", "medium", "hard"]
24 | },
25 | "flag": {
26 | "type": "string"
27 | },
28 | "isLocked": {
29 | "type": "boolean"
30 | }
31 | },
32 | "additionalProperties": false,
33 | "required": [
34 | "title",
35 | "categories",
36 | "points",
37 | "difficulty",
38 | "isLocked"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/Logout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter } from 'react-router-dom';
5 |
6 | class Logout extends Component {
7 | static propTypes = {
8 | authLogout: PropTypes.func,
9 | history: PropTypes.object
10 | };
11 |
12 | componentDidMount() {
13 | const { authLogout, history } = this.props;
14 |
15 | authLogout();
16 | history.push('/auth/login');
17 | }
18 |
19 | render() {
20 | return ;
21 | }
22 | }
23 |
24 | const mapStateToProps = state => ({
25 | ...state.api.effects.auth.logout
26 | });
27 |
28 | const mapDispatchToProps = dispatch => ({
29 | authLogout: dispatch.auth.logout
30 | });
31 |
32 | export default withRouter(
33 | connect(
34 | mapStateToProps,
35 | mapDispatchToProps
36 | )(Logout)
37 | );
38 |
--------------------------------------------------------------------------------
/controllers/schemas/admin/challenges/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AdminChallengesCreate",
3 | "description": "Challenge create request",
4 | "type": "object",
5 | "properties": {
6 | "title": {
7 | "type": "string"
8 | },
9 | "description": {
10 | "type": "string"
11 | },
12 | "categories": {
13 | "type": "array",
14 | "items": {
15 | "type": "string"
16 | }
17 | },
18 | "points": {
19 | "type": "number"
20 | },
21 | "difficulty": {
22 | "type": "string",
23 | "enum": ["easy", "medium", "hard"]
24 | },
25 | "flag": {
26 | "type": "string"
27 | },
28 | "isLocked": {
29 | "type": "boolean"
30 | }
31 | },
32 | "additionalProperties": false,
33 | "required": [
34 | "title",
35 | "categories",
36 | "points",
37 | "difficulty",
38 | "flag",
39 | "isLocked"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/web/admin/src/components/filter-dropdown/FilterDropdown.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import styles from './FilterDropdown.module.css'
5 |
6 | class FilterDropdown extends Component {
7 |
8 | static propTypes = {
9 | onConfirm: PropTypes.func,
10 | onReset: PropTypes.func,
11 | children: PropTypes.node,
12 | }
13 |
14 | render() {
15 | return (
16 |
17 |
18 | {this.props.children}
19 |
20 |
24 |
25 | )
26 | }
27 | }
28 |
29 | export default FilterDropdown
30 |
--------------------------------------------------------------------------------
/web/admin/src/index.css:
--------------------------------------------------------------------------------
1 | @import "~antd/dist/antd.css";
2 | @import "~codemirror/lib/codemirror.css";
3 | @import "~codemirror/theme/idea.css";
4 |
5 | :root {
6 | /* TEXT COLORS */
7 | --heading-color: color-mod(#000 alpha(85%));
8 | --text-color: color-mod(#000 alpha(65%));
9 | --text-color-secondary: color-mod(#000 alpha(45%));
10 |
11 | --heading-color-dark: color-mod(#fff alpha(100%));
12 | --text-color-dark: color-mod(#fff alpha(85%));
13 | --text-color-secondary-dark: color-mod(#fff alpha(65%));
14 |
15 | /* BACKGROUND COLORS */
16 | --dark-background: #001529;
17 | --light-background: #f8f8f8;
18 | --trigger-background: #002140;
19 |
20 | /* FILTER ICON */
21 | --filter-color-active: #108ee9;
22 | --filter-color-inactive: #aaa;
23 |
24 | /* FORM */
25 | --field-border-color: #d9d9d9;
26 | }
27 |
28 | html,
29 | body,
30 | #root {
31 | width: 100%;
32 | min-height: 100vh;
33 | }
34 |
--------------------------------------------------------------------------------
/controllers/schemas/User.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "User",
3 | "description": "User model",
4 | "type": "object",
5 | "properties": {
6 | "id": {
7 | "type": "integer",
8 | "minimum": 1
9 | },
10 | "name": {
11 | "type": "string"
12 | },
13 | "email": {
14 | "type": "string",
15 | "format": "email"
16 | },
17 | "password": {
18 | "type": "string",
19 | "minLength": 8
20 | },
21 | "isActivated": {
22 | "type": "boolean"
23 | },
24 | "extra": {
25 | "type": "object"
26 | },
27 | "createdAt": {
28 | "type": "string",
29 | "format": "date-time"
30 | },
31 | "updatedAt": {
32 | "type": "string",
33 | "format": "date-time"
34 | }
35 | },
36 | "additionalProperties": false,
37 | "required": [
38 | "id",
39 | "name",
40 | "email",
41 | "extra",
42 | "isActivated"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/models/fixtures/tokens.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | user_id: 4
3 | token: 1534e7c3fc6b3dbb411a5170b5fa94cd95f324f1ca072853ab2cb34c1378c061
4 | type: activate
5 | expires_at: RAW=NOW() AT TIME ZONE 'UTC'
6 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
7 |
8 | - id: 2
9 | user_id: 4
10 | token: 5b2fae511a73355aff9a99b2164caa343889773150854d6160e4acc6c0137f17
11 | type: activate
12 | expires_at: RAW=NOW() AT TIME ZONE 'UTC' + INTERVAL '1 hour'
13 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
14 |
15 | - id: 3
16 | user_id: 3
17 | token: 150ef2dc5ab146ef23bc1a239fcb7f631006677453782dc779dc8a45a763751f
18 | type: reset
19 | expires_at: RAW=NOW() AT TIME ZONE 'UTC'
20 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
21 |
22 | - id: 4
23 | user_id: 3
24 | token: e2e5fbe043d0f05f37405f363a1f442124ea5de85f862e2138fcb5b9d6eff142
25 | type: reset
26 | expires_at: RAW=NOW() AT TIME ZONE 'UTC' + INTERVAL '1 hour'
27 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
28 |
--------------------------------------------------------------------------------
/web/admin/src/index.js:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 |
6 | import { init } from '@rematch/core'
7 | import apiPlugin from './utils/rematch-api'
8 | import { Provider } from 'react-redux'
9 | import { createLogger } from 'redux-logger'
10 |
11 | import { BrowserRouter as Router } from 'react-router-dom';
12 |
13 | import models from './models'
14 | import App from './scenes/App'
15 |
16 | import './index.css'
17 |
18 | let middlewares = []
19 |
20 | if (process.env.NODE_ENV === 'development') {
21 | middlewares.push(createLogger())
22 | }
23 |
24 | const store = init({
25 | models,
26 | redux: {
27 | middlewares: middlewares,
28 | },
29 | plugins: [
30 | apiPlugin({}),
31 | ],
32 | })
33 |
34 | ReactDOM.render(
35 |
36 |
37 |
38 |
39 | ,
40 | document.getElementById('root'),
41 | )
42 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | test:
8 | name: Test
9 | runs-on: ubuntu-latest
10 | services:
11 | postgres:
12 | image: postgres:11.5
13 | env:
14 | POSTGRES_USER: postgres
15 | POSTGRES_PASSWORD: postgres
16 | POSTGRES_DB: postgres_test
17 | ports:
18 | - 5432/tcp
19 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
20 | steps:
21 | - name: Set up Golang
22 | uses: actions/setup-go@v1
23 | with:
24 | go-version: 1.12
25 |
26 | - name: Check out code
27 | uses: actions/checkout@v1
28 |
29 | - name: Run unit tests
30 | run: make test
31 | env:
32 | CTF_DB_DSN: 'postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/postgres_test?sslmode=disable'
33 |
--------------------------------------------------------------------------------
/controllers/schemas/ScoresCTFTime.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "ScoresCTFTime",
3 | "description": "List of scores in CTFTime format (https://ctftime.org/json-scoreboard-feed)",
4 | "type": "object",
5 | "properties": {
6 | "standings": {
7 | "type": "array",
8 | "items": {
9 | "type": "object",
10 | "properties": {
11 | "team": {
12 | "type": "string"
13 | },
14 | "score": {
15 | "type": "number"
16 | },
17 | "pos": {
18 | "type": "number"
19 | },
20 | "lastAccept": {
21 | "type": "number"
22 | }
23 | },
24 | "additionalProperties": false,
25 | "required": [
26 | "pos",
27 | "score",
28 | "team",
29 | "lastAccept"
30 | ]
31 | }
32 | }
33 | },
34 | "additionalProperties": false,
35 | "required": [
36 | "standings"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/web/admin/src/components/with-fields/withFields.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | export default (WrappedComponent) => {
4 | return class WithFiedls extends Component {
5 |
6 | static defaultState = {
7 | fields: {},
8 | updated: {},
9 | }
10 |
11 | state = {
12 | fields: {},
13 | updated: {},
14 | }
15 |
16 | handleFieldsChange = (changedFields) => {
17 | this.setState(({ fields, updated }) => ({
18 | fields: { ...fields, ...changedFields },
19 | updated: Object.assign(
20 | updated,
21 | ...Object.keys(changedFields).map((field) => ({ [field]: new Date() })),
22 | ),
23 | }))
24 | }
25 |
26 | render() {
27 | const props = {
28 | ...this.props,
29 | fields: this.state.fields,
30 | handleFieldsChange: this.handleFieldsChange,
31 | }
32 |
33 | return
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/models/fixtures/solutions.yml:
--------------------------------------------------------------------------------
1 | - user_id: 1 # LC↯BC
2 | challenge_id: 1 # web-100
3 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
4 |
5 | - user_id: 1 # LC↯BC
6 | challenge_id: 3 # crypto-300
7 | created_at: RAW=NOW() AT TIME ZONE 'UTC' + INTERVAL '1 second'
8 |
9 | - user_id: 2 # PPP
10 | challenge_id: 1 # web-100
11 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
12 |
13 | - user_id: 2 # PPP
14 | challenge_id: 3 # crypto-300
15 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
16 |
17 | - user_id: 3 # Dragon Sector
18 | challenge_id: 1 # web-100
19 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
20 |
21 | - user_id: 3 # Dragon Sector
22 | challenge_id: 2 # reverse-200
23 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
24 |
25 | - user_id: 5 # TokyoWesterns
26 | challenge_id: 1 # web-100
27 | created_at: RAW=NOW() AT TIME ZONE 'UTC' + INTERVAL '1 second'
28 |
29 | - user_id: 5 # TokyoWesterns
30 | challenge_id: 3 # crypto-300
31 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
32 |
--------------------------------------------------------------------------------
/middlewares/recaptcha/handler.go:
--------------------------------------------------------------------------------
1 | package recaptcha
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | type handler struct {
8 | next http.Handler
9 | opts *options
10 | }
11 |
12 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
13 | err := check(
14 | h.opts.secret,
15 | r.Header.Get(h.opts.header),
16 | r.Header.Get("X-Forwared-For"))
17 |
18 | if err != nil {
19 | h.opts.errorFunc(w, r, err)
20 | return
21 | }
22 |
23 | h.next.ServeHTTP(w, r)
24 | }
25 |
26 | func defaultErrorFunc(w http.ResponseWriter, r *http.Request, err error) {
27 | http.Error(w, "Wrong captcha", 403)
28 | }
29 |
30 | func New(secret string, opts ...Option) func(http.Handler) http.Handler {
31 | return func(next http.Handler) http.Handler {
32 | do := *defaultOptions
33 |
34 | do.secret = secret
35 |
36 | h := &handler{
37 | next: next,
38 | opts: &do,
39 | }
40 |
41 | for _, fn := range opts {
42 | fn(h.opts)
43 | }
44 |
45 | return h
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/web/public/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ctfzone-public",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@rematch/core": "^1.4.0",
7 | "axios": "^0.21.1",
8 | "classnames": "^2.2.6",
9 | "dayjs": "^1.10.3",
10 | "http-proxy-middleware": "^1.0.6",
11 | "node-sass": "^4.0.0",
12 | "npm-check-updates": "^10.2.5",
13 | "rc-form": "^2.4.12",
14 | "react": "^17.0.1",
15 | "react-dom": "^17.0.1",
16 | "react-markdown": "^5.0.3",
17 | "react-recaptcha": "^2.3.10",
18 | "react-redux": "^7.2.2",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "4.0.1"
21 | },
22 | "scripts": {
23 | "start": "react-scripts start",
24 | "build": "react-scripts build",
25 | "test": "react-scripts test",
26 | "eject": "react-scripts eject"
27 | },
28 | "eslintConfig": {
29 | "extends": "react-app"
30 | },
31 | "proxy": "http://localhost:8000/",
32 | "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"]
33 | }
34 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:latest AS ui
2 | WORKDIR /usr/src/
3 | COPY web .
4 | ENV NODE_ENV production
5 |
6 | WORKDIR /usr/src/public
7 | RUN npm --production=false install
8 | RUN npm run build
9 |
10 | WORKDIR /usr/src/admin
11 | RUN npm --production=false install
12 | RUN npm run build
13 |
14 |
15 | FROM golang:latest AS api
16 | WORKDIR /go/src/github.com/ctf-zone/ctfzone
17 | COPY . .
18 | ENV GO111MODULE on
19 | ENV GOOS linux
20 | ENV CGO_ENABLED 0
21 | RUN make
22 |
23 |
24 | FROM alpine:latest
25 | RUN addgroup -S ctfzone && adduser -S -u 1001 -G ctfzone ctfzone
26 | RUN apk add --no-cache ca-certificates
27 | USER ctfzone
28 | WORKDIR /home/ctfzone
29 | RUN mkdir -p files static/public static/admin
30 | COPY templates ./templates
31 | COPY --from=ui --chown=ctfzone:ctfzone /usr/src/public/build static/public/
32 | COPY --from=ui --chown=ctfzone:ctfzone /usr/src/admin/build static/admin/
33 | COPY --from=api --chown=ctfzone:ctfzone /go/src/github.com/ctf-zone/ctfzone/ctfzone .
34 | EXPOSE 8080 8443
35 | CMD ["./ctfzone"]
36 |
--------------------------------------------------------------------------------
/models/solutions.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Solution struct {
8 | UserID int64 `db:"user_id" json:"userId"`
9 | ChallengeID int64 `db:"challenge_id" json:"challengeId"`
10 | CreatedAt time.Time `db:"created_at" json:"createdAt"`
11 | }
12 |
13 | func (r *Repository) SolutionsInsert(o *Solution) error {
14 | o.CreatedAt = now()
15 |
16 | stmt, err := r.db.PrepareNamed(
17 | "INSERT INTO solutions (user_id, challenge_id, created_at) " +
18 | "VALUES(:user_id, :challenge_id, :created_at) " +
19 | "RETURNING created_at")
20 |
21 | if err != nil {
22 | return err
23 | }
24 |
25 | return stmt.QueryRowx(o).Scan(&o.CreatedAt)
26 | }
27 |
28 | func (r *Repository) SolutionsOneByID(userID, challengeID int64) (*Solution, error) {
29 | var o Solution
30 |
31 | err := r.db.Get(&o, "SELECT * FROM solutions WHERE user_id = $1 and challenge_id = $2", userID, challengeID)
32 |
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | return &o, nil
38 | }
39 |
--------------------------------------------------------------------------------
/controllers/file.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "io/ioutil"
5 | "path"
6 | "time"
7 | )
8 |
9 | type File struct {
10 | Name string `json:"name"`
11 | IsDirectory bool `json:"isDirectory"`
12 | Items []File `json:"items"`
13 | Size int64 `json:"size"`
14 | UpdatedAt time.Time `json:"updatedAt"`
15 | }
16 |
17 | func getDirFiles(dirPath string) ([]File, error) {
18 | var err error
19 |
20 | files := make([]File, 0)
21 |
22 | ff, err := ioutil.ReadDir(dirPath)
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | for _, f := range ff {
28 | var items []File
29 |
30 | if f.IsDir() {
31 | items, err = getDirFiles(path.Join(dirPath, f.Name()))
32 | if err != nil {
33 | return nil, err
34 | }
35 | }
36 |
37 | files = append(files, File{
38 | Name: f.Name(),
39 | IsDirectory: f.IsDir(),
40 | Items: items,
41 | Size: f.Size(),
42 | UpdatedAt: f.ModTime(),
43 | })
44 | }
45 |
46 | return files, nil
47 | }
48 |
--------------------------------------------------------------------------------
/web/admin/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 8,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "modules": true
8 | }
9 | },
10 | "env": {
11 | "browser": true,
12 | "node": true
13 | },
14 | "extends": [
15 | "eslint:recommended",
16 | "plugin:react/recommended"
17 | ],
18 | "rules": {
19 | "arrow-parens": ["error", "always"],
20 | "react/sort-comp": ["error", {
21 | "order": [
22 | "static-methods",
23 | "lifecycle",
24 | "everything-else",
25 | "/^handle.+$/",
26 | "/^render.+$/",
27 | "render"
28 | ]
29 | }],
30 | "object-curly-spacing": ["error", "always"],
31 | "react/jsx-curly-spacing": ["error", "never"],
32 | "template-curly-spacing": ["error", "never"],
33 | "lines-between-class-members": ["error", "always"],
34 | "space-before-function-paren": ["error", "never"],
35 | "comma-dangle": ["error", "always-multiline"]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/web/public/src/models/challenges.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api';
2 |
3 | export default {
4 | state: {
5 | // TODO: use array
6 | items: {}
7 | },
8 | reducers: {
9 | set: (state, payload) => {
10 | return { ...state, ...payload };
11 | },
12 | setItem: (state, { id, item }) => {
13 | return {
14 | ...state,
15 | items: {
16 | ...state.items,
17 | [id]: item
18 | }
19 | };
20 | }
21 | },
22 | effects: {
23 | async list() {
24 | const response = await api.get('/challenges');
25 | const items = response.data.reduce((result, item) => {
26 | result[item.challenge.id] = item;
27 | return result;
28 | }, {});
29 |
30 | this.set({ items });
31 |
32 | return items;
33 | },
34 | async get(id) {
35 | const response = await api.get(`/challenges/${id}`);
36 | const item = response.data;
37 |
38 | this.setItem({ id: item.challenge.id, item });
39 |
40 | return item;
41 | }
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/controllers/utils.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | )
8 |
9 | func jsonDecode(r *http.Request, out interface{}) error {
10 | body, err := ioutil.ReadAll(r.Body)
11 | if err != nil {
12 | return err
13 | }
14 | return json.Unmarshal(body, out)
15 | }
16 |
17 | func requestHost(r *http.Request) (host string) {
18 | // not standard, but most popular
19 | host = r.Header.Get("X-Forwarded-Host")
20 | if host != "" {
21 | return
22 | }
23 |
24 | // if all else fails fall back to request host
25 | host = r.Host
26 | return
27 | }
28 |
29 | func responseJSON(w http.ResponseWriter, data interface{}) error {
30 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
31 | w.WriteHeader(200)
32 | return json.NewEncoder(w).Encode(data)
33 | }
34 |
35 | func responseOK(w http.ResponseWriter) {
36 | w.Header().Set("Content-Type", "")
37 | w.WriteHeader(200)
38 | }
39 |
40 | func responseCreated(w http.ResponseWriter) {
41 | w.Header().Set("Content-Type", "")
42 | w.WriteHeader(201)
43 | }
44 |
--------------------------------------------------------------------------------
/web/public/src/components/form-item/FormItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | class FormItem extends Component {
6 |
7 | static propTypes = {
8 | label: PropTypes.string,
9 | children: PropTypes.node,
10 | errors: PropTypes.array,
11 | }
12 |
13 | render() {
14 | const { children, errors, label } = this.props
15 | const valid = !errors || errors.length === 0
16 | const baseClass = 'ctf-form-item'
17 | const className = classNames({
18 | [baseClass]: true,
19 | [baseClass + '--valid']: valid,
20 | [baseClass + '--invalid']: !valid,
21 | })
22 |
23 | return (
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 | {(errors || []).join(', ')}
31 |
32 |
33 | )
34 | }
35 | }
36 |
37 | export default FormItem
38 |
--------------------------------------------------------------------------------
/web/admin/src/utils/form.js:
--------------------------------------------------------------------------------
1 | import { Form } from 'antd'
2 |
3 | export const hasErrors = (fieldsError) => {
4 | return Object
5 | .keys(fieldsError)
6 | .some((field) => fieldsError[field]);
7 | }
8 |
9 | export const addErrors = (fields, errors) => {
10 | return Object.assign(
11 | fields,
12 | ...Object.keys(errors).map((field) => ({
13 | [field]: {
14 | ...fields[field],
15 | errors: errors[field].map((e) => new Error(e)),
16 | },
17 | })),
18 | )
19 | }
20 |
21 | export const addValues = (fields, values) => {
22 | return Object.assign(
23 | fields,
24 | ...Object.keys(values).map((field) => ({
25 | [field]: {
26 | ...fields[field],
27 | value: values[field],
28 | },
29 | })),
30 | )
31 | }
32 |
33 | export const mapPropsToFields = ({ fields }) => {
34 | return Object.assign(
35 | {},
36 | ...Object.keys(fields || {}).map((field) => {
37 | return {
38 | [field]: Form.createFormField({
39 | ...fields[field],
40 | }),
41 | }
42 | }),
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/controllers/game.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/ctf-zone/ctfzone/config"
8 | )
9 |
10 | // GameInfo return information about CTF
11 | // summary: Returns information about game
12 | // tags: [game]
13 | func GameInfo(c *config.Game) http.HandlerFunc {
14 |
15 | type Response struct {
16 | Status string `json:"status"`
17 | Start time.Time `json:"start"`
18 | End time.Time `json:"end"`
19 | }
20 |
21 | start, end := c.StartTime(), c.EndTime()
22 |
23 | return func(w http.ResponseWriter, r *http.Request) {
24 |
25 | info := &Response{
26 | Start: start.UTC(),
27 | End: end.UTC(),
28 | }
29 |
30 | now := time.Now()
31 |
32 | if now.Before(start) {
33 | info.Status = "countdown"
34 | } else if now.After(start) && now.Before(end) {
35 | info.Status = "started"
36 | } else if now.After(end) {
37 | info.Status = "ended"
38 | }
39 |
40 | // schema: GameInfo
41 | if err := responseJSON(w, info); err != nil {
42 | handleError(w, r, ErrInternal.SetError(err))
43 | return
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/middlewares/recaptcha/options.go:
--------------------------------------------------------------------------------
1 | package recaptcha
2 |
3 | import "net/http"
4 |
5 | var defaultOptions = &options{
6 | header: "X-G-Recaptcha-Response",
7 | secret: "",
8 | errorFunc: defaultErrorFunc,
9 | }
10 |
11 | type options struct {
12 | header string
13 | secret string
14 | errorFunc func(http.ResponseWriter, *http.Request, error)
15 | }
16 |
17 | // Option defines the functional arguments for configuring the middleware.
18 | type Option func(*options)
19 |
20 | // Header sets the header name from which the recaptcha response will be taken.
21 | // Default value is "X-G-Recaptcha-Response".
22 | func Header(s string) Option {
23 | return func(opts *options) {
24 | opts.header = s
25 | }
26 | }
27 |
28 | // ErrorFunc allows you to control behavior when recaptcha check failed.
29 | // The default behavior is for a HTTP 403 status code to be written to the
30 | // ResponseWriter along with the plain-text error string.
31 | func ErrorFunc(f func(http.ResponseWriter, *http.Request, error)) Option {
32 | return func(opts *options) {
33 | opts.errorFunc = f
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/controllers/schemas/Challenge.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Challenge",
3 | "description": "Challenge model",
4 | "type": "object",
5 | "properties": {
6 | "id": {
7 | "type": "number"
8 | },
9 | "title": {
10 | "type": "string"
11 | },
12 | "description": {
13 | "type": "string"
14 | },
15 | "difficulty": {
16 | "type": "string",
17 | "enum": ["easy", "medium", "hard"]
18 | },
19 | "categories": {
20 | "type": "array",
21 | "items": {
22 | "type": "string"
23 | }
24 | },
25 | "points": {
26 | "type": "number"
27 | },
28 | "isLocked": {
29 | "type": "boolean"
30 | },
31 | "createdAt": {
32 | "type": "string",
33 | "format": "date-time"
34 | },
35 | "updatedAt": {
36 | "type": "string",
37 | "format": "date-time"
38 | }
39 | },
40 | "additionalProperties": false,
41 | "required": [
42 | "id",
43 | "title",
44 | "description",
45 | "difficulty",
46 | "categories",
47 | "points",
48 | "createdAt",
49 | "updatedAt"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/web/admin/src/components/pagination/Pagination.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Button, Icon } from 'antd'
4 |
5 | import styles from './Pagination.module.css'
6 |
7 | class Pagination extends Component {
8 |
9 | static propTypes = {
10 | onNext: PropTypes.func,
11 | onPrev: PropTypes.func,
12 | hasNext: PropTypes.bool,
13 | hasPrev: PropTypes.bool,
14 | }
15 |
16 | render() {
17 | return (
18 |
19 |
20 |
28 |
36 |
37 |
38 | )
39 | }
40 | }
41 |
42 | export default Pagination
43 |
--------------------------------------------------------------------------------
/models/migrations/migrations.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "github.com/golang-migrate/migrate/v4"
5 | _ "github.com/golang-migrate/migrate/v4/database/postgres"
6 | bindata "github.com/golang-migrate/migrate/v4/source/go_bindata"
7 | "github.com/pkg/errors"
8 | )
9 |
10 | func Up(dsn string) error {
11 | assets := bindata.Resource(AssetNames(),
12 | func(name string) ([]byte, error) {
13 | return Asset(name)
14 | })
15 |
16 | src, err := bindata.WithInstance(assets)
17 | if err != nil {
18 | return errors.Wrap(err, "model: fail to create bindata source")
19 | }
20 |
21 | mgr, err := migrate.NewWithSourceInstance("go-bindata", src, dsn)
22 | if err != nil {
23 | return errors.Wrap(err, "model: fail init migrate")
24 | }
25 |
26 | if err := mgr.Up(); err != nil && err != migrate.ErrNoChange {
27 | return errors.Wrap(err, "model: migrations up failed")
28 | }
29 |
30 | if err := src.Close(); err != nil {
31 | return errors.Wrap(err, "model: fail to close source")
32 | }
33 |
34 | if se, de := mgr.Close(); se != nil || de != nil {
35 | return errors.Wrap(de, "model: fail to close source")
36 | }
37 |
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/web/public/src/models/news.js:
--------------------------------------------------------------------------------
1 | import { stringify } from 'qs';
2 |
3 | import api from '../utils/api';
4 |
5 | export default {
6 | state: {
7 | items: [],
8 | hints: {}
9 | },
10 | reducers: {
11 | set: (state, payload) => {
12 | return { ...state, ...payload };
13 | },
14 | setHints(state, { challengeId, hints }) {
15 | return {
16 | ...state,
17 | hints: {
18 | ...state.hints,
19 | [challengeId]: hints
20 | }
21 | };
22 | }
23 | },
24 | effects: {
25 | async list({ link = null, filters = {} }) {
26 | const query = stringify(filters);
27 | const response = await api.get(
28 | link || query ? `/annonncements?${query}` : '/announcements'
29 | );
30 | const items = response.data;
31 |
32 | this.set({ items });
33 |
34 | return items;
35 | },
36 | async getHints(challengeId) {
37 | const response = await api.get(
38 | `/announcements?challengeId=${challengeId}`
39 | );
40 | const hints = response.data;
41 | this.setHints({ challengeId, hints });
42 | return hints;
43 | }
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/web/admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ctfzone-admin",
3 | "version": "1.0.0",
4 | "description": "CTFZone Admin UI",
5 | "author": "russtone@yandex.ru",
6 | "license": "MIT",
7 | "main": "src/index.js",
8 | "dependencies": {
9 | "@rematch/core": "^1.4.0",
10 | "antd": "^3.26.20",
11 | "axios": "^0.21.1",
12 | "codemirror": "^5.59.1",
13 | "parse-link-header": "^1.0.1",
14 | "rc-form": "^2.4.12",
15 | "react": "^17.0.1",
16 | "react-codemirror2": "^7.2.1",
17 | "react-dom": "^17.0.1",
18 | "react-markdown": "^5.0.3",
19 | "react-moment": "^1.1.1",
20 | "react-redux": "^7.2.2",
21 | "react-router-dom": "^5.2.0",
22 | "react-scripts": "4.0.1"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "proxy": "http://localhost:8000/",
34 | "browserslist": [
35 | ">0.2%",
36 | "not dead",
37 | "not ie <= 11",
38 | "not op_mini all"
39 | ],
40 | "devDependencies": {
41 | "redux-logger": "^3.0.6"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/web/public/src/scenes/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | class Nav extends Component {
6 |
7 | static propTypes = {
8 | leftItems: PropTypes.array,
9 | rightItems: PropTypes.array,
10 | rightPrefix: PropTypes.node,
11 | }
12 |
13 | static defaultProps = {
14 | leftItems: [],
15 | rightItems: [],
16 | }
17 |
18 | renderLinks(items) {
19 | return items.filter(({ hidden }) => !hidden).map(({ title, path }, i) => {
20 | return (
21 |
22 | {title}
23 |
24 | )
25 | })
26 | }
27 |
28 | render() {
29 | const { leftItems, rightItems, rightPrefix } = this.props
30 |
31 | return (
32 |
33 |
34 | {this.renderLinks(leftItems)}
35 |
36 |
37 | {rightPrefix}
38 | {this.renderLinks(rightItems)}
39 |
40 |
41 | )
42 | }
43 | }
44 |
45 | export default Nav
46 |
--------------------------------------------------------------------------------
/models/likes.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Like struct {
8 | UserID int64 `db:"user_id" json:"userId"`
9 | ChallengeID int64 `db:"challenge_id" json:"challengeId"`
10 | CreatedAt time.Time `db:"created_at" json:"createdAt"`
11 | }
12 |
13 | func (r *Repository) LikesInsert(o *Like) error {
14 | o.CreatedAt = now()
15 |
16 | stmt, err := r.db.PrepareNamed(
17 | "INSERT INTO likes (user_id, challenge_id, created_at) " +
18 | "VALUES(:user_id, :challenge_id, :created_at) " +
19 | "RETURNING created_at")
20 |
21 | if err != nil {
22 | return err
23 | }
24 |
25 | return stmt.QueryRowx(o).Scan(&o.CreatedAt)
26 | }
27 |
28 | func (r *Repository) LikesDelete(userID, challengeID int64) error {
29 | return r.db.QueryRow("DELETE FROM likes WHERE user_id = $1 AND challenge_id = $2 RETURNING user_id",
30 | userID, challengeID).Scan(&userID)
31 | }
32 |
33 | func (r *Repository) LikesOneByID(userID, challengeID int64) (*Like, error) {
34 | var o Like
35 |
36 | err := r.db.Get(&o, "SELECT * FROM likes WHERE user_id = $1 and challenge_id = $2", userID, challengeID)
37 |
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | return &o, nil
43 | }
44 |
--------------------------------------------------------------------------------
/models/solutions_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | . "github.com/ctf-zone/ctfzone/models"
11 | )
12 |
13 | func Test_Solutions_Insert_Success(t *testing.T) {
14 | setup(t)
15 | defer teardown(t)
16 |
17 | o := &Solution{
18 | UserID: 5,
19 | ChallengeID: 2,
20 | }
21 |
22 | err := db.SolutionsInsert(o)
23 | assert.NoError(t, err)
24 | assert.WithinDuration(t, time.Now().UTC(), o.CreatedAt, 5*time.Second)
25 | }
26 |
27 | func Test_Solutions_Insert_Duplicate(t *testing.T) {
28 | setup(t)
29 | defer teardown(t)
30 |
31 | o := &Solution{
32 | UserID: 1,
33 | ChallengeID: 1,
34 | }
35 |
36 | err := db.SolutionsInsert(o)
37 | assert.Error(t, err)
38 | }
39 |
40 | func Test_Solutions_OneByID_Success(t *testing.T) {
41 | setup(t)
42 | defer teardown(t)
43 |
44 | o, err := db.SolutionsOneByID(1, 1)
45 | assert.NoError(t, err)
46 | assert.NotNil(t, o)
47 | }
48 |
49 | func Test_Solutions_OneByID_NotExist(t *testing.T) {
50 | setup(t)
51 | defer teardown(t)
52 |
53 | o, err := db.SolutionsOneByID(1, 1337)
54 | assert.Error(t, err)
55 | assert.Nil(t, o)
56 | assert.EqualError(t, err, sql.ErrNoRows.Error())
57 | }
58 |
--------------------------------------------------------------------------------
/middlewares/cntcheck/handler.go:
--------------------------------------------------------------------------------
1 | package cntcheck
2 |
3 | import (
4 | "errors"
5 | "mime"
6 | "net/http"
7 | "strings"
8 | )
9 |
10 | type handler struct {
11 | next http.Handler
12 | opts *options
13 | }
14 |
15 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
16 | mediatype, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
17 | charset, ok := params["charset"]
18 | if !ok {
19 | charset = "UTF-8"
20 | }
21 |
22 | // per net/http doc, means that the length is known and non-null
23 | if r.ContentLength > 0 &&
24 | !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") {
25 | h.opts.errorFunc(w, r, errors.New("bad Content-Type or charset"))
26 | return
27 | }
28 |
29 | h.next.ServeHTTP(w, r)
30 | }
31 |
32 | func defaultErrorFunc(w http.ResponseWriter, r *http.Request, err error) {
33 | http.Error(w, "Bad Content-Type or charset, expected 'application/json'", 415)
34 | }
35 |
36 | func New(opts ...Option) func(http.Handler) http.Handler {
37 | return func(next http.Handler) http.Handler {
38 | do := *defaultOptions
39 |
40 | h := &handler{
41 | next: next,
42 | opts: &do,
43 | }
44 |
45 | for _, f := range opts {
46 | f(h.opts)
47 | }
48 |
49 | return h
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/web/public/src/models/auth.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api';
2 |
3 | export default {
4 | state: null,
5 | reducers: {
6 | set: (state, payload) => {
7 | return payload;
8 | }
9 | },
10 | // TODO: replace data with fields
11 | effects: {
12 | async register(data) {
13 | const { reCaptchaResponse, ...rest } = data;
14 | console.log(reCaptchaResponse, rest);
15 | await api.post('/auth/register', rest, {
16 | headers: {
17 | 'X-G-Recaptcha-Response': reCaptchaResponse,
18 | }
19 | });
20 | },
21 | async login(data) {
22 | await api.post('/auth/login', data);
23 | this.set(true);
24 | },
25 | async logout() {
26 | await api.post('/auth/logout', {});
27 | this.set(false);
28 | },
29 | async check() {
30 | const response = await api.get('/auth/check');
31 | const { isLoggedIn } = response.data;
32 | this.set(isLoggedIn);
33 | },
34 | async resetPassword(data) {
35 | await api.post('/auth/reset-password', data);
36 | },
37 | async sendToken({ type, token, email }) {
38 | await api.post('/auth/send-token', { type, token, email });
39 | },
40 | async activate({ token }) {
41 | await api.post('/auth/activate', { token });
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/middlewares/timecheck/handler.go:
--------------------------------------------------------------------------------
1 | package timecheck
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | var (
10 | MinTime = time.Time{}
11 | MaxTime = time.Unix(1<<63-62135596801, 999999999)
12 | )
13 |
14 | var (
15 | ErrTooEarly = errors.New("timecheck: too early")
16 | ErrTooLate = errors.New("timecheck: too late")
17 | )
18 |
19 | type handler struct {
20 | next http.Handler
21 | opts *options
22 | }
23 |
24 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25 |
26 | now := time.Now().UTC()
27 |
28 | if now.Before(h.opts.start) {
29 | h.opts.errorFunc(w, r, ErrTooEarly)
30 | return
31 | }
32 |
33 | if now.After(h.opts.end) {
34 | h.opts.errorFunc(w, r, ErrTooLate)
35 | return
36 | }
37 |
38 | h.next.ServeHTTP(w, r)
39 | }
40 |
41 | func defaultErrorFunc(w http.ResponseWriter, r *http.Request, err error) {
42 | http.Error(w, "Access is limited for a period of time", 403)
43 | }
44 |
45 | func New(start, end time.Time, opts ...Option) func(http.Handler) http.Handler {
46 | return func(next http.Handler) http.Handler {
47 | do := *defaultOptions
48 |
49 | do.start = start
50 | do.end = end
51 |
52 | h := &handler{
53 | next: next,
54 | opts: &do,
55 | }
56 |
57 | for _, f := range opts {
58 | f(h.opts)
59 | }
60 |
61 | return h
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/web/admin/src/components/markdown-editor/MarkdownEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Tabs } from 'antd'
5 | import { Controlled as CodeMirror } from 'react-codemirror2'
6 | import ReactMarkdown from 'react-markdown'
7 |
8 | import 'codemirror/mode/gfm/gfm'
9 |
10 | import './MarkdownEditor.css'
11 | import styles from './MarkdownEditor.module.css'
12 |
13 | class MarkdownEditor extends Component {
14 |
15 | static propTypes = {
16 | value: PropTypes.string,
17 | }
18 |
19 | render() {
20 | const { value, ...rest } = this.props
21 |
22 | return (
23 |
24 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 | }
45 |
46 | export default MarkdownEditor
47 |
--------------------------------------------------------------------------------
/models/fixtures/challenges.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | title: Web
3 | categories: '{web}'
4 | points: 100
5 | description: Web challenge
6 | difficulty: easy
7 | flag_hash: 845151a1e2230f5cb2bf3f96354d4b82a4bd30ee1b132469b2a4884ae50fe661
8 | is_locked: false
9 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
10 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
11 |
12 | - id: 2
13 | title: Reverse
14 | categories: '{reverse}'
15 | points: 200
16 | description: Reverse challenge
17 | difficulty: medium
18 | flag_hash: 8633dd1332ede139ffa48af8ac62edf8ff76208dedbcf7a9abbbb3dc02bfc8f9
19 | is_locked: false
20 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
21 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
22 |
23 | - id: 3
24 | title: Crypto
25 | categories: '{crypto}'
26 | points: 300
27 | description: Crypto challenge
28 | difficulty: medium
29 | flag_hash: 39d5e8462b14a3694be559c51c4c150e5f201a6f7fc3b868acf01016271e3a07
30 | is_locked: false
31 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
32 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
33 |
34 | - id: 4
35 | title: PWN
36 | categories: '{pwn}'
37 | points: 500
38 | description: Pwn challenge
39 | difficulty: hard
40 | flag_hash: 49890eae64cefff4a47ad7975c319b813864dd1443205111c552795c194c56ac
41 | is_locked: true
42 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
43 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
44 |
--------------------------------------------------------------------------------
/models/migrations/1489143364_initial_schema.down.sql:
--------------------------------------------------------------------------------
1 | -- VIEWS
2 |
3 | -- ----------------------
4 | -- challenges_hints
5 | -- ----------------------
6 |
7 | DROP VIEW IF EXISTS challenges_hints;
8 |
9 | -- ----------------------
10 | -- challenges_likes
11 | -- ----------------------
12 |
13 | DROP VIEW IF EXISTS challenges_likes;
14 |
15 | -- ----------------------
16 | -- challenges_solutions
17 | -- ----------------------
18 |
19 | DROP VIEW IF EXISTS challenges_solutions;
20 |
21 | -- TABLES
22 |
23 | -- ---------------
24 | -- settings
25 | -- ---------------
26 |
27 | DROP TABLE IF EXISTS settings;
28 | DROP TYPE IF EXISTS scoring_type;
29 |
30 | -- ---------------
31 | -- likes
32 | -- ---------------
33 |
34 | DROP TABLE IF EXISTS likes;
35 |
36 | -- ---------------
37 | -- announcements
38 | -- ---------------
39 |
40 | DROP TABLE IF EXISTS announcements;
41 |
42 | -- ---------------
43 | -- solutions
44 | -- ---------------
45 |
46 | DROP TABLE IF EXISTS solutions;
47 |
48 | -- ---------------
49 | -- challenges
50 | -- ---------------
51 |
52 | DROP TABLE IF EXISTS challenges;
53 | DROP TYPE IF EXISTS difficulty;
54 |
55 | -- ---------------
56 | -- tokens
57 | -- ---------------
58 |
59 | DROP TABLE IF EXISTS tokens;
60 | DROP TYPE IF EXISTS token_type;
61 |
62 | -- ---------------
63 | -- users
64 | -- ---------------
65 |
66 | DROP TABLE IF EXISTS users;
67 |
--------------------------------------------------------------------------------
/web/public/src/scenes/components/UserStats.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | class UserStats extends Component {
6 | static propTypes = {
7 | userGetStats: PropTypes.func,
8 | userStats: PropTypes.object
9 | };
10 |
11 | state = {
12 | intervalId: 0
13 | };
14 |
15 | componentDidMount() {
16 | this.fetchData();
17 |
18 | const intervalId = setInterval(this.fetchData, 30000);
19 | this.setState({ intervalId });
20 | }
21 |
22 | componentWillUnmount() {
23 | clearInterval(this.state.intervalId);
24 | }
25 |
26 | fetchData = () => {
27 | const { userGetStats } = this.props;
28 | userGetStats();
29 | };
30 |
31 | render() {
32 | const { userStats } = this.props;
33 | const prefixClass = 'ctf-user-stats';
34 | return (
35 |
36 |
{userStats.rank}
37 |
{userStats.score}
38 |
39 | );
40 | }
41 | }
42 |
43 | const mapStateToProps = state => ({
44 | userStats: state.user.stats
45 | });
46 |
47 | const mapDispatchToProps = dispatch => ({
48 | userGetStats: dispatch.user.getStats
49 | });
50 |
51 | export default connect(
52 | mapStateToProps,
53 | mapDispatchToProps
54 | )(UserStats);
55 |
--------------------------------------------------------------------------------
/web/public/src/components/layout/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import classNames from 'classnames'
4 |
5 | const generator = (props) => {
6 | return (BasicComponent) => {
7 | return class Adapter extends Component {
8 | render() {
9 | const { prefixClass } = props
10 | return
11 | }
12 | }
13 | }
14 | }
15 |
16 | class Basic extends Component {
17 | static propTypes = {
18 | className: PropTypes.string,
19 | prefixClass: PropTypes.string,
20 | children: PropTypes.node,
21 | }
22 |
23 | render() {
24 | const { prefixClass, className, children, ...others } = this.props
25 | const divClass = classNames(className, prefixClass)
26 | return (
27 | {children}
28 | );
29 | }
30 | }
31 |
32 | const Layout = generator({ prefixClass: 'ctf-layout' })(Basic)
33 | const Header = generator({ prefixClass: 'ctf-layout-header' })(Basic)
34 | const Content = generator({ prefixClass: 'ctf-layout-content' })(Basic)
35 | const Container = generator({ prefixClass: 'ctf-layout-container' })(Basic)
36 | const Footer = generator({ prefixClass: 'ctf-layout-footer' })(Basic)
37 |
38 | Layout.Header = Header
39 | Layout.Content = Content
40 | Layout.Footer = Footer
41 | Layout.Container = Container
42 |
43 | export default Layout
44 |
--------------------------------------------------------------------------------
/middlewares/csrf/token.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/subtle"
6 | "encoding/base64"
7 | "io"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | var tokenLen = 32
13 |
14 | func genToken() []byte {
15 | token := make([]byte, tokenLen)
16 |
17 | if _, err := io.ReadFull(rand.Reader, token); err != nil {
18 | panic(err)
19 | }
20 |
21 | return token
22 | }
23 |
24 | func compareTokens(a, b []byte) bool {
25 | return subtle.ConstantTimeCompare(a, b) == 1
26 | }
27 |
28 | func getSentToken(r *http.Request, header string) []byte {
29 |
30 | encoded := r.Header.Get(header)
31 |
32 | token, err := base64.URLEncoding.DecodeString(encoded)
33 | if err != nil {
34 | return nil
35 | }
36 |
37 | return token
38 | }
39 |
40 | func getRealToken(r *http.Request, cookieName string) []byte {
41 |
42 | cookie, err := r.Cookie(cookieName)
43 | if err != nil {
44 | return nil
45 | }
46 |
47 | token, err := base64.URLEncoding.DecodeString(cookie.Value)
48 | if err != nil {
49 | return nil
50 | }
51 |
52 | return token
53 | }
54 |
55 | func sendNewToken(w http.ResponseWriter, token []byte, opts *cookieOptions) {
56 |
57 | cookie := http.Cookie{
58 | Name: opts.name,
59 | Value: base64.URLEncoding.EncodeToString(token),
60 | Expires: time.Now().UTC().Add(opts.lifetime),
61 | HttpOnly: opts.httponly,
62 | Secure: opts.secure,
63 | Path: "/",
64 | }
65 |
66 | http.SetCookie(w, &cookie)
67 | }
68 |
--------------------------------------------------------------------------------
/models/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: LC↯BC
3 | email: lcbc@mail.com
4 | password_hash: '$2a$10$tp5jB2UF05bQq2TXxrgiE.QeNZM1Xws0ST8KOPjLe74lmRNFfZodO'
5 | extra:
6 | country: ru
7 | is_activated: true
8 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
9 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
10 |
11 | - id: 2
12 | name: PPP
13 | email: ppp@mail.com
14 | password_hash: '$2a$10$VVfUYQGziVC5RPUbFi6zQu8WbXxuQ2JlBNIpFfdzIayXNcxPX1R0K'
15 | extra:
16 | country: us
17 | is_activated: true
18 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
19 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
20 |
21 | - id: 3
22 | name: Dragon Sector
23 | email: dragonsector@mail.com
24 | password_hash: '$2a$10$lvbGv1KdoLe2uJUFmvZUN.Qcfbetvajwk4aOVG7DSUT/jSFEWf8Zm'
25 | extra:
26 | country: pl
27 | is_activated: true
28 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
29 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
30 |
31 | - id: 4
32 | name: '217'
33 | email: 217@mail.com
34 | password_hash: '$2a$10$9T8rX6PukjyGk21zO2jRyuxmibC.PoXdoefeLE3/yJKARGc.qTWra'
35 | extra:
36 | country: tw
37 | is_activated: false
38 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
39 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
40 |
41 | - id: 5
42 | name: 'TokyoWesterns'
43 | email: 'tokyowesterns@mail.com'
44 | password_hash: '$2a$10$Y9YU0OQV4/hjKrSXiQi5leO7/RRlbxr221jB9A0.vvGBNXpeONJN6'
45 | extra:
46 | country: jp
47 | is_activated: true
48 | created_at: RAW=NOW() AT TIME ZONE 'UTC'
49 | updated_at: RAW=NOW() AT TIME ZONE 'UTC'
50 |
--------------------------------------------------------------------------------
/web/admin/src/models/announcements.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 | import parseLinkHeader from 'parse-link-header'
3 | import { stringify } from 'qs'
4 |
5 | export default {
6 | state: {
7 | items: [],
8 | item: {},
9 | links: {},
10 | },
11 | reducers: {
12 | set: (state, payload) => {
13 | return { ...state, ...payload }
14 | },
15 | },
16 | effects: {
17 | async list({ link = null, filters = {} }) {
18 | const query = stringify(filters)
19 | const response = await api.get(link || query ? `announcements?${query}` : 'announcements')
20 | const links = parseLinkHeader(response.headers.link) || {}
21 | const items = response.data
22 |
23 | this.set({ items, links })
24 | return items
25 | },
26 | async get(announcementId) {
27 | const response = await api.get(`announcements/${announcementId}`)
28 | const item = response.data
29 | this.set({ item })
30 | return item
31 | },
32 | async delete(announcementId) {
33 | await api.delete(`announcements/${announcementId}`)
34 | },
35 | async create(data) {
36 | const response = await api.post(`announcements`, data)
37 | const item = response.data
38 | this.set({ item })
39 | return item
40 | },
41 | async update(data) {
42 | const { id, ...rest } = data
43 | const response = await api.put(`announcements/${id}`, rest)
44 | const item = response.data
45 | this.set({ item })
46 | return item
47 | },
48 | },
49 | }
50 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/scenes/files-tree/FilesTree.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Tree } from 'antd'
4 | import { connect } from 'react-redux'
5 |
6 | class FilesTree extends Component {
7 |
8 | static propTypes = {
9 | files: PropTypes.object.isRequired,
10 | filesList: PropTypes.func.isRequired,
11 | }
12 |
13 | componentDidMount() {
14 | const { filesList } = this.props
15 |
16 | filesList()
17 | }
18 |
19 | renderNodes(nodes, keyPrefix) {
20 | const listNodes = nodes.map((node, i) =>
21 |
26 | {this.renderNodes(node.items || [], `${keyPrefix}-${i}`)}
27 | ,
28 | )
29 |
30 | return listNodes
31 | }
32 |
33 | render() {
34 | const { files } = this.props.files
35 |
36 | return (
37 |
38 |
Files
39 |
42 | {this.renderNodes(files, '0')}
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 |
50 | const mapStateToProps = (state) => ({
51 | files: state.files,
52 | filesListResult: state.api.effects.files.list,
53 | });
54 |
55 | const mapDispatchToProps = (dispatch) => ({
56 | filesList: dispatch.files.list,
57 | });
58 |
59 | export default connect(mapStateToProps, mapDispatchToProps)(FilesTree)
60 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/Activate.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter, Link } from 'react-router-dom';
5 |
6 | import { Loading, Window } from '../../../components';
7 |
8 | class Login extends Component {
9 | static propTypes = {
10 | authActivate: PropTypes.func,
11 | match: PropTypes.object,
12 | history: PropTypes.object
13 | };
14 |
15 | state = {
16 | success: false
17 | };
18 |
19 | async componentDidMount() {
20 | const { authActivate, history } = this.props;
21 | const { token } = this.props.match.params;
22 | try {
23 | await authActivate({ token });
24 | this.setState({ success: true });
25 | } catch (e) {
26 | history.push('/auth/login');
27 | }
28 | }
29 |
30 | render() {
31 | const { success } = this.state;
32 |
33 | return (
34 |
35 |
36 |
37 | Your account was activated. Now you can log in.
38 |
39 |
40 | Log In
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
48 | const mapDispatchToProps = dispatch => ({
49 | authActivate: dispatch.auth.activate
50 | });
51 |
52 | export default withRouter(
53 | connect(
54 | () => ({}),
55 | mapDispatchToProps
56 | )(Login)
57 | );
58 |
--------------------------------------------------------------------------------
/web/admin/src/images/logo-small.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/admin/src/models/challenges.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 | import parseLinkHeader from 'parse-link-header'
3 | import { stringify } from 'qs'
4 |
5 | export default {
6 | state: {
7 | challenges: [],
8 | challenge: {},
9 | links: {},
10 | },
11 | reducers: {
12 | set: (state, payload) => {
13 | return { ...state, ...payload }
14 | },
15 | },
16 | effects: {
17 | async list({ link = null, filters = {} }) {
18 | const query = stringify(filters, { allowDots: true, arrayFormat: 'repeat' })
19 | const response = await api.get(link || 'challenges?' + query)
20 | const links = parseLinkHeader(response.headers.link) || {}
21 | const challenges = response.data
22 |
23 | this.set({ challenges, links })
24 | return challenges
25 | },
26 | async get(challengeId) {
27 | const response = await api.get(`challenges/${challengeId}`)
28 | const challenge = response.data
29 | this.set({ challenge })
30 | return challenge
31 | },
32 | async delete(challengeId) {
33 | await api.delete(`challenges/${challengeId}`)
34 | },
35 | async create(data) {
36 | const response = await api.post(`challenges`, data)
37 | const challenge = response.data
38 | this.set({ challenge })
39 | return challenge
40 | },
41 | async update(data) {
42 | const { id, ...rest } = data
43 | const response = await api.put(`challenges/${id}`, rest)
44 | const challenge = response.data
45 | this.set({ challenge })
46 | return challenge
47 | },
48 | },
49 | }
50 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/rules/Rules.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 |
4 | import { Page } from '../../../components';
5 |
6 | // TODO: get from API
7 | const rules = `
8 | Online-stage, starts on 20.10.2018 at 09:00 UTC and will last for 24 hours.
9 | Stage format – Jeopardy. Top 10 teams will be invited to the final.
10 |
11 | Rules:
12 |
13 | - register;
14 | - tasks will be available at the link;
15 | - those who earn more points wins the CTF;
16 | - in case of equal points, the winner will be the one who earned them first.
17 |
18 | It's forbidden to:
19 |
20 | - attack the organizer's infrastructure;
21 | - generate large amounts of traffic (DDoS);
22 | - attack computers of the jury or other participants;
23 | - share flags with other participants.
24 |
25 | The amount of points that every team gets for each task depends on how many times this task was solved by all teams.
26 |
27 | Each task is marked as easy, medium or hard.
28 | This difficulty level doesn’t affect scoring formula of the task,
29 | which only depends on the amount of teams that has submitted the flag.
30 |
31 | Flag format: \`mctf{[a-f0-9]{32}}\`
32 |
33 | The organizers reserve the right to disqualify participants for violating the rules.
34 | `;
35 |
36 | class Rules extends Component {
37 | static propTypes = {};
38 |
39 | componentDidMount() {}
40 |
41 | render() {
42 | return (
43 |
44 |
45 |
46 | );
47 | }
48 | }
49 |
50 | export default Rules;
51 |
--------------------------------------------------------------------------------
/config/helpers.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "time"
9 | )
10 |
11 | func (c Game) StartTime() time.Time {
12 | t, err := time.Parse(time.RFC3339, c.Start)
13 | if err != nil {
14 | panic(err)
15 | }
16 | return t
17 | }
18 |
19 | func (c Game) EndTime() time.Time {
20 | t, err := time.Parse(time.RFC3339, c.End)
21 | if err != nil {
22 | panic(err)
23 | }
24 | return t
25 | }
26 |
27 | func (c Session) SecretBytes() []byte {
28 | b, err := hex.DecodeString(c.Secret)
29 | if err != nil {
30 | panic(err)
31 | }
32 | return b
33 | }
34 |
35 | func (c Server) BaseURL() string {
36 | url := ""
37 |
38 | if c.TLS.Enabled {
39 | url += "https://"
40 | } else {
41 | url += "http://"
42 | }
43 |
44 | url += c.Domain
45 |
46 | if c.IncludePort {
47 | if c.TLS.Enabled {
48 | url += fmt.Sprintf(":%d", c.TLS.Port)
49 | } else {
50 | url += fmt.Sprintf(":%d", c.Port)
51 | }
52 | }
53 |
54 | return url
55 | }
56 |
57 | func (c Server) AdminBaseURL() string {
58 | url := ""
59 |
60 | if c.TLS.Enabled {
61 | url += "https://"
62 | } else {
63 | url += "http://"
64 | }
65 |
66 | url += fmt.Sprintf("admin.%s", c.Domain)
67 |
68 | if c.IncludePort {
69 | if c.TLS.Enabled {
70 | url += fmt.Sprintf(":%d", c.TLS.Port)
71 | } else {
72 | url += fmt.Sprintf(":%d", c.Port)
73 | }
74 | }
75 |
76 | return url
77 | }
78 |
79 | func (c Files) Dir() string {
80 | if filepath.IsAbs(c.Path) {
81 | return c.Path
82 | }
83 |
84 | workDir, _ := os.Getwd()
85 |
86 | return filepath.Join(workDir, c.Path)
87 | }
88 |
--------------------------------------------------------------------------------
/models/likes_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | . "github.com/ctf-zone/ctfzone/models"
11 | )
12 |
13 | func Test_Likes_Insert_Success(t *testing.T) {
14 | setup(t)
15 | defer teardown(t)
16 |
17 | l := &Like{
18 | UserID: 1,
19 | ChallengeID: 2,
20 | }
21 |
22 | err := db.LikesInsert(l)
23 | assert.NoError(t, err)
24 |
25 | assert.WithinDuration(t, time.Now().UTC(), l.CreatedAt, 5*time.Second)
26 | }
27 |
28 | func Test_Likes_Insert_Duplicate(t *testing.T) {
29 | setup(t)
30 | defer teardown(t)
31 |
32 | l := &Like{
33 | UserID: 1,
34 | ChallengeID: 1,
35 | }
36 |
37 | err := db.LikesInsert(l)
38 | assert.Error(t, err)
39 | }
40 |
41 | func Test_Likes_Delete_Success(t *testing.T) {
42 | setup(t)
43 | defer teardown(t)
44 |
45 | err := db.LikesDelete(1, 1)
46 | assert.NoError(t, err)
47 | }
48 |
49 | func Test_Likes_Delete_NotExist(t *testing.T) {
50 | setup(t)
51 | defer teardown(t)
52 |
53 | err := db.LikesDelete(1337, 1337)
54 | assert.Error(t, err)
55 | assert.EqualError(t, err, sql.ErrNoRows.Error())
56 | }
57 |
58 | func Test_Likes_OneByID_Success(t *testing.T) {
59 | setup(t)
60 | defer teardown(t)
61 |
62 | s, err := db.LikesOneByID(1, 1)
63 | assert.NoError(t, err)
64 | assert.NotNil(t, s)
65 | }
66 |
67 | func Test_Likes_OneByID_NotExist(t *testing.T) {
68 | setup(t)
69 | defer teardown(t)
70 |
71 | s, err := db.LikesOneByID(1, 1337)
72 | assert.Error(t, err)
73 | assert.Nil(t, s)
74 | assert.EqualError(t, err, sql.ErrNoRows.Error())
75 | }
76 |
--------------------------------------------------------------------------------
/middlewares/recaptcha/check.go:
--------------------------------------------------------------------------------
1 | package recaptcha
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "net/url"
7 | "time"
8 | )
9 |
10 | // See https://developers.google.com/recaptcha/docs/verify
11 | var (
12 | checkURL = "https://www.google.com/recaptcha/api/siteverify"
13 | errorText = map[string]string{
14 | "missing-input-secret": "The secret parameter is missing",
15 | "invalid-input-secret": "The secret parameter is invalid or malformed",
16 | "missing-input-response": "The response parameter is missing",
17 | "invalid-input-response": "The response parameter is invalid or malformed",
18 | }
19 | )
20 |
21 | type recaptchaErrors []string
22 |
23 | func (re recaptchaErrors) Error() string {
24 | var err string
25 | for _, e := range re {
26 | err += errorText[e] + "; "
27 | }
28 | return err
29 | }
30 |
31 | type recaptchaResponse struct {
32 | Success bool `json:"success"`
33 | Challenge time.Time `json:"challenge_ts"`
34 | Hostname string `json:"hostname"`
35 | Errors recaptchaErrors `json:"error-codes"`
36 | }
37 |
38 | func check(secret, clientResponse, clientIP string) error {
39 |
40 | r, err := http.PostForm(checkURL, url.Values{
41 | "secret": {secret},
42 | "response": {clientResponse},
43 | "remoteip": {clientIP},
44 | })
45 |
46 | if err != nil {
47 | return err
48 | }
49 |
50 | defer r.Body.Close()
51 |
52 | var res recaptchaResponse
53 | err = json.NewDecoder(r.Body).Decode(&res)
54 |
55 | if err != nil {
56 | return err
57 | }
58 |
59 | if !res.Success {
60 | return res.Errors
61 | }
62 |
63 | return nil
64 | }
65 |
--------------------------------------------------------------------------------
/middlewares/csrf/options.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "net/http"
5 | "time"
6 | )
7 |
8 | var defaultOptions = &options{
9 | header: "X-CSRF-Token",
10 | cookie: &cookieOptions{
11 | name: "csrf-token",
12 | secure: true,
13 | lifetime: time.Hour * 24,
14 | httponly: false,
15 | },
16 | errorFunc: defaultErrorFunc,
17 | }
18 |
19 | type cookieOptions struct {
20 | name string
21 | secure bool
22 | lifetime time.Duration
23 | httponly bool
24 | }
25 |
26 | type options struct {
27 | header string
28 | cookie *cookieOptions
29 | errorFunc func(http.ResponseWriter, *http.Request, error)
30 | }
31 |
32 | // Option defines the functional arguments for configuring the middleware.
33 | type Option func(*options)
34 |
35 | // Header sets the header name from which the CSRF-token will be taken.
36 | // Default value is "X-CSRF-Token".
37 | func Header(s string) Option {
38 | return func(opts *options) {
39 | opts.header = s
40 | }
41 | }
42 |
43 | func CookieName(s string) Option {
44 | return func(opts *options) {
45 | opts.cookie.name = s
46 | }
47 | }
48 |
49 | func CookieSecure(b bool) Option {
50 | return func(opts *options) {
51 | opts.cookie.secure = b
52 | }
53 | }
54 |
55 | func CookieLifetime(t time.Duration) Option {
56 | return func(opts *options) {
57 | opts.cookie.lifetime = t
58 | }
59 | }
60 |
61 | func CookieHTTPOnly(b bool) Option {
62 | return func(opts *options) {
63 | opts.cookie.httponly = b
64 | }
65 | }
66 |
67 | func ErrorFunc(f func(http.ResponseWriter, *http.Request, error)) Option {
68 | return func(opts *options) {
69 | opts.errorFunc = f
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BINARY = ctfzone
2 |
3 | pkgs = $(shell go list ./...)
4 |
5 | build:
6 | @echo ">> building binaries"
7 | @go build -ldflags "-s -w" -o ${BINARY}
8 |
9 | clean:
10 | @echo ">> cleaning binaries"
11 | @if [ -f ${BINARY} ]; then rm ${BINARY}; fi
12 |
13 | format:
14 | @echo ">> formatting code"
15 | @go fmt ${pkgs}
16 |
17 | test:
18 | @echo ">> running all tests"
19 | @go test -p 1 ${pkgs}
20 |
21 | tools:
22 | @echo ">> install nesessary tools"
23 | @go get -u github.com/jteeuwen/go-bindata/...
24 | @go get -u github.com/vektra/mockery/...
25 |
26 | pack-schemas:
27 | @echo ">> writing json-schemas"
28 | @go-bindata -pkg schemas \
29 | -ignore ".*\.go" \
30 | -prefix "controllers/schemas" \
31 | -o controllers/schemas/bindata.go \
32 | controllers/schemas/...
33 | @go fmt ./controllers/schemas
34 |
35 | pack-migrations:
36 | @echo ">> writing migrations"
37 | @go-bindata -pkg migrations \
38 | -ignore ".*\.go" \
39 | -prefix "models/migrations" \
40 | -o models/migrations/bindata.go \
41 | models/migrations/...
42 | @go fmt ./models/migrations
43 |
44 | mailer-mock:
45 | @echo ">> generating mailer mock"
46 | @mockery -dir modules/mailer \
47 | -output modules/mailer/mock \
48 | -outpkg mailer_mock \
49 | -name Sender
50 | @go fmt ./modules/mailer/mock
51 |
52 | db-clean:
53 | @echo ">> cleaning database"
54 | @psql ${CTF_DB_DSN} -c "DROP OWNED BY ctfzone"
55 |
56 | migrations-up:
57 | @echo ">> migrations up"
58 | @migrate -database ${CTF_DB_DSN} -path ./models/migrations up
59 |
60 | migrations-down:
61 | @echo ">> migrations down"
62 | @migrate -database ${CTF_DB_DSN} -path ./models/migrations down
63 |
--------------------------------------------------------------------------------
/middlewares/csrf/handler.go:
--------------------------------------------------------------------------------
1 | package csrf
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | )
7 |
8 | var safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
9 |
10 | type handler struct {
11 | next http.Handler
12 | opts *options
13 | }
14 |
15 | // contains returns true if string is in slice.
16 | func contains(vals []string, s string) bool {
17 | for _, v := range vals {
18 | if v == s {
19 | return true
20 | }
21 | }
22 | return false
23 | }
24 |
25 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26 | real := getRealToken(r, h.opts.cookie.name)
27 | if len(real) != tokenLen {
28 | token := genToken()
29 | sendNewToken(w, token, h.opts.cookie)
30 | r = addTokenToRequest(r, token)
31 | } else {
32 | r = addTokenToRequest(r, real)
33 | }
34 |
35 | if !contains(safeMethods, r.Method) {
36 | sent := getSentToken(r, h.opts.header)
37 |
38 | if real == nil || !compareTokens(real, sent) {
39 | h.opts.errorFunc(w, r, errors.New("tokens do not match"))
40 | return
41 | }
42 | }
43 |
44 | // Set the "Vary: Cookie" header to protect clients from caching the response.
45 | w.Header().Add("Vary", "Cookie")
46 |
47 | h.next.ServeHTTP(w, r)
48 | }
49 |
50 | func defaultErrorFunc(w http.ResponseWriter, r *http.Request, err error) {
51 | http.Error(w, "Wrong CSRF-token", 403)
52 | }
53 |
54 | func New(opts ...Option) func(http.Handler) http.Handler {
55 | return func(next http.Handler) http.Handler {
56 | do := *defaultOptions
57 |
58 | h := &handler{
59 | next: next,
60 | opts: &do,
61 | }
62 |
63 | for _, f := range opts {
64 | f(h.opts)
65 | }
66 |
67 | return h
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/web/admin/src/images/logo-big.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/controllers/challenges_test.go:
--------------------------------------------------------------------------------
1 | package controllers_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/ctf-zone/ctfzone/controllers"
7 | )
8 |
9 | func TestChallengesList(t *testing.T) {
10 | setup(t)
11 | defer teardown(t)
12 |
13 | e := heDefault(t)
14 | e = heHost(e, "ctfzone.test")
15 | e = heCSRF(e)
16 | e = heAuth(e, "lcbc@mail.com", "lcbc")
17 |
18 | res := e.GET("/api/challenges").
19 | Expect().
20 | Status(200)
21 |
22 | checkJSONSchema(t, "Challenges.json", res.Body().Raw())
23 |
24 | res.JSON().Array().Length().Equal(3)
25 | }
26 |
27 | func TestChallengesGet_InvalidID(t *testing.T) {
28 | setup(t)
29 | defer teardown(t)
30 |
31 | e := heDefault(t)
32 | e = heHost(e, "ctfzone.test")
33 | e = heCSRF(e)
34 | e = heAuth(e, "lcbc@mail.com", "lcbc")
35 |
36 | res := e.GET("/api/challenges/").
37 | Expect().
38 | Status(400)
39 |
40 | res.JSON().Path("$.error").String().Equal(ErrInvalidID.Msg)
41 | }
42 |
43 | func TestChallengesGet_NotExistingChallenge(t *testing.T) {
44 | setup(t)
45 | defer teardown(t)
46 |
47 | e := heDefault(t)
48 | e = heHost(e, "ctfzone.test")
49 | e = heCSRF(e)
50 | e = heAuth(e, "lcbc@mail.com", "lcbc")
51 |
52 | res := e.GET("/api/challenges/1337").
53 | Expect().
54 | Status(404)
55 |
56 | res.JSON().Path("$.error").String().Equal(ErrChallengeNotFound.Msg)
57 | }
58 |
59 | func TestChallengesGet_Success(t *testing.T) {
60 | setup(t)
61 | defer teardown(t)
62 |
63 | e := heDefault(t)
64 | e = heHost(e, "ctfzone.test")
65 | e = heCSRF(e)
66 | e = heAuth(e, "lcbc@mail.com", "lcbc")
67 |
68 | res := e.GET("/api/challenges/1").
69 | Expect().
70 | Status(200)
71 |
72 | checkJSONSchema(t, "ChallengeE.json", res.Body().Raw())
73 | }
74 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { withRouter, Switch, Route } from 'react-router-dom';
4 |
5 | import { NotFound } from '../../../components';
6 | import Login from './Login';
7 | import Signup from './Signup';
8 | import Logout from './Logout';
9 | import SendToken from './SendToken';
10 | import ResetPassword from './ResetPassword';
11 | import Activate from './Activate';
12 |
13 | class Auth extends Component {
14 | static propTypes = {
15 | match: PropTypes.object,
16 | history: PropTypes.object
17 | };
18 |
19 | componentDidMount() {
20 | const { match, history } = this.props;
21 |
22 | if (match.isExact) {
23 | history.push('/auth/login');
24 | }
25 | }
26 |
27 | render() {
28 | const { match } = this.props;
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
40 |
45 |
50 |
51 |
52 | );
53 | }
54 | }
55 |
56 | export default withRouter(Auth);
57 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/components/nav/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 | import { Menu, Icon } from "antd";
5 | import { withRouter } from "react-router";
6 |
7 | class Nav extends Component {
8 | static propTypes = {
9 | location: PropTypes.object,
10 | };
11 |
12 |
13 | render() {
14 | const { location } = this.props;
15 |
16 | const menuItems = [
17 | {
18 | title: 'Users',
19 | path: '/users',
20 | icon: 'team',
21 | },
22 | {
23 | title: 'Challenges',
24 | path: '/challenges',
25 | icon: 'bulb',
26 | },
27 | {
28 | title: 'Announcements',
29 | path: '/announcements',
30 | icon: 'notification',
31 | },
32 | {
33 | title: 'Files',
34 | path: '/files',
35 | icon: 'file',
36 | },
37 | ]
38 |
39 | let selectedKeys = [];
40 | menuItems.forEach((m, i) => {
41 | if (m.path === "/") {
42 | if (location.pathname === "/") selectedKeys.push(i.toString());
43 | return;
44 | }
45 |
46 | if (location.pathname.startsWith(m.path)) selectedKeys.push(i.toString());
47 | });
48 |
49 | return (
50 |
60 | );
61 | }
62 | }
63 |
64 | export default withRouter(Nav);
65 |
--------------------------------------------------------------------------------
/web/admin/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | CTFZone Admin Panel
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/web/admin/src/models/users.js:
--------------------------------------------------------------------------------
1 | import api from '../utils/api'
2 | import parseLinkHeader from 'parse-link-header'
3 | import { stringify } from 'qs'
4 |
5 | export default {
6 | state: {
7 | users: [],
8 | user: {},
9 | links: {},
10 | },
11 | reducers: {
12 | set: (state, payload) => {
13 | return { ...state, ...payload }
14 | },
15 | },
16 | effects: {
17 | async list({ link = null, filters = {} }) {
18 | const { extra, ...restFilters } = filters
19 |
20 | let extraQuery = Object.
21 | entries(extra || {}).
22 | reduce((acc, v) => acc += `${v[0]}:${v[1].join(',')};`, '')
23 |
24 | let query = extraQuery.length !== 0
25 | ? `extra=${extraQuery}&`
26 | : ''
27 |
28 | query += stringify(restFilters, { allowDots: true })
29 |
30 | const response = await api.get(link || 'users?' + query)
31 | const links = parseLinkHeader(response.headers.link) || {}
32 |
33 | const users = response.data
34 | this.set({ users, links })
35 |
36 | return users
37 | },
38 | async get(userId) {
39 | const response = await api.get(`users/${userId}`)
40 | const user = response.data
41 | this.set({ user })
42 | return user
43 | },
44 | async delete(userId) {
45 | await api.delete(`users/${userId}`)
46 | },
47 | async create(data) {
48 | const response = await api.post(`users`, data)
49 | const user = response.data
50 | this.set({ user })
51 | return user
52 | },
53 | async update({ id, ...rest }) {
54 | const response = await api.put(`users/${id}`, rest)
55 | const user = response.data
56 | this.set({ user })
57 | return user
58 | },
59 | },
60 | }
61 |
--------------------------------------------------------------------------------
/controllers/announcements.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 |
7 | "github.com/ctf-zone/ctfzone/models"
8 | "github.com/go-chi/chi"
9 | )
10 |
11 | // AnnouncementsList returns all announcements
12 | func AnnouncementsList(db *models.Repository) http.HandlerFunc {
13 |
14 | type Params struct {
15 | models.AnnouncementsFilters
16 | models.Pagination
17 | }
18 |
19 | return func(w http.ResponseWriter, r *http.Request) {
20 | var params Params
21 |
22 | if err := decoder.Decode(¶ms, r.URL.Query()); err != nil {
23 | handleError(w, r, ErrInvalidQueryParams.SetMessage(err.Error()))
24 | return
25 | }
26 |
27 | announcements, _, err := db.AnnouncementsList(
28 | models.AnnouncementsFilter(params.AnnouncementsFilters),
29 | models.AnnouncementsPagination(params.Pagination),
30 | )
31 | if err != nil {
32 | handleError(w, r, ErrInternal.SetError(err))
33 | return
34 | }
35 |
36 | // schema: Announcements
37 | if err := responseJSON(w, announcements); err != nil {
38 | handleError(w, r, ErrInternal.SetError(err))
39 | return
40 | }
41 | }
42 | }
43 |
44 | func AnnouncementsGet(db *models.Repository) http.HandlerFunc {
45 | return func(w http.ResponseWriter, r *http.Request) {
46 | announcementID, err := strconv.ParseInt(chi.URLParam(r, "announcementId"), 10, 32)
47 | if err != nil {
48 | handleError(w, r, ErrInvalidID)
49 | return
50 | }
51 |
52 | announcement, err := db.AnnouncementsOneByID(announcementID)
53 | if err != nil {
54 | handleError(w, r, ErrInternal.SetError(err))
55 | return
56 | }
57 |
58 | if err := responseJSON(w, announcement); err != nil {
59 | handleError(w, r, ErrInternal.SetError(err))
60 | return
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/middlewares/schemacheck/handler.go:
--------------------------------------------------------------------------------
1 | package schemacheck
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "net/http"
7 |
8 | "github.com/xeipuuv/gojsonschema"
9 | )
10 |
11 | type handler struct {
12 | next http.Handler
13 | opts *options
14 | }
15 |
16 | func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
17 |
18 | // Load request body.
19 | body, err := ioutil.ReadAll(r.Body)
20 | if err != nil {
21 | h.opts.errorFunc(w, r, &Error{Msg: "invalid json"})
22 | return
23 | }
24 |
25 | // Validate schema.
26 | res, err := h.opts.schema.Validate(gojsonschema.NewBytesLoader(body))
27 | if err != nil {
28 | h.opts.errorFunc(w, r, &Error{Msg: "invalid json"})
29 | return
30 | }
31 |
32 | // Check validation successful.
33 | if !res.Valid() {
34 | e := &Error{Msg: "validation failed"}
35 | for _, err := range res.Errors() {
36 | e.Errs = append(e.Errs, &FieldError{Field: err.Field(), Msg: err.Description()})
37 | }
38 |
39 | h.opts.errorFunc(w, r, e)
40 | return
41 | }
42 |
43 | // Restore body
44 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
45 |
46 | h.next.ServeHTTP(w, r)
47 | }
48 |
49 | func defaultErrorFunc(w http.ResponseWriter, r *http.Request, err error) {
50 | http.Error(w, "Bad request", 400)
51 | }
52 |
53 | func New(schema gojsonschema.JSONLoader, opts ...Option) func(http.Handler) http.Handler {
54 | return func(next http.Handler) http.Handler {
55 | do := *defaultOptions
56 |
57 | // Load schema
58 | s, err := gojsonschema.NewSchema(schema)
59 |
60 | if err != nil {
61 | panic(err)
62 | }
63 |
64 | do.schema = s
65 |
66 | h := &handler{
67 | next: next,
68 | opts: &do,
69 | }
70 |
71 | for _, fn := range opts {
72 | fn(h.opts)
73 | }
74 |
75 | return h
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/templates/email/ext/reset.tmpl:
--------------------------------------------------------------------------------
1 | {{define "meta"}}
2 | {
3 | "subject": "Reset password",
4 | "from": "MCTF "
5 | }
6 | {{end}}
7 |
8 | {{define "content"}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | |
20 | Hello {{ .Name }}!
21 | Click on button below to reset your password
22 |
23 |
24 |
25 | |
26 |
33 | |
34 |
35 |
36 |
37 | Please do not reply to this e-mail.
38 | |
39 |
40 |
41 | |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
59 | {{end}}
60 |
--------------------------------------------------------------------------------
/web/public/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/web/admin/src/scenes/scenes/main/components/sidebar/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Layout, Icon } from 'antd'
4 |
5 | import logoBig from '../../../../../images/logo-big.svg'
6 | import logoSmall from '../../../../../images/logo-small.svg'
7 |
8 | import styles from './Sidebar.module.css'
9 |
10 | class Sidebar extends Component {
11 |
12 | static propTypes = {
13 | children: PropTypes.node,
14 | }
15 |
16 | state = {
17 | collapsed: false,
18 | }
19 |
20 | toggleSiderbar = () => {
21 | const { collapsed } = this.state
22 |
23 | this.setState({
24 | collapsed: !collapsed,
25 | })
26 | }
27 |
28 | render() {
29 | const { collapsed } = this.state
30 | const { children } = this.props
31 |
32 | const logo = collapsed
33 | ? logoSmall
34 | : logoBig
35 |
36 | const toggleIcon = collapsed ? 'right' : 'left'
37 |
38 | return (
39 |
43 |
46 |
47 |
48 |

49 |
50 |
51 |
52 | {children}
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 | }
67 |
68 | export default Sidebar
69 |
--------------------------------------------------------------------------------
/controllers/admin_files.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "crypto/sha256"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "os"
9 | "path"
10 |
11 | "github.com/ctf-zone/ctfzone/config"
12 | )
13 |
14 | func AdminFilesUpload(cfg *config.Files, baseURL string) http.HandlerFunc {
15 |
16 | type Response struct {
17 | URL string `json:"url"`
18 | }
19 |
20 | return func(w http.ResponseWriter, r *http.Request) {
21 | r.ParseMultipartForm(cfg.MaxSize)
22 |
23 | file, hdr, err := r.FormFile("file")
24 | if err != nil {
25 | handleError(w, r, ErrInternal.SetError(err))
26 | return
27 | }
28 | defer file.Close()
29 |
30 | h := sha256.New()
31 | if _, err := io.Copy(h, file); err != nil {
32 | handleError(w, r, ErrInternal.SetError(err))
33 | return
34 | }
35 |
36 | fpath := path.Join(cfg.Path, path.Base(
37 | fmt.Sprintf("%s.%x", hdr.Filename, h.Sum(nil)),
38 | ))
39 |
40 | file.Seek(0, os.SEEK_SET)
41 |
42 | f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE, 0666)
43 | if err != nil {
44 | handleError(w, r, ErrInternal.SetError(err))
45 | return
46 | }
47 | defer f.Close()
48 |
49 | if _, err := io.Copy(f, file); err != nil {
50 | handleError(w, r, ErrInternal.SetError(err))
51 | return
52 | }
53 |
54 | res := &Response{
55 | URL: fmt.Sprintf("%s/%s", baseURL, fpath),
56 | }
57 |
58 | if err := responseJSON(w, res); err != nil {
59 | handleError(w, r, ErrInternal.SetError(err))
60 | }
61 | }
62 | }
63 |
64 | func AdminFilesGetAll(c *config.Files) http.HandlerFunc {
65 |
66 | return func(w http.ResponseWriter, r *http.Request) {
67 |
68 | files, err := getDirFiles(c.Path)
69 | if err != nil {
70 | handleError(w, r, ErrInternal.SetError(err))
71 | return
72 | }
73 |
74 | if err := responseJSON(w, files); err != nil {
75 | handleError(w, r, ErrInternal.SetError(err))
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/templates/email/ext/activate.tmpl:
--------------------------------------------------------------------------------
1 | {{define "meta"}}
2 | {
3 | "subject": "Account activation",
4 | "from": "MCTF "
5 | }
6 | {{end}}
7 |
8 | {{define "content"}}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | |
20 | Hello {{ .Name }}!
21 | Thanks for participating in our CTF! By clicking on the link below to activate your account, you accept our rules
22 |
23 |
24 |
25 | |
26 |
33 | |
34 |
35 |
36 |
37 | Please do not reply to this e-mail.
38 | |
39 |
40 |
41 | |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
59 | {{end}}
60 |
--------------------------------------------------------------------------------
/models/models_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "testing"
8 |
9 | "github.com/go-testfixtures/testfixtures"
10 | "github.com/jmoiron/sqlx"
11 | _ "github.com/lib/pq"
12 | "github.com/pkg/errors"
13 | "github.com/stretchr/testify/require"
14 |
15 | "github.com/ctf-zone/ctfzone/models"
16 | . "github.com/ctf-zone/ctfzone/models"
17 | "github.com/ctf-zone/ctfzone/models/migrations"
18 | )
19 |
20 | var (
21 | db *Repository
22 | dbx *sqlx.DB
23 | fixtures *testfixtures.Context
24 | )
25 |
26 | func TestMain(m *testing.M) {
27 | if err := setupGlobals(); err != nil {
28 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
29 | os.Exit(1)
30 | }
31 |
32 | ret := m.Run()
33 |
34 | if err := teardownGlobals(); err != nil {
35 | fmt.Fprintf(os.Stderr, "error: %v\n", err)
36 | os.Exit(1)
37 | }
38 |
39 | os.Exit(ret)
40 | }
41 |
42 | func setupGlobals() error {
43 | dsn, ok := os.LookupEnv("CTF_DB_DSN")
44 | if !ok {
45 | return errors.New("empty CTF_DB_DSN")
46 | }
47 |
48 | var err error
49 |
50 | db, err = models.New(dsn)
51 | if err != nil {
52 | return errors.Wrap(err, "fail to init models")
53 | }
54 |
55 | dbx, err = sqlx.Connect("postgres", dsn)
56 | if err != nil {
57 | return errors.Wrap(err, "fail to connect database")
58 | }
59 |
60 | if err := migrations.Up(dsn); err != nil {
61 | log.Fatal(err)
62 | }
63 |
64 | fixtures, err = testfixtures.NewFolder(dbx.DB,
65 | &testfixtures.PostgreSQL{}, "fixtures")
66 | if err != nil {
67 | return errors.Wrap(err, "fail to load fixtures")
68 | }
69 |
70 | return nil
71 | }
72 |
73 | func teardownGlobals() error {
74 | if db != nil {
75 | if err := db.Close(); err != nil {
76 | return errors.Wrap(err, "fail to close connection to database")
77 | }
78 | }
79 | return nil
80 | }
81 |
82 | func setup(t *testing.T) {
83 | err := fixtures.Load()
84 | require.NoError(t, err)
85 | }
86 |
87 | func teardown(t *testing.T) {}
88 |
--------------------------------------------------------------------------------
/web/admin/src/components/filters/withFilters.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import * as _ from 'lodash'
3 |
4 | import { set, unset } from '../../utils/object'
5 |
6 | export default (WrappedComponent) => {
7 | return class WithFilters extends Component {
8 | state = {
9 | filters: {},
10 | resetFilters: {},
11 | }
12 |
13 | static defaultState = {
14 | filters: {},
15 | resetFilters: {},
16 | }
17 |
18 | isFiltered = (path) => {
19 | return _.has(this.state.filters, path)
20 | }
21 |
22 | handleFilterSet = (path, value, confirm, clearFilters) => () => {
23 | confirm()
24 |
25 | const resetFilters = { ...this.state.resetFilters }
26 |
27 | if (!([path] in resetFilters)) {
28 | resetFilters[path] = clearFilters
29 | }
30 |
31 | this.setState({
32 | filters: set(path, value, this.state.filters),
33 | resetFilters,
34 | })
35 | }
36 |
37 | handleFilterReset = (path, clearFilters) => () => {
38 | clearFilters()
39 |
40 | const resetFilters = { ...this.state.resetFilters }
41 |
42 | if ([path] in resetFilters) {
43 | delete resetFilters[path]
44 | }
45 |
46 | this.setState({
47 | filters: unset(path, this.state.filters),
48 | resetFilters,
49 | })
50 | }
51 |
52 | handleResetFilters = () => {
53 | Object.values(this.state.resetFilters).
54 | forEach((clear) => clear())
55 |
56 | this.setState({
57 | filters: {},
58 | })
59 | }
60 |
61 | render() {
62 | const props = {
63 | ...this.props,
64 | filters: this.state.filters,
65 | handleFilterSet: this.handleFilterSet,
66 | handleFilterReset: this.handleFilterReset,
67 | handleResetFilters: this.handleResetFilters,
68 | isFiltered: this.isFiltered,
69 | }
70 |
71 | return
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/challenge/FlagForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { createForm } from 'rc-form';
4 |
5 | import { FormItem, Button } from '../../../components';
6 |
7 | class FlagForm extends Component {
8 | static propTypes = {
9 | onSubmit: PropTypes.func.isRequired,
10 | form: PropTypes.object.isRequired,
11 | fields: PropTypes.object,
12 | className: PropTypes.string
13 | };
14 |
15 | handleSubmit = e => {
16 | e.preventDefault();
17 |
18 | const { getFieldsValue } = this.props.form;
19 | this.props.onSubmit(getFieldsValue());
20 | };
21 |
22 | renderFlagField({ getFieldDecorator, getFieldError }) {
23 | return (
24 |
25 | {getFieldDecorator('flag', {
26 | initialValue: '',
27 | rules: [
28 | {
29 | required: true,
30 | message: 'Flag is required'
31 | }
32 | ]
33 | })()}
34 |
35 | );
36 | }
37 |
38 | renderSubmitButton({ getFieldError, isFieldTouched }) {
39 | const canSubmit = ['flag'].reduce((result, field) => {
40 | return result && isFieldTouched(field) && !getFieldError(field);
41 | }, true);
42 |
43 | return (
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | render() {
51 | const { form, className } = this.props;
52 |
53 | return (
54 |
58 | );
59 | }
60 | }
61 |
62 | const onFieldsChange = (props, changed, all) => {
63 | props.onChange(all);
64 | };
65 |
66 | const mapPropsToFields = ({ fields }) => {
67 | return fields;
68 | };
69 |
70 | export default createForm({ onFieldsChange, mapPropsToFields })(FlagForm);
71 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/ResetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter, Link } from 'react-router-dom';
5 |
6 | import { Window } from '../../../components';
7 | import { addErrors } from '../../../utils/form';
8 | import ResetPasswordForm from './ResetPasswordForm';
9 |
10 | class ResetPassword extends Component {
11 | static propTypes = {
12 | authResetPassword: PropTypes.func,
13 | match: PropTypes.object,
14 | history: PropTypes.object
15 | };
16 |
17 | state = {
18 | fields: {}
19 | };
20 |
21 | handleSubmit = async values => {
22 | const { token } = this.props.match.params;
23 | const { authResetPassword, history } = this.props;
24 |
25 | try {
26 | await authResetPassword({ token, ...values }, { throw: true });
27 | history.push('/auth/login');
28 | } catch (e) {
29 | let errors = {};
30 |
31 | if (e.status === 400) {
32 | errors = { password: e.errors.token || e.errors.password };
33 | } else if (e.status === 404) {
34 | errors = { password: ['Token not found'] };
35 | }
36 |
37 | this.setState(({ fields }) => ({
38 | fields: addErrors(fields, errors)
39 | }));
40 | }
41 | };
42 |
43 | handleChange = fields => {
44 | this.setState({ fields });
45 | };
46 |
47 | render() {
48 | return (
49 |
50 |
56 |
57 | Log In
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const mapDispatchToProps = dispatch => ({
65 | authResetPassword: dispatch.auth.resetPassword
66 | });
67 |
68 | export default withRouter(
69 | connect(
70 | () => {},
71 | mapDispatchToProps
72 | )(ResetPassword)
73 | );
74 |
--------------------------------------------------------------------------------
/controllers/admin_auth.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/alexedwards/scs"
7 | "golang.org/x/crypto/bcrypt"
8 |
9 | "github.com/ctf-zone/ctfzone/config"
10 | "github.com/ctf-zone/ctfzone/models"
11 | )
12 |
13 | // AdminAuthLogin handles admin login
14 | // summary: Authorizes admin
15 | // tags: [auth]
16 | func AdminAuthLogin(c *config.Config, db *models.Repository, sm *scs.Manager) http.HandlerFunc {
17 |
18 | type Request struct {
19 | Password string `json:"password"`
20 | }
21 |
22 | return func(w http.ResponseWriter, r *http.Request) {
23 | var req Request
24 |
25 | if err := jsonDecode(r, &req); err != nil {
26 | handleError(w, r, ErrInvalidJSON)
27 | return
28 | }
29 |
30 | if err := bcrypt.CompareHashAndPassword(
31 | []byte(c.Admin.Password),
32 | []byte(req.Password),
33 | ); err != nil {
34 | handleError(w, r, ErrInvalidCreds)
35 | return
36 | }
37 |
38 | session := sm.Load(r)
39 |
40 | if err := session.RenewToken(w); err != nil {
41 | handleError(w, r, ErrInternal.SetError(err))
42 | return
43 | }
44 |
45 | if err := session.PutBool(w, "isAdmin", true); err != nil {
46 | handleError(w, r, ErrInternal.SetError(err))
47 | return
48 | }
49 |
50 | responseOK(w)
51 | }
52 | }
53 |
54 | // AdminAuthLogout handles admin logout
55 | // summary: Logout
56 | // tags: [auth]
57 | func AdminAuthLogout(sm *scs.Manager) http.HandlerFunc {
58 |
59 | type Request struct{}
60 |
61 | return func(w http.ResponseWriter, r *http.Request) {
62 |
63 | var req Request
64 |
65 | if err := jsonDecode(r, &req); err != nil {
66 | handleError(w, r, ErrInvalidJSON)
67 | return
68 | }
69 |
70 | session := sm.Load(r)
71 |
72 | if err := session.Clear(w); err != nil {
73 | handleError(w, r, ErrInternal.SetError(err))
74 | return
75 | }
76 |
77 | responseOK(w)
78 | }
79 | }
80 |
81 | // AdminAuthCheck handles auth check request
82 | // description: Returns 200 if admin is logged in
83 | // tags: [auth]
84 | func AdminAuthCheck() http.HandlerFunc {
85 | return func(w http.ResponseWriter, r *http.Request) {
86 | responseOK(w)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/controllers/challenges.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "database/sql"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/alexedwards/scs"
9 | "github.com/go-chi/chi"
10 |
11 | "github.com/ctf-zone/ctfzone/config"
12 | "github.com/ctf-zone/ctfzone/models"
13 | )
14 |
15 | // ChallengesList handles get all challenges request
16 | func ChallengesList(cfg *config.Scoring, db *models.Repository, sm *scs.Manager) http.HandlerFunc {
17 | return func(w http.ResponseWriter, r *http.Request) {
18 | session := sm.Load(r)
19 |
20 | userID, err := session.GetInt64("userId")
21 | if err != nil {
22 | handleError(w, r, ErrUnauthorizedRequest)
23 | return
24 | }
25 |
26 | challenges, _, err := db.ChallengesListE(
27 | cfg,
28 | models.ChallengesIncludeMeta(),
29 | models.ChallengesIncludeUser(userID),
30 | )
31 | if err != nil {
32 | handleError(w, r, ErrInternal.SetError(err))
33 | return
34 | }
35 |
36 | if err := responseJSON(w, challenges); err != nil {
37 | handleError(w, r, ErrInternal.SetError(err))
38 | return
39 | }
40 | }
41 | }
42 |
43 | // ChallengesGet handles get challenge request
44 | func ChallengesGet(cfg *config.Scoring, db *models.Repository, sm *scs.Manager) http.HandlerFunc {
45 | return func(w http.ResponseWriter, r *http.Request) {
46 | session := sm.Load(r)
47 |
48 | userID, err := session.GetInt64("userId")
49 | if err != nil {
50 | handleError(w, r, ErrUnauthorizedRequest)
51 | return
52 | }
53 |
54 | challengeID, err := strconv.ParseInt(chi.URLParam(r, "challengeId"), 10, 32)
55 | if err != nil {
56 | handleError(w, r, ErrInvalidID)
57 | return
58 | }
59 |
60 | c, err := db.ChallengesOneByIDE(
61 | cfg,
62 | challengeID,
63 | models.ChallengesIncludeMeta(),
64 | models.ChallengesIncludeUser(userID),
65 | )
66 |
67 | if err == sql.ErrNoRows {
68 | handleError(w, r, ErrChallengeNotFound)
69 | return
70 | } else if err != nil {
71 | handleError(w, r, ErrInternal.SetError(err))
72 | return
73 | }
74 |
75 | if err := responseJSON(w, c); err != nil {
76 | handleError(w, r, ErrInternal.SetError(err))
77 | return
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/news/Countdown.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class Countdown extends Component {
5 | static propTypes = {
6 | startTime: PropTypes.object,
7 | onStart: PropTypes.func
8 | };
9 |
10 | state = {
11 | intervalId: 0,
12 | currentTime: new Date().getTime()
13 | };
14 |
15 | componentDidMount() {
16 | const intervalId = setInterval(this.updateTime, 1000);
17 | this.setState({ intervalId });
18 | }
19 |
20 | componentWillUnmount() {
21 | clearInterval(this.state.intervalId);
22 | }
23 |
24 | updateTime = () => {
25 | const now = new Date().getTime();
26 | const { startTime } = this.props;
27 |
28 | if (now <= startTime) {
29 | this.setState({ currentTime: now });
30 | } else {
31 | this.setState({ currentTime: startTime });
32 | clearInterval(this.state.intervalId);
33 | this.props.onStart();
34 | }
35 | };
36 |
37 | render() {
38 | const { startTime } = this.props;
39 | const { currentTime } = this.state;
40 |
41 | const distance = startTime - currentTime;
42 |
43 | const days = Math.floor(distance / (1000 * 60 * 60 * 24));
44 | const hours = Math.floor(
45 | (distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
46 | );
47 | const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
48 | const seconds = Math.floor((distance % (1000 * 60)) / 1000);
49 |
50 | const prefixClass = 'ctf-countdown';
51 |
52 | return (
53 |
54 |
Contest starts in
55 |
{('0' + days).slice(-2)}
56 |
57 | {('0' + hours).slice(-2)}
58 |
59 |
60 | {('0' + minutes).slice(-2)}
61 |
62 |
63 | {('0' + seconds).slice(-2)}
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | export default Countdown;
71 |
--------------------------------------------------------------------------------
/controllers/scores.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/ctf-zone/ctfzone/config"
9 | "github.com/ctf-zone/ctfzone/models"
10 | )
11 |
12 | // ScoresList handles scores request
13 | // summary: Returns list of scores
14 | // tags: [scores]
15 | func ScoresList(cfg *config.Scoring, db *models.Repository) http.HandlerFunc {
16 | return func(w http.ResponseWriter, r *http.Request) {
17 | scores, _, err := db.ScoresList(cfg)
18 |
19 | if err != nil {
20 | handleError(w, r, ErrInternal.SetError(err))
21 | return
22 | }
23 |
24 | // schema: Scores
25 | if err := responseJSON(w, scores); err != nil {
26 | handleError(w, r, ErrInternal.SetError(err))
27 | return
28 | }
29 | }
30 | }
31 |
32 | // ScoresCtftimeList returns scoreboard in ctftime format
33 | // summary: Returns scores in CTFTime format
34 | // description: https://ctftime.org/json-scoreboard-feed
35 | // tags: [scores]
36 | func ScoresCtftimeList(cfg *config.Scoring, db *models.Repository) http.HandlerFunc {
37 |
38 | type ScoreCtftime struct {
39 | Pos int `json:"pos"`
40 | Team json.RawMessage `json:"team"`
41 | Score int `json:"score"`
42 | LastAccept int64 `json:"lastAccept"`
43 | }
44 |
45 | type Ctftime struct {
46 | Standings []*ScoreCtftime `json:"standings"`
47 | }
48 |
49 | return func(w http.ResponseWriter, r *http.Request) {
50 | scores, _, err := db.ScoresList(cfg)
51 |
52 | if err != nil {
53 | handleError(w, r, ErrInternal.SetError(err))
54 | return
55 | }
56 |
57 | ctftime := &Ctftime{}
58 |
59 | standings := make([]*ScoreCtftime, 0)
60 |
61 | for _, s := range scores {
62 | if s.Score != 0 {
63 | standings = append(standings, &ScoreCtftime{
64 | Pos: s.Rank,
65 | Score: s.Score,
66 | Team: []byte(fmt.Sprintf("%+q", s.UserP.Name)),
67 | LastAccept: s.UpdatedAt.Unix(),
68 | })
69 | }
70 | }
71 |
72 | ctftime.Standings = standings
73 |
74 | // schema: ScoresCTFTime
75 | if err := responseJSON(w, ctftime); err != nil {
76 | handleError(w, r, ErrInternal.SetError(err))
77 | return
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/SendToken.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter, Link } from 'react-router-dom';
5 |
6 | import { Window } from '../../../components';
7 | import { addErrors } from '../../../utils/form';
8 | import SendTokenForm from './SendTokenForm';
9 |
10 | class SendToken extends Component {
11 | static propTypes = {
12 | authSendToken: PropTypes.func,
13 | match: PropTypes.object
14 | };
15 |
16 | state = {
17 | fields: {},
18 | tokenIsSent: false
19 | };
20 |
21 | handleSubmit = async values => {
22 | const { authSendToken } = this.props;
23 | const { type } = this.props.match.params;
24 |
25 | try {
26 | await authSendToken({ type, ...values }, { throw: true });
27 | this.setState({ tokenIsSent: true });
28 | } catch (e) {
29 | let errors = {};
30 |
31 | if (e.status === 400) {
32 | errors = e.errors;
33 | }
34 |
35 | this.setState(({ fields }) => ({
36 | fields: addErrors(fields, errors)
37 | }));
38 | }
39 | };
40 |
41 | handleChange = fields => {
42 | this.setState({ fields });
43 | };
44 |
45 | render() {
46 | const { tokenIsSent } = this.state;
47 |
48 | return (
49 |
50 | {tokenIsSent ? (
51 |
52 | Link was sent to your email. Please, check you inbox.
53 |
54 | ) : (
55 |
61 | )}
62 |
63 | Log In
64 |
65 |
66 | );
67 | }
68 | }
69 |
70 | const mapDispatchToProps = dispatch => ({
71 | authSendToken: dispatch.auth.sendToken
72 | });
73 |
74 | export default withRouter(
75 | connect(
76 | () => ({}),
77 | mapDispatchToProps
78 | )(SendToken)
79 | );
80 |
--------------------------------------------------------------------------------
/web/public/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `npm start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `npm test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `npm run build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `npm run eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
--------------------------------------------------------------------------------
/controllers/admin_announcements.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "database/sql"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/ctf-zone/ctfzone/models"
9 | "github.com/go-chi/chi"
10 | )
11 |
12 | func AdminAnnouncementsCreate(db *models.Repository) http.HandlerFunc {
13 | return func(w http.ResponseWriter, r *http.Request) {
14 | var a models.Announcement
15 |
16 | if err := jsonDecode(r, &a); err != nil {
17 | handleError(w, r, ErrInvalidJSON)
18 | return
19 | }
20 |
21 | if err := db.AnnouncementsInsert(&a); err != nil {
22 | handleError(w, r, ErrInternal.SetError(err))
23 | return
24 | }
25 |
26 | if err := responseJSON(w, a); err != nil {
27 | handleError(w, r, ErrInternal.SetError(err))
28 | return
29 | }
30 | }
31 | }
32 |
33 | func AdminAnnouncementsUpdate(db *models.Repository) http.HandlerFunc {
34 | return func(w http.ResponseWriter, r *http.Request) {
35 | var a models.Announcement
36 |
37 | if err := jsonDecode(r, &a); err != nil {
38 | handleError(w, r, ErrInvalidJSON)
39 | return
40 | }
41 |
42 | announcementID, err := strconv.ParseInt(chi.URLParam(r, "announcementId"), 10, 32)
43 | if err != nil {
44 | handleError(w, r, ErrInvalidID)
45 | return
46 | }
47 |
48 | a.ID = announcementID
49 |
50 | err = db.AnnouncementsUpdate(&a)
51 | if err == sql.ErrNoRows {
52 | handleError(w, r, ErrAnnouncementNotFound)
53 | return
54 | } else if err != nil {
55 | handleError(w, r, ErrInternal.SetError(err))
56 | return
57 | }
58 |
59 | if err := responseJSON(w, a); err != nil {
60 | handleError(w, r, ErrInternal.SetError(err))
61 | return
62 | }
63 | }
64 | }
65 |
66 | func AdminAnnouncementsDelete(db *models.Repository) http.HandlerFunc {
67 | return func(w http.ResponseWriter, r *http.Request) {
68 |
69 | announcementID, err := strconv.ParseInt(chi.URLParam(r, "announcementId"), 10, 32)
70 | if err != nil {
71 | handleError(w, r, ErrInvalidID)
72 | return
73 | }
74 |
75 | err = db.AnnouncementsDelete(announcementID)
76 | if err == sql.ErrNoRows {
77 | handleError(w, r, ErrAnnouncementNotFound)
78 | return
79 | } else if err != nil {
80 | handleError(w, r, ErrInternal.SetError(err))
81 | return
82 | }
83 |
84 | responseOK(w)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ctf-zone/ctfzone
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/ajg/form v1.5.1 // indirect
7 | github.com/alexedwards/scs v1.4.1
8 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
9 | github.com/elazarl/go-bindata-assetfs v1.0.0
10 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect
11 | github.com/fatih/structs v1.1.0 // indirect
12 | github.com/gavv/httpexpect v2.0.0+incompatible
13 | github.com/go-chi/chi v4.0.2+incompatible
14 | github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
15 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
16 | github.com/go-testfixtures/testfixtures v2.5.1+incompatible
17 | github.com/golang-migrate/migrate/v4 v4.6.1
18 | github.com/gorilla/schema v1.1.0
19 | github.com/imkira/go-interpol v1.1.0 // indirect
20 | github.com/jmoiron/sqlx v1.2.0
21 | github.com/joho/godotenv v1.3.0 // indirect
22 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
23 | github.com/lib/pq v1.2.0
24 | github.com/mattn/go-colorable v0.1.2 // indirect
25 | github.com/mattn/go-oci8 v0.0.0-20190906045332-63b1f03e8cae // indirect
26 | github.com/moul/http2curl v1.0.0 // indirect
27 | github.com/pkg/errors v0.8.1
28 | github.com/sergi/go-diff v1.0.0 // indirect
29 | github.com/sirupsen/logrus v1.4.2
30 | github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
31 | github.com/spf13/cobra v0.0.5
32 | github.com/spf13/viper v1.4.0
33 | github.com/stretchr/testify v1.3.0
34 | github.com/valyala/fasthttp v1.4.0 // indirect
35 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
36 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
37 | github.com/xeipuuv/gojsonschema v1.1.0
38 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
39 | github.com/yudai/gojsondiff v1.0.0 // indirect
40 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
41 | github.com/yudai/pp v2.0.1+incompatible // indirect
42 | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
43 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
44 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
45 | )
46 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/scoreboard/Scoreboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 |
5 | import dayjs from '../../../utils/date';
6 | import { Page } from '../../../components';
7 |
8 | class Scoreboard extends Component {
9 | static propTypes = {
10 | scores: PropTypes.object,
11 | scoresList: PropTypes.func
12 | };
13 |
14 | state = {
15 | intervalId: 0
16 | };
17 |
18 | componentDidMount() {
19 | this.fetchData();
20 | const intervalId = setInterval(this.fetchData, 30000);
21 | this.setState({ intervalId });
22 | }
23 |
24 | componentWillUnmount() {
25 | clearInterval(this.state.intervalId);
26 | }
27 |
28 | fetchData = () => {
29 | const { scoresList } = this.props;
30 | scoresList();
31 | };
32 |
33 | render() {
34 | const { items: scores } = this.props.scores;
35 |
36 | return (
37 |
38 |
39 |
40 |
41 | | Rank |
42 | Team |
43 | Score |
44 | Last flag submit |
45 |
46 |
47 |
48 | {scores.map((score, i) => {
49 | const updatedAt = dayjs(score.updatedAt);
50 | return (
51 |
52 | | {score.rank} |
53 | {score.user.name} |
54 | {score.score} |
55 |
56 | {score.updatedAt
57 | ? `${updatedAt.format(
58 | 'HH:mm DD.MM.YYYY'
59 | )} (${updatedAt.fromNow()})`
60 | : 'N/A'}
61 | |
62 |
63 | );
64 | })}
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | const mapStateToProps = state => ({
73 | scores: state.scores
74 | });
75 |
76 | const mapDispatchToProps = dispatch => ({
77 | scoresList: dispatch.scores.list
78 | });
79 |
80 | export default connect(
81 | mapStateToProps,
82 | mapDispatchToProps
83 | )(Scoreboard);
84 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/SendTokenForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { createForm } from 'rc-form';
4 |
5 | import { FormItem, Button } from '../../../components';
6 |
7 | class SendTokenForm extends Component {
8 | static propTypes = {
9 | onSubmit: PropTypes.func.isRequired,
10 | form: PropTypes.object.isRequired,
11 | fields: PropTypes.object,
12 | className: PropTypes.string
13 | };
14 |
15 | componentDidMount() {
16 | this.emailInput.focus();
17 | }
18 |
19 | handleSubmit = e => {
20 | e.preventDefault();
21 |
22 | const { getFieldsValue } = this.props.form;
23 | this.props.onSubmit(getFieldsValue());
24 | };
25 |
26 | renderEmailField({ getFieldDecorator, getFieldError }) {
27 | return (
28 |
29 | {getFieldDecorator('email', {
30 | initialValue: '',
31 | rules: [
32 | {
33 | required: true,
34 | message: 'Email is required'
35 | },
36 | {
37 | type: 'email',
38 | message: 'Invalid email'
39 | }
40 | ]
41 | })(
42 | (this.emailInput = el)}
46 | />
47 | )}
48 |
49 | );
50 | }
51 |
52 | renderSubmitButton({ getFieldError, isFieldTouched }) {
53 | const canSubmit = ['email'].reduce((result, field) => {
54 | return result && isFieldTouched(field) && !getFieldError(field);
55 | }, true);
56 |
57 | return (
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | render() {
65 | const { form, className } = this.props;
66 |
67 | return (
68 |
72 | );
73 | }
74 | }
75 |
76 | const onFieldsChange = (props, changed, all) => {
77 | props.onChange(all);
78 | };
79 |
80 | const mapPropsToFields = ({ fields }) => {
81 | return fields;
82 | };
83 |
84 | export default createForm({ onFieldsChange, mapPropsToFields })(SendTokenForm);
85 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/ResetPasswordForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { createForm } from 'rc-form';
4 |
5 | import { FormItem, Button } from '../../../components';
6 |
7 | class ResetPasswordForm extends Component {
8 | static propTypes = {
9 | onSubmit: PropTypes.func.isRequired,
10 | form: PropTypes.object.isRequired,
11 | fields: PropTypes.object,
12 | className: PropTypes.string
13 | };
14 |
15 | componentDidMount() {
16 | this.passwordInput.focus();
17 | }
18 |
19 | handleSubmit = e => {
20 | e.preventDefault();
21 |
22 | const { getFieldsValue } = this.props.form;
23 | this.props.onSubmit(getFieldsValue());
24 | };
25 |
26 | renderPaswordField({ getFieldDecorator, getFieldError }) {
27 | return (
28 |
29 | {getFieldDecorator('password', {
30 | initialValue: '',
31 | rules: [
32 | {
33 | required: true,
34 | message: 'Password is required'
35 | },
36 | {
37 | min: 8,
38 | message: 'Password is too short'
39 | }
40 | ]
41 | })(
42 | (this.passwordInput = el)}
46 | />
47 | )}
48 |
49 | );
50 | }
51 |
52 | renderSubmitButton({ getFieldError, isFieldTouched }) {
53 | const canSubmit = ['password'].reduce((result, field) => {
54 | return result && isFieldTouched(field) && !getFieldError(field);
55 | }, true);
56 |
57 | return (
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | render() {
65 | const { form, className } = this.props;
66 |
67 | return (
68 |
72 | );
73 | }
74 | }
75 |
76 | const onFieldsChange = (props, changed, all) => {
77 | props.onChange(all);
78 | };
79 |
80 | const mapPropsToFields = ({ fields }) => {
81 | return fields;
82 | };
83 |
84 | export default createForm({ onFieldsChange, mapPropsToFields })(
85 | ResetPasswordForm
86 | );
87 |
--------------------------------------------------------------------------------
/controllers/middlewares.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | "github.com/alexedwards/scs"
8 | assetfs "github.com/elazarl/go-bindata-assetfs"
9 | "github.com/xeipuuv/gojsonschema"
10 |
11 | "github.com/ctf-zone/ctfzone/controllers/schemas"
12 | "github.com/ctf-zone/ctfzone/middlewares/schemacheck"
13 | )
14 |
15 | func isLoggedIn(sm *scs.Manager) func(http.Handler) http.Handler {
16 | return func(next http.Handler) http.Handler {
17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 |
19 | session := sm.Load(r)
20 |
21 | userID, err := session.GetInt("userId")
22 | if err != nil || userID <= 0 {
23 | handleError(w, r, &Error{Code: 401, Msg: "Unauthorized request"})
24 | return
25 | }
26 |
27 | next.ServeHTTP(w, r)
28 | })
29 | }
30 | }
31 |
32 | func isAdmin(sm *scs.Manager) func(http.Handler) http.Handler {
33 | return func(next http.Handler) http.Handler {
34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35 |
36 | session := sm.Load(r)
37 |
38 | isAdmin, err := session.GetBool("isAdmin")
39 | if err != nil || !isAdmin {
40 | handleError(w, r, &Error{Code: 403, Msg: "Access denied"})
41 | return
42 | }
43 |
44 | next.ServeHTTP(w, r)
45 | })
46 | }
47 | }
48 |
49 | func validate(schemaName string) func(http.Handler) http.Handler {
50 | return schemacheck.New(
51 | // Load schema from go-bindata file system.
52 | gojsonschema.NewReferenceLoaderFileSystem(
53 | fmt.Sprintf("file://%s", schemaName),
54 | &assetfs.AssetFS{
55 | Asset: schemas.Asset,
56 | AssetDir: schemas.AssetDir,
57 | AssetInfo: schemas.AssetInfo,
58 | },
59 | ),
60 | schemacheck.ErrorFunc(func(w http.ResponseWriter, r *http.Request, err error) {
61 | t := err.(*schemacheck.Error)
62 | e := &Error{
63 | Code: 400,
64 | Msg: t.Msg,
65 | Errs: make(map[string][]string),
66 | }
67 | for _, fe := range t.Errs {
68 | if _, ok := e.Errs[fe.Field]; !ok {
69 | e.Errs[fe.Field] = make([]string, 0)
70 | }
71 | e.Errs[fe.Field] = append(e.Errs[fe.Field], fe.Msg)
72 | }
73 | handleError(w, r, e)
74 | }),
75 | )
76 | }
77 |
78 | func conditional(handler func(http.Handler) http.Handler, condition bool) func(http.Handler) http.Handler {
79 |
80 | if condition {
81 | return handler
82 | }
83 |
84 | return func(next http.Handler) http.Handler {
85 | return next
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter, Link } from 'react-router-dom';
5 |
6 | import { Window } from '../../../components';
7 | import { addErrors } from '../../../utils/form';
8 | import LoginForm from './LoginForm';
9 |
10 | class Login extends Component {
11 | static propTypes = {
12 | authLogin: PropTypes.func,
13 | authSendToken: PropTypes.func
14 | };
15 |
16 | state = {
17 | fields: {},
18 | activationError: false
19 | };
20 |
21 | handleSubmit = async values => {
22 | const { authLogin, history } = this.props;
23 |
24 | try {
25 | await authLogin(values, { throw: true });
26 | history.push('/news');
27 | } catch (e) {
28 | let errors = {};
29 |
30 | if (e.status === 400) {
31 | errors = e.errors;
32 | } else if (e.status === 401) {
33 | errors = { password: [e.message] };
34 | } else if (e.status === 422) {
35 | this.setState({ activationError: true });
36 | return;
37 | }
38 |
39 | this.setState(({ fields }) => ({
40 | fields: addErrors(fields, errors)
41 | }));
42 | }
43 | };
44 |
45 | handleChange = fields => {
46 | this.setState({ fields });
47 | };
48 |
49 | render() {
50 | const { activationError } = this.state;
51 |
52 | return (
53 |
54 | {activationError ? (
55 |
56 | Account is not activated.
57 |
58 | Resend token
59 |
60 | ) : (
61 |
67 | )}
68 |
69 | Reset Password
70 | Sign Up
71 |
72 |
73 | );
74 | }
75 | }
76 |
77 | const mapDispatchToProps = dispatch => ({
78 | authLogin: dispatch.auth.login,
79 | authSendToken: dispatch.auth.sendToken
80 | });
81 |
82 | export default withRouter(
83 | connect(
84 | () => ({}),
85 | mapDispatchToProps
86 | )(Login)
87 | );
88 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { withRouter, Link } from 'react-router-dom';
5 |
6 | import { Window } from '../../../components';
7 | import { addErrors } from '../../../utils/form';
8 | import SignupForm from './SignupForm';
9 |
10 | class Signup extends Component {
11 | static propTypes = {
12 | authRegister: PropTypes.func,
13 | history: PropTypes.object
14 | };
15 |
16 | state = {
17 | fields: {},
18 | reCaptchaResponse: '',
19 | tokenIsSent: false
20 | };
21 |
22 | handleSubmit = async values => {
23 | const { authRegister } = this.props;
24 | const { reCaptchaResponse } = this.state
25 |
26 | try {
27 | console.log({...values, reCaptchaResponse});
28 | await authRegister({ ...values, reCaptchaResponse }, { throw: true });
29 | this.setState({ tokenIsSent: true });
30 | } catch (e) {
31 | if (e.status === 400 || e.status === 409) {
32 | this.setState(({ fields }) => ({
33 | fields: addErrors(fields, e.errors)
34 | }));
35 | }
36 | }
37 | };
38 |
39 | handleChange = fields => {
40 | this.setState({ fields });
41 | };
42 |
43 | handleReCaptcha = response => {
44 | console.log(response);
45 | this.setState({ reCaptchaResponse: response });
46 | }
47 |
48 | render() {
49 | const { tokenIsSent } = this.state;
50 |
51 | return (
52 |
53 | {tokenIsSent ? (
54 |
55 | Link was sent to your email. Please, check you inbox.
56 |
57 | ) : (
58 |
65 | )}
66 |
67 | Log In
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | const mapStateToProps = state => ({
75 | ...state.api.effects.auth.register
76 | });
77 |
78 | const mapDispatchToProps = dispatch => ({
79 | authRegister: dispatch.auth.register
80 | });
81 |
82 | export default withRouter(
83 | connect(
84 | mapStateToProps,
85 | mapDispatchToProps
86 | )(Signup)
87 | );
88 |
--------------------------------------------------------------------------------
/models/tokens.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "io"
7 | "time"
8 | )
9 |
10 | type TokenType string
11 |
12 | const (
13 | TokenTypeActivate = TokenType("activate")
14 | TokenTypeReset = TokenType("reset")
15 | )
16 |
17 | type Token struct {
18 | ID int64 `db:"id,omitempty" json:"id"`
19 | UserID int64 `db:"user_id" json:"userId"`
20 | Token string `db:"token" json:"token"`
21 | Type TokenType `db:"type" json:"type"`
22 | ExpiresAt time.Time `db:"expires_at" json:"expiresAt"`
23 | CreatedAt time.Time `db:"created_at" json:"createdAt"`
24 | }
25 |
26 | const tokenLength = 32
27 |
28 | func (r *Repository) TokensNew(userID int64, tp TokenType, lifetime time.Duration) (*Token, error) {
29 | token := make([]byte, tokenLength)
30 |
31 | if n, err := io.ReadFull(rand.Reader, token); err != nil || n != tokenLength {
32 | return nil, err
33 | }
34 |
35 | t := &Token{
36 | UserID: userID,
37 | Type: tp,
38 | Token: hex.EncodeToString(token),
39 | ExpiresAt: time.Now().Add(lifetime).UTC(),
40 | }
41 |
42 | return t, nil
43 | }
44 |
45 | func (r *Repository) TokensInsert(o *Token) error {
46 | o.CreatedAt = now()
47 |
48 | stmt, err := r.db.PrepareNamed(
49 | "INSERT INTO tokens (user_id, token, type, expires_at, created_at) " +
50 | "VALUES(:user_id, :token, :type, :expires_at, :created_at) " +
51 | "RETURNING id")
52 |
53 | if err != nil {
54 | return err
55 | }
56 |
57 | return stmt.QueryRowx(o).Scan(&o.ID)
58 | }
59 |
60 | func (r *Repository) TokensOneByID(id int64) (*Token, error) {
61 | var o Token
62 |
63 | err := r.db.Get(&o, "SELECT * FROM tokens WHERE id = $1", id)
64 |
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | return &o, nil
70 | }
71 |
72 | func (r *Repository) TokensOneByTokenAndType(token string, tp TokenType) (*Token, error) {
73 | var o Token
74 |
75 | err := r.db.Get(&o, "SELECT * FROM tokens WHERE token = $1 and type = $2", token, tp)
76 |
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | return &o, nil
82 | }
83 |
84 | func (r *Repository) TokensOneByUserAndType(userID int64, tp TokenType) (*Token, error) {
85 | var o Token
86 |
87 | err := r.db.Get(&o, "SELECT * FROM tokens WHERE user_id = $1 and type = $2", userID, tp)
88 |
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | return &o, nil
94 | }
95 |
96 | func (r *Repository) TokensDelete(id int64) error {
97 | return r.db.QueryRow("DELETE FROM tokens WHERE id = $1 RETURNING id", id).Scan(&id)
98 | }
99 |
--------------------------------------------------------------------------------
/models/announcements_test.go:
--------------------------------------------------------------------------------
1 | package models_test
2 |
3 | import (
4 | "database/sql"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | . "github.com/ctf-zone/ctfzone/models"
11 | )
12 |
13 | func Test_Announcements_Insert_Success(t *testing.T) {
14 | setup(t)
15 | defer teardown(t)
16 |
17 | o := &Announcement{
18 | Title: "Test title",
19 | Body: "Test body",
20 | ChallengeID: nil,
21 | }
22 |
23 | err := db.AnnouncementsInsert(o)
24 | assert.NoError(t, err)
25 | assert.NotZero(t, o.ID)
26 | assert.WithinDuration(t, time.Now().UTC(), o.CreatedAt, 5*time.Second)
27 | assert.WithinDuration(t, time.Now().UTC(), o.UpdatedAt, 5*time.Second)
28 | }
29 |
30 | func Test_Announcements_Update_Success(t *testing.T) {
31 | setup(t)
32 | defer teardown(t)
33 |
34 | o1, err := db.AnnouncementsOneByID(1)
35 | assert.NoError(t, err)
36 |
37 | o1.Title = o1.Title + " [Updated]"
38 | updatedAt := o1.UpdatedAt
39 |
40 | err = db.AnnouncementsUpdate(o1)
41 | assert.NoError(t, err)
42 |
43 | o2, err := db.AnnouncementsOneByID(1)
44 | assert.NoError(t, err)
45 | assert.True(t, o2.UpdatedAt.After(updatedAt))
46 | assert.WithinDuration(t, time.Now(), o2.UpdatedAt, 5*time.Second)
47 |
48 | assert.Equal(t, o1, o2)
49 | }
50 |
51 | func Test_Announcements_Update_NotExist(t *testing.T) {
52 | setup(t)
53 | defer teardown(t)
54 |
55 | o := &Announcement{
56 | ID: 1337,
57 | }
58 |
59 | err := db.AnnouncementsUpdate(o)
60 | assert.Error(t, err)
61 | assert.EqualError(t, err, sql.ErrNoRows.Error())
62 | }
63 |
64 | func Test_Announcements_OneByID_Success(t *testing.T) {
65 | o, err := db.AnnouncementsOneByID(1)
66 | assert.NoError(t, err)
67 | assert.Equal(t, int64(1), o.ID)
68 | }
69 |
70 | func Test_Announcements_OneByID_NotExist(t *testing.T) {
71 | o, err := db.AnnouncementsOneByID(1337)
72 | assert.Error(t, err)
73 | assert.EqualError(t, err, sql.ErrNoRows.Error())
74 | assert.Nil(t, o)
75 | }
76 |
77 | func Test_Announcements_Delete_Success(t *testing.T) {
78 | setup(t)
79 | defer teardown(t)
80 |
81 | err := db.AnnouncementsDelete(1)
82 | assert.NoError(t, err)
83 |
84 | o, err := db.AnnouncementsOneByID(1)
85 | assert.Error(t, err)
86 | assert.EqualError(t, err, sql.ErrNoRows.Error())
87 | assert.Nil(t, o)
88 | }
89 |
90 | func Test_Announcements_Delete_NotExist(t *testing.T) {
91 | setup(t)
92 | defer teardown(t)
93 |
94 | err := db.AnnouncementsDelete(1337)
95 | assert.Error(t, err)
96 | assert.EqualError(t, err, sql.ErrNoRows.Error())
97 | }
98 |
99 | func Test_Announcements_List_Basic(t *testing.T) {
100 | setup(t)
101 | defer teardown(t)
102 |
103 | list, _, err := db.AnnouncementsList(
104 | AnnouncementsListPagination(Pagination{Count: 10}),
105 | )
106 | assert.NoError(t, err)
107 | assert.Len(t, list, 3)
108 | }
109 |
--------------------------------------------------------------------------------
/controllers/error.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 |
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | // Error represents an error that can be marshaled to json and sended to user.
12 | type Error struct {
13 | Err error `json:"-"`
14 | Code int `json:"-"`
15 | Msg string `json:"error"`
16 | Errs map[string][]string `json:"errors,omitempty"`
17 | }
18 |
19 | // Allow Error to satisfy error interface.
20 | func (e *Error) Error() string {
21 | var s string
22 | for field, err := range e.Errs {
23 | s += fmt.Sprintf("%s: %s;\n", field, err)
24 | }
25 | return s
26 | }
27 |
28 | func (e *Error) SetFieldsErrors(errs map[string][]string) *Error {
29 | e.Errs = errs
30 | return e
31 | }
32 |
33 | func (e *Error) SetError(err error) *Error {
34 | e.Err = err
35 | return e
36 | }
37 |
38 | func (e *Error) SetMessage(msg string) *Error {
39 | e.Msg = msg
40 | return e
41 | }
42 |
43 | func handleError(w http.ResponseWriter, r *http.Request, e *Error) {
44 |
45 | if e.Code == 500 {
46 | log.Error(e.Err)
47 | }
48 |
49 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
50 | w.WriteHeader(e.Code)
51 | if err := json.NewEncoder(w).Encode(e); err != nil {
52 | log.Error(err)
53 | }
54 | }
55 |
56 | func errorFunc(e *Error) func(http.ResponseWriter, *http.Request, error) {
57 | return func(w http.ResponseWriter, r *http.Request, err error) {
58 | handleError(w, r, e.SetError(err))
59 | }
60 | }
61 |
62 | var (
63 | ErrInvalidQueryParams = &Error{Code: 400, Msg: "Invalid query params"}
64 | ErrInvalidJSON = &Error{Code: 400, Msg: "Invalid JSON"}
65 | ErrInvalidID = &Error{Code: 400, Msg: "Invalid ID supplied"}
66 | ErrInvalidContentType = &Error{Code: 400, Msg: "Invalid Content-Type"}
67 | ErrUnauthorizedRequest = &Error{Code: 401, Msg: "Unauthorized request"}
68 | ErrInvalidCreds = &Error{Code: 401, Msg: "Invalid credentials"}
69 | ErrTokenIsExpired = &Error{Code: 403, Msg: "Token is expired"}
70 | ErrAccessDenied = &Error{Code: 403, Msg: "Access denied"}
71 | ErrInvalidCSRFToken = &Error{Code: 403, Msg: "Invalid CSRF token"}
72 | ErrInvalidCaptcha = &Error{Code: 403, Msg: "Invalid captcha"}
73 | ErrCtfNotStarted = &Error{Code: 403, Msg: "CTF hasn't started yet"}
74 | ErrCtfAlreadyEnded = &Error{Code: 403, Msg: "CTF has already ended"}
75 | ErrPathNotFound = &Error{Code: 404, Msg: "Path not found"}
76 | ErrTokenNotFound = &Error{Code: 404, Msg: "Token not found"}
77 | ErrChallengeNotFound = &Error{Code: 404, Msg: "Challenge not found"}
78 | ErrAnnouncementNotFound = &Error{Code: 404, Msg: "Announcement not found"}
79 | ErrUserNotFound = &Error{Code: 404, Msg: "User not found"}
80 | ErrDuplicate = &Error{Code: 409, Msg: "Duplicate entry"}
81 | ErrInvalidFlag = &Error{Code: 418, Msg: "Invalid flag"}
82 | ErrAccountIsNotActivated = &Error{Code: 422, Msg: "Account is not activated"}
83 | ErrInternal = &Error{Code: 500, Msg: "Internal error"}
84 | )
85 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { createForm } from 'rc-form';
4 |
5 | import { FormItem, Button } from '../../../components';
6 |
7 | class LoginForm extends Component {
8 | static propTypes = {
9 | onSubmit: PropTypes.func.isRequired,
10 | form: PropTypes.object.isRequired,
11 | fields: PropTypes.object,
12 | className: PropTypes.string
13 | };
14 |
15 | componentDidMount() {
16 | this.emailInput.focus();
17 | }
18 |
19 | handleSubmit = e => {
20 | e.preventDefault();
21 |
22 | const { getFieldsValue } = this.props.form;
23 | this.props.onSubmit(getFieldsValue());
24 | };
25 |
26 | renderEmailField({ getFieldDecorator, getFieldError }) {
27 | return (
28 |
29 | {getFieldDecorator('email', {
30 | initialValue: '',
31 | rules: [
32 | {
33 | required: true,
34 | message: 'Email is required'
35 | },
36 | {
37 | type: 'email',
38 | message: 'Invalid email'
39 | }
40 | ]
41 | })(
42 | (this.emailInput = el)}
46 | />
47 | )}
48 |
49 | );
50 | }
51 |
52 | renderPaswordField({ getFieldDecorator, getFieldError }) {
53 | return (
54 |
55 | {getFieldDecorator('password', {
56 | initialValue: '',
57 | rules: [
58 | {
59 | required: true,
60 | message: 'Password is required'
61 | },
62 | {
63 | min: 8,
64 | message: 'Password is too short'
65 | }
66 | ]
67 | })()}
68 |
69 | );
70 | }
71 |
72 | renderSubmitButton({ getFieldError, isFieldTouched }) {
73 | const canSubmit = ['email', 'password'].reduce((result, field) => {
74 | return result && isFieldTouched(field) && !getFieldError(field);
75 | }, true);
76 |
77 | return (
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | render() {
85 | const { form, className } = this.props;
86 |
87 | return (
88 |
93 | );
94 | }
95 | }
96 |
97 | const onFieldsChange = (props, changed, all) => {
98 | props.onChange(all);
99 | };
100 |
101 | const mapPropsToFields = ({ fields }) => {
102 | return fields;
103 | };
104 |
105 | export default createForm({ onFieldsChange, mapPropsToFields })(LoginForm);
106 |
--------------------------------------------------------------------------------
/models/scores.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/ctf-zone/ctfzone/config"
8 | )
9 |
10 | type scoresListOptions struct {
11 | Pagination
12 | }
13 |
14 | type scoresOption func(*scoresListOptions)
15 |
16 | func ScoresPagination(p Pagination) scoresOption {
17 | return func(params *scoresListOptions) {
18 | params.Pagination = p
19 | }
20 | }
21 |
22 | // Score represents line in scoreboard.
23 | type Score struct {
24 | UserP `json:"user"`
25 | Score int `db:"score" json:"score"`
26 | Rank int `db:"rank" json:"rank,omitempty"`
27 | UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
28 | }
29 |
30 | type Scores []*Score
31 |
32 | func (l Scores) First() int64 {
33 | if len(l) == 0 {
34 | return 0
35 | }
36 | return l[0].ID
37 | }
38 |
39 | func (l Scores) Last() int64 {
40 | if len(l) == 0 {
41 | return 0
42 | }
43 | return l[len(l)-1].ID
44 | }
45 |
46 | func (l Scores) Len() int {
47 | return len(l)
48 | }
49 |
50 | func scoresQuery(cfg *config.Scoring) string {
51 | inner := "SELECT users.id, users.name, users.extra, MAX(solutions.created_at) AS updated_at, "
52 |
53 | switch cfg.Type {
54 | default:
55 | fallthrough
56 |
57 | case "classic":
58 | // Simply calculate sum of points of solved challenges.
59 | inner += "COALESCE(SUM(challenges.points), 0) AS score "
60 |
61 | case "dynamic":
62 | p := cfg.Dynamic
63 |
64 | // Calculate sum of solved challenges points.
65 | // points = min + (max - min) * coeff ^ (n - 1)
66 | formula := fmt.Sprintf(
67 | "FLOOR(%d + %d * POWER(%.3f, %s - 1))",
68 | p.Min,
69 | p.Max-p.Min,
70 | p.Coeff,
71 | "challenges_solutions.count",
72 | )
73 |
74 | inner += fmt.Sprintf("COALESCE(SUM(%s), 0) AS score ", formula)
75 | }
76 |
77 | inner += "FROM users " +
78 | "LEFT JOIN solutions ON solutions.user_id = users.id " +
79 | "LEFT JOIN challenges ON challenges.id = solutions.challenge_id " +
80 | "LEFT JOIN challenges_solutions ON challenges_solutions.challenge_id = challenges.id " +
81 | "GROUP BY users.id, users.name, users.extra " +
82 | "ORDER BY score DESC, updated_at, name"
83 |
84 | query := fmt.Sprintf("SELECT *, ROW_NUMBER() OVER() AS rank FROM (%s) AS s", inner)
85 |
86 | return query
87 | }
88 |
89 | func (r *Repository) ScoresList(cfg *config.Scoring, options ...scoresOption) ([]*Score, *PagesInfo, error) {
90 | list := make(Scores, 0)
91 |
92 | var params scoresListOptions
93 | for _, opt := range options {
94 | opt(¶ms)
95 | }
96 |
97 | err := r.db.Select(&list, scoresQuery(cfg))
98 | if err != nil {
99 | return nil, nil, err
100 | }
101 |
102 | return list, nil, nil
103 | }
104 |
105 | func (r *Repository) ScoresOneByUserID(cfg *config.Scoring, userID int64) (*Score, error) {
106 | var o Score
107 |
108 | query := fmt.Sprintf("SELECT * FROM (%s) AS scores WHERE id = $1", scoresQuery(cfg))
109 |
110 | err := r.db.Get(&o, query, userID)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | return &o, nil
116 | }
117 |
--------------------------------------------------------------------------------
/web/admin/src/images/logo-big-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/public/src/scenes/scenes/challenges/Challenges.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from 'react-redux';
4 | import { Link } from 'react-router-dom';
5 | import classNames from 'classnames';
6 |
7 | import { Page, Divider } from '../../../components';
8 |
9 | class Challenges extends Component {
10 | static propTypes = {
11 | challenges: PropTypes.object,
12 | challengesList: PropTypes.func
13 | };
14 |
15 | state = {
16 | intervalId: 0
17 | };
18 |
19 | componentDidMount() {
20 | this.fetchData();
21 |
22 | const intervalId = setInterval(this.fetchData, 30000);
23 | this.setState({ intervalId });
24 | }
25 |
26 | componentWillUnmount() {
27 | clearInterval(this.state.intervalId);
28 | }
29 |
30 | fetchData = () => {
31 | const { challengesList } = this.props;
32 | challengesList();
33 | };
34 |
35 | renderChallenge({ challenge, user, meta }, index) {
36 | const prefixClass = 'ctf-challenge-list-item';
37 |
38 | const className = classNames({
39 | [prefixClass]: true,
40 | [prefixClass + '--solved']: user.isSolved
41 | });
42 |
43 | return (
44 |
45 |
46 |
47 |
{challenge.title}
48 |
53 | {challenge.difficulty}
54 |
55 |
56 | {meta.solutionsCount}
57 |
58 |
{challenge.points}
59 |
60 | {challenge.categories.map((category, i) => {
61 | return (
62 |
63 | {category}
64 |
65 | );
66 | })}
67 |
68 | {meta.hintsCount > 0 ? (
69 |
70 | ) : (
71 | ''
72 | )}
73 |
74 |
75 |
76 | );
77 | }
78 |
79 | render() {
80 | const { items: challenges } = this.props.challenges;
81 |
82 | return (
83 |
84 | {Object.values(challenges).map(this.renderChallenge)}
85 |
86 | );
87 | }
88 | }
89 |
90 | const mapStateToProps = state => ({
91 | challenges: state.challenges,
92 | effects: state.api.effects.challenges.list
93 | });
94 |
95 | const mapDispatchToProps = dispatch => ({
96 | challengesList: dispatch.challenges.list
97 | });
98 |
99 | export default connect(
100 | mapStateToProps,
101 | mapDispatchToProps
102 | )(Challenges);
103 |
--------------------------------------------------------------------------------
/web/public/src/utils/rematch-api.js:
--------------------------------------------------------------------------------
1 | import { ApiError } from './api';
2 |
3 | const createReducer = initial => (state, { name, action, payload = {} }) => {
4 | const mixin = { ...initial, ...payload };
5 |
6 | return {
7 | ...state,
8 | global: { ...state.global, ...mixin },
9 | models: {
10 | ...state.models,
11 | [name]: { ...state.models[name], ...mixin }
12 | },
13 | effects: {
14 | ...state.effects,
15 | [name]: {
16 | ...state.effects[name],
17 | [action]: { ...state.effects[name][action], ...mixin }
18 | }
19 | }
20 | };
21 | };
22 |
23 | const initialState = {
24 | success: false,
25 | error: null,
26 | loading: false,
27 | time: new Date()
28 | };
29 |
30 | const api = {
31 | state: {
32 | global: { ...initialState },
33 | models: {},
34 | effects: {}
35 | },
36 | reducers: {
37 | success: createReducer({ success: true }),
38 | failure: createReducer({ success: false }),
39 | start: createReducer({ loading: true }),
40 | stop: createReducer({ loading: false })
41 | }
42 | };
43 |
44 | export default ({ blacklist = ['api'], whitelist = [] }) => ({
45 | config: {
46 | models: {
47 | api
48 | }
49 | },
50 | onModel({ name }) {
51 | if (whitelist.length) {
52 | if (!whitelist.includes(name)) {
53 | return;
54 | }
55 | } else {
56 | if (blacklist.includes(name)) {
57 | return;
58 | }
59 | }
60 |
61 | // Set initial state
62 | api.state.models[name] = { ...initialState };
63 | api.state.effects[name] = {};
64 |
65 | Object.keys(this.dispatch[name]).forEach(action => {
66 | if (!this.dispatch[name][action].isEffect) {
67 | return;
68 | }
69 |
70 | api.state.effects[name][action] = { ...initialState };
71 |
72 | const originalEffect = this.dispatch[name][action];
73 |
74 | let wrapperEffect = async (...args) => {
75 | let effectResult;
76 |
77 | try {
78 | this.dispatch.api.start({ name, action });
79 |
80 | effectResult = await originalEffect(...args);
81 |
82 | this.dispatch.api.success({
83 | name,
84 | action,
85 | payload: { error: null }
86 | });
87 | } catch (err) {
88 | this.dispatch.api.failure({
89 | name,
90 | action,
91 | payload: { error: err }
92 | });
93 |
94 | if (args.length > 1 && 'throw' in args[args.length - 1]) {
95 | const { status, data } = err.response;
96 | const { error, errors } = data;
97 | throw new ApiError(status, error, errors);
98 | }
99 | } finally {
100 | this.dispatch.api.stop({
101 | name,
102 | action,
103 | payload: { time: new Date() }
104 | });
105 | }
106 |
107 | return effectResult;
108 | };
109 |
110 | wrapperEffect.isEffect = true;
111 |
112 | this.dispatch[name][action] = wrapperEffect;
113 | });
114 | }
115 | });
116 |
--------------------------------------------------------------------------------
/web/admin/src/utils/rematch-api.js:
--------------------------------------------------------------------------------
1 | import { ApiError } from '../utils/api'
2 |
3 | const createReducer = (initial) => (state, { name, action, payload = {} }) => {
4 | const mixin = { ...initial, ...payload }
5 |
6 | return {
7 | ...state,
8 | global: { ...state.global, ...mixin },
9 | models: {
10 | ...state.models,
11 | [name]: { ...state.models[name], ...mixin },
12 | },
13 | effects: {
14 | ...state.effects,
15 | [name]: {
16 | ...state.effects[name],
17 | [action]: { ...state.effects[name][action], ...mixin },
18 | },
19 | },
20 | }
21 | }
22 |
23 | const initialState = {
24 | success: false,
25 | error: null,
26 | loading: false,
27 | time: new Date(),
28 | }
29 |
30 | const api = {
31 | state: {
32 | global: { ...initialState },
33 | models: {},
34 | effects: {},
35 | },
36 | reducers: {
37 | success: createReducer({ success: true }),
38 | failure: createReducer({ success: false }),
39 | start: createReducer({ loading: true }),
40 | stop: createReducer({ loading: false }),
41 | },
42 | }
43 |
44 | export default ({ blacklist = [ 'api' ], whitelist = [] }) => ({
45 | config: {
46 | models: {
47 | api,
48 | },
49 | },
50 | onModel({ name }) {
51 |
52 | if (whitelist.length) {
53 | if (!whitelist.includes(name)) {
54 | return
55 | }
56 | } else {
57 | if (blacklist.includes(name)) {
58 | return
59 | }
60 | }
61 |
62 | // Set initial state
63 | api.state.models[name] = { ...initialState }
64 | api.state.effects[name] = {}
65 |
66 | Object.keys(this.dispatch[name]).forEach((action) => {
67 |
68 | if (!this.dispatch[name][action].isEffect) {
69 | return
70 | }
71 |
72 | api.state.effects[name][action] = { ...initialState }
73 |
74 | const originalEffect = this.dispatch[name][action]
75 |
76 | let wrapperEffect = async(...args) => {
77 | let effectResult
78 |
79 | try {
80 | this.dispatch.api.start({ name, action })
81 |
82 | effectResult = await originalEffect(...args)
83 |
84 | this.dispatch.api.success({
85 | name,
86 | action,
87 | payload: { error: null },
88 | })
89 | } catch (err) {
90 | this.dispatch.api.failure({
91 | name,
92 | action,
93 | payload: { error: err },
94 | })
95 |
96 | if (args.length > 1 && ('throw' in args[args.length - 1])) {
97 | const { status, data } = err.response
98 | const { error, errors } = data
99 | throw new ApiError(status, error, errors)
100 | }
101 | } finally {
102 | this.dispatch.api.stop({
103 | name,
104 | action,
105 | payload: { time: new Date() },
106 | })
107 | }
108 |
109 | return effectResult
110 | }
111 |
112 | wrapperEffect.isEffect = true
113 |
114 | this.dispatch[name][action] = wrapperEffect
115 | })
116 | },
117 | })
118 |
--------------------------------------------------------------------------------