├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── gulpfile.js ├── icons ├── app-icon.icns ├── app-icon.ico └── app-icon.png ├── index.html ├── lepus-ctf.desktop ├── package.json ├── res ├── alternative-icon.png └── lepus.svg └── src ├── api ├── api.jsx └── error-handler.jsx ├── data └── store.jsx ├── index.jsx └── pages ├── announcements.jsx ├── dashboard.jsx ├── login.jsx ├── main.jsx ├── problem.jsx ├── problems.jsx ├── ranking.jsx └── top.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | js 3 | font 4 | release 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuki MIZUNO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lepus-CTF frontend application 2 | 3 | CTF frontent application made with Electron 4 | 5 | ## Usage 6 | 7 | ### Install dependened modules 8 | 9 | ``` 10 | $ npm install 11 | ``` 12 | 13 | ### Build and run 14 | 15 | ``` 16 | $ npm run compile 17 | $ npm start 18 | ``` 19 | 20 | > NOTE: If you want to set a specific score-server url, use `--host=` option like 21 | > `npm run compile -- --host=https://score.example.ctf` 22 | 23 | ### Packaging 24 | ``` 25 | $ npm run package:all 26 | ``` 27 | 28 | 29 | ## License 30 | MIT 31 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('app'); 4 | var BrowserWindow = require('browser-window'); 5 | var Menu = require('menu'); 6 | 7 | require('crash-reporter').start(); 8 | 9 | var mainWindow = null; 10 | 11 | app.on('window-all-closed', function() { 12 | if (process.platform != 'darwin') 13 | app.quit(); 14 | }); 15 | 16 | app.on('ready', function() { 17 | 18 | mainWindow = new BrowserWindow({width: 1024, height: 600}); 19 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 20 | 21 | mainWindow.on('closed', function() { 22 | mainWindow = null; 23 | }); 24 | 25 | var menu = Menu.buildFromTemplate([ 26 | { 27 | label: 'Lepus-CTF', 28 | submenu: [ 29 | { 30 | label: 'Settings', 31 | accelerator: 'CmdOrCtrl+,', 32 | }, 33 | { 34 | type: 'separator' 35 | }, 36 | { 37 | label: 'Quit', 38 | accelerator: 'CmdOrCtrl+Q', 39 | click: function () { 40 | app.quit(); 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | label: 'View', 47 | submenu: [ 48 | { 49 | label: 'Toggle Full Screen', 50 | accelerator: 'CmdOrCtrl+Shift+F', 51 | click: function() { 52 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 53 | } 54 | }, 55 | { 56 | label: 'Toggle developer tool', 57 | accelerator: 'CmdOrCtrl+Alt+I', 58 | click: function() { 59 | mainWindow.toggleDevTools(); 60 | } 61 | } 62 | ] 63 | }, 64 | { 65 | label: 'Help', 66 | submenu: [ 67 | {label: 'About'}, 68 | {label: 'Contact to admin'}, 69 | ] 70 | } 71 | ]); 72 | Menu.setApplicationMenu(menu); 73 | }); 74 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var $ = require('gulp-load-plugins')(); 3 | var packager = require('electron-packager'); 4 | 5 | gulp.task('download:font', function (done) { 6 | var urls = [ 7 | "http://mplus-fonts.osdn.jp/webfonts/mplus-2m-regular.ttf", 8 | "http://mplus-fonts.osdn.jp/webfonts/mplus-2p-regular.ttf", 9 | ]; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var dirname = "font" 14 | 15 | if (fs.existsSync(dirname)) { 16 | // Already exists. Skip downloading font. 17 | return done(); 18 | } 19 | fs.mkdirSync(dirname); 20 | 21 | console.log('downloading web font.'); 22 | 23 | process.on('download', function(i) { 24 | var url = urls[i]; 25 | if (!url) { 26 | return done(); 27 | } 28 | console.log(url); 29 | var http = require(url.split(':')[0]); 30 | http.get(url, function(res) { 31 | var filename = path.join(dirname, path.basename(url)); 32 | var output = fs.createWriteStream(filename); 33 | res.pipe(output); 34 | res.on('end', function() { 35 | process.nextTick(function() { 36 | process.emit('download', ++i); 37 | }) 38 | }); 39 | }).on('error', function(err) { 40 | console.log('error', err); 41 | }); 42 | }) 43 | process.emit('download', 0); 44 | }); 45 | 46 | gulp.task('compile', function(){ 47 | var host = process.argv[3] ? process.argv[3].split('--host=').pop() : ''; 48 | if (!host) { 49 | host = "https://score.sakura.tductf.org"; 50 | } 51 | return gulp.src('src/**/*.{js,jsx}') 52 | .pipe($.replace('SCORE_SERVER_URL', host)) 53 | .pipe( 54 | $.babel({ 55 | stage: 0 56 | }) 57 | ) 58 | .pipe(gulp.dest('js')); 59 | }); 60 | 61 | var commonOption = { 62 | dir: './', 63 | out: 'release', 64 | name: 'Lepus-CTF', 65 | arch: 'all', 66 | platform: 'all', 67 | asar: true, 68 | ignore: [ 69 | './node_modules/electron*', 70 | './node_modules/.bin', 71 | './release/', 72 | './src/', 73 | './.git*' 74 | ], 75 | version: '0.30.6' 76 | } 77 | 78 | gulp.task('package:darwin', ['compile'], function (done) { 79 | var option = commonOption; 80 | option.platform = 'darwin'; 81 | option.arch = 'x64'; 82 | packager(option, function (err, path) { 83 | done(); 84 | }); 85 | }); 86 | 87 | gulp.task('package:linux:ia32', ['compile'], function (done) { 88 | var option = commonOption; 89 | option.platform = 'linux'; 90 | option.arch = 'ia32'; 91 | packager(option, function (err, path) { 92 | done(); 93 | }); 94 | }); 95 | 96 | gulp.task('package:linux:x64', ['compile'], function (done) { 97 | var option = commonOption; 98 | option.platform = 'linux'; 99 | option.arch = 'x64'; 100 | packager(option, function (err, path) { 101 | done(); 102 | }); 103 | }); 104 | 105 | gulp.task('package:win32:ia32', ['compile'], function (done) { 106 | var option = commonOption; 107 | option.platform = 'win32'; 108 | option.arch = 'ia32'; 109 | packager(option, function (err, path) { 110 | done(); 111 | }); 112 | }); 113 | 114 | gulp.task('package:win32:x64', ['compile'], function (done) { 115 | var option = commonOption; 116 | option.platform = 'win32'; 117 | option.arch = 'x64'; 118 | packager(option, function (err, path) { 119 | done(); 120 | }); 121 | }); 122 | 123 | gulp.task('package:all', [ 124 | 'package:darwin', 125 | 'package:linux:ia32', 126 | 'package:linux:x64', 127 | 'package:win32:ia32', 128 | 'package:win32:x64'], function (done) { 129 | done(); 130 | }); 131 | 132 | -------------------------------------------------------------------------------- /icons/app-icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.icns -------------------------------------------------------------------------------- /icons/app-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.ico -------------------------------------------------------------------------------- /icons/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lepus-CTF 6 | 7 | 53 | 54 | 55 | 56 | 57 | 58 | 125 | 126 | -------------------------------------------------------------------------------- /lepus-ctf.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Lepus-CTF 3 | Comment=Lepus-CTF frontend application 4 | Exec=Lepus-CTF 5 | Icon=app-icon.png 6 | Type=Application 7 | Terminal=false 8 | Categories=Game; 9 | Keywords=CTF; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lepus-frontend", 3 | "version": "0.1.0", 4 | "description": "Lepus-CTF front-end application", 5 | "main": "app.js", 6 | "desktopName": "lepus-ctf.desktop", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "compile": "gulp compile", 10 | "start": "electron .", 11 | "package:darwin": "gulp package:darwin", 12 | "package:linux:x64": "gulp package:linux:x64", 13 | "package:linux:ia32": "gulp package:linux:ia32", 14 | "package:win32:x64": "gulp package:win32:x64", 15 | "package:win32:ia32": "gulp package:win32:ia32", 16 | "package:all": "gulp package:all", 17 | "postinstall": "gulp download:font" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/lepus-ctf/lepus-frontend.git" 22 | }, 23 | "keywords": [ 24 | "lepus-ctf", 25 | "ctf", 26 | "electron" 27 | ], 28 | "author": "mzyy94", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/lepus-ctf/lepus-frontend/issues" 32 | }, 33 | "homepage": "https://github.com/lepus-ctf/lepus-frontend", 34 | "devDependencies": { 35 | "electron-packager": "^5.0.2", 36 | "electron-prebuilt": "^0.30.6", 37 | "gulp-babel": "^5.2.0", 38 | "gulp-load-plugins": "^1.0.0-rc.1" 39 | }, 40 | "dependencies": { 41 | "better-console": "^0.2.4", 42 | "del": "^1.2.1", 43 | "extend": "^3.0.0", 44 | "gulp": "^3.9.0", 45 | "gulp-autoprefixer": "^2.3.1", 46 | "gulp-chmod": "^1.2.0", 47 | "gulp-clone": "^1.0.0", 48 | "gulp-concat": "^2.6.0", 49 | "gulp-concat-css": "^2.2.0", 50 | "gulp-copy": "0.0.2", 51 | "gulp-flatten": "^0.1.1", 52 | "gulp-header": "^1.2.2", 53 | "gulp-help": "^1.6.0", 54 | "gulp-if": "^1.2.5", 55 | "gulp-less": "^3.0.3", 56 | "gulp-minify-css": "^1.2.0", 57 | "gulp-notify": "^2.2.0", 58 | "gulp-plumber": "^1.0.1", 59 | "gulp-print": "^1.1.0", 60 | "gulp-rename": "^1.2.2", 61 | "gulp-replace": "^0.5.4", 62 | "gulp-rtlcss": "^0.1.4", 63 | "gulp-uglify": "^1.2.0", 64 | "gulp-util": "^3.0.6", 65 | "gulp-watch": "^4.3.5", 66 | "map-stream": "0.0.6", 67 | "md2react": "^0.8.8", 68 | "object-assign": "^4.0.0", 69 | "particles.js": "^2.0.0", 70 | "qs": "^4.0.0", 71 | "react": "^0.13.3", 72 | "react-redux": "^1.0.1", 73 | "react-router": "^0.13.3", 74 | "redux": "^1.0.1", 75 | "require-dot-file": "^0.4.0", 76 | "semantic-ui-css": "^2.0.8", 77 | "socket.io-client": "^1.3.6", 78 | "superagent": "^1.3.0", 79 | "yamljs": "^0.2.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /res/alternative-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/res/alternative-icon.png -------------------------------------------------------------------------------- /res/lepus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | image/svg+xml 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/api/api.jsx: -------------------------------------------------------------------------------- 1 | import ErrorHandler from './error-handler' 2 | export class Api { 3 | constructor() { 4 | this.superagent = require('superagent'); 5 | this.serverUrl = "SCORE_SERVER_URL"; 6 | this.apiEndpoint = this.serverUrl + "/api" 7 | this.token = ""; 8 | this.errorHandler = new ErrorHandler(); 9 | 10 | var https = require('https'); 11 | var _addRequest = https.Agent.prototype.addRequest; 12 | var self = this; 13 | https.Agent.prototype.addRequest = function() { 14 | var args = arguments; 15 | var cookies = self.agent.jar.getCookies(args[0]).map((cookie) => cookie.toValueString()).join('; ');; 16 | var old = args[0]._headers.cookie; 17 | args[0]._headers.cookie = cookies + (old ? '; ' + old : ''); 18 | args[0]._headerNames['cookie'] = 'Cookie'; 19 | return _addRequest.apply(this, args); 20 | }; 21 | this.https = https; 22 | } 23 | setCriticalAction(func) { 24 | this.errorHandler.criticalAction = func || (() => {}); 25 | } 26 | login(username, password, success, failure) { 27 | this.agent = this.superagent.agent(); 28 | this.agent 29 | .post(this.apiEndpoint + '/auth.json') 30 | .send({username: username, password: password}) 31 | .end((err, res) => { 32 | if (err || username != res.body.username) { 33 | const error = this.errorHandler.parseError(err, res); 34 | failure(error); 35 | } else { 36 | this.agent.saveCookies(res); 37 | this.token = this.agent.jar.getCookie("csrftoken", res.req).value; 38 | success(res.body); 39 | } 40 | }); 41 | } 42 | 43 | signup(username, password, success, failure) { 44 | this.agent 45 | .post(this.apiEndpoint + '/users.json') 46 | .send({username: username, password: password, team_name: username, team_password: password}) 47 | .end((err, res) => { 48 | if (err) { 49 | const error = this.errorHandler.parseError(err, res); 50 | failure(error); 51 | } else { 52 | success(); 53 | } 54 | }); 55 | } 56 | 57 | configurations(success, failure) { 58 | this.agent 59 | .get(this.apiEndpoint + '/configurations.json') 60 | .end((err, res) => { 61 | if (err) { 62 | const error = this.errorHandler.parseError(err, res); 63 | failure(error); 64 | } else { 65 | this.agent.saveCookies(res); 66 | success(res.body); 67 | } 68 | }); 69 | } 70 | 71 | problems(success, failure) { 72 | this.agent 73 | .get(this.apiEndpoint + '/questions.json') 74 | .query({include: '1'}) 75 | .end((err, res) => { 76 | if (err) { 77 | const error = this.errorHandler.parseError(err, res); 78 | failure(error); 79 | } else { 80 | this.agent.saveCookies(res); 81 | success(res.body); 82 | } 83 | }); 84 | } 85 | 86 | problem(id, success, failure) { 87 | this.agent 88 | .get(this.apiEndpoint + '/questions/' + id + '.json') 89 | .query({include: '1'}) 90 | .end((err, res) => { 91 | if (err) { 92 | const error = this.errorHandler.parseError(err, res); 93 | failure(error); 94 | } else { 95 | this.agent.saveCookies(res); 96 | success(res.body); 97 | } 98 | }); 99 | } 100 | 101 | announcement(id, success, failure) { 102 | this.agent 103 | .get(this.apiEndpoint + '/notices/' + id + '.json') 104 | .end((err, res) => { 105 | if (err) { 106 | const error = this.errorHandler.parseError(err, res); 107 | failure(error); 108 | } else { 109 | this.agent.saveCookies(res); 110 | success(res.body); 111 | } 112 | }); 113 | } 114 | 115 | announcements(success, failure) { 116 | this.agent 117 | .get(this.apiEndpoint + '/notices.json') 118 | .end((err, res) => { 119 | if (err) { 120 | const error = this.errorHandler.parseError(err, res); 121 | failure(error); 122 | } else { 123 | this.agent.saveCookies(res); 124 | success(res.body); 125 | } 126 | }); 127 | } 128 | 129 | submitFlag(id, flag, success, failure) { 130 | this.agent 131 | .post(this.apiEndpoint + '/answers.json') 132 | .set('X-CSRFToken', this.token) 133 | .send({question: id, answer: flag}) 134 | .end((err, res) => { 135 | if (err) { 136 | const error = this.errorHandler.parseError(err, res); 137 | failure(error); 138 | } else if (res.body.is_correct) { 139 | this.agent.saveCookies(res); 140 | success(); 141 | } else { 142 | console.error(err); 143 | console.error(res); 144 | failure(err, res); 145 | } 146 | }); 147 | } 148 | 149 | teamlist(success, failure) { 150 | this.agent 151 | .get(this.apiEndpoint + '/teams.json') 152 | .set('X-CSRFToken', this.token) 153 | .end((err, res) => { 154 | if (err) { 155 | const error = this.errorHandler.parseError(err, res); 156 | failure(error); 157 | } else { 158 | success(res.body); 159 | } 160 | }); 161 | } 162 | 163 | team(id, success, failure) { 164 | this.agent 165 | .get(this.apiEndpoint + '/teams/' + id + '.json') 166 | .set('X-CSRFToken', this.token) 167 | .end((err, res) => { 168 | if (err) { 169 | const error = this.errorHandler.parseError(err, res); 170 | failure(error); 171 | } else { 172 | success(res.body); 173 | } 174 | }); 175 | } 176 | 177 | downloadFile(filepath, savepath, success, failure) { 178 | this.https 179 | .get(this.serverUrl + filepath, (res) => { 180 | if (res.statusCode >= 200 && res.statusCode < 300) { 181 | var fs = require('fs'); 182 | var stream = fs.createWriteStream(savepath); 183 | stream.on('error', (err) => { 184 | const error = this.errorHandler.parseError(err, null); 185 | failure(error); 186 | }); 187 | res.pipe(stream); 188 | stream.on('finish', () => { 189 | stream.close((err) => { 190 | if (err) { 191 | const error = this.errorHandler.parseError(err, null); 192 | failure(error); 193 | } else { 194 | success(); 195 | } 196 | }); 197 | }) 198 | } else { 199 | const error = this.errorHandler.parseError(res.statusMessage, res); 200 | failure(error); 201 | } 202 | }).on('error', (err) => { 203 | console.log(err); 204 | const error = this.errorHandler.parseError(err, null); 205 | failure(error); 206 | }); 207 | } 208 | 209 | }; 210 | export default new Api(); 211 | -------------------------------------------------------------------------------- /src/api/error-handler.jsx: -------------------------------------------------------------------------------- 1 | export default class ErrorHandler { 2 | constructor() { 3 | this.criticalAction = (() => {}); 4 | this.errorCodes = [ 5 | "REQUIRED", 6 | "TOO_LONG", 7 | "TOO_SHORT", 8 | "TOO_SMALL", 9 | "TOO_BIG", 10 | "INVALID", 11 | "DUPLICATED", 12 | "NOT_FOUND", 13 | "NUMERIC_IS_REQUIRED", 14 | "UNAUTHORIZED", 15 | "PERMISSION_DENIED", 16 | "INVALID_CREDENTIALS", 17 | "INCORRECT_ANSWER", 18 | "ALREADY_SUBMITTED", 19 | "MAX_FAILUER", 20 | "MAX_ANSWERS", 21 | "NOT_STARTED", 22 | "CLOSED", 23 | ]; 24 | this.errorMessagesJP = { 25 | "REQUIRED": "入力が必須です。", 26 | "TOO_LONG": "入力文字が長過ぎます。", 27 | "TOO_SHORT": "入力文字が短すぎます。", 28 | "TOO_SMALL": "入力値が小さすぎます。", 29 | "TOO_BIG": "入力値が大きすぎます。", 30 | "INVALID": "入力値が不正です。", 31 | "DUPLICATED": "入力値はすでに存在しています。", 32 | "NOT_FOUND": "項目が見つかりません。", 33 | "NUMERIC_IS_REQUIRED": "数値で入力する必要があります。", 34 | "UNAUTHORIZED": "アクセス認証がされていません。", 35 | "PERMISSION_DENIED": "あなたにはアクセス権限がありません。", 36 | "INVALID_CREDENTIALS": "認証情報が不正です。", 37 | "INCORRECT_ANSWER": "回答が間違っています。", 38 | "ALREADY_SUBMITTED": "すでに回答しています", 39 | "MAX_FAILUER": "回答回数制限に達したため、これ以上回答できません。", 40 | "MAX_ANSWERS": "正答者数が上限に達したため、回答を受け付けられません。", 41 | "NOT_STARTED": "まだ開始していません。", 42 | "CLOSED": "終了しました。", 43 | }; 44 | } 45 | 46 | parseError(err, res) { 47 | try { 48 | console.error("Path:" + res.req.path); 49 | } catch(e) { 50 | console.error("Client side error"); 51 | } 52 | console.error(err); 53 | var errors; 54 | if (!!res && res.text) { 55 | try { 56 | var json = JSON.parse(res.text); 57 | errors = json.errors; 58 | console.error("REST error: " + json.message); 59 | } catch(e) { 60 | console.error("Error object parsing failed."); 61 | } 62 | } else { 63 | console.error("Can't parse error object."); 64 | console.error("Unknown error.") 65 | } 66 | 67 | var messages = []; 68 | if (errors && errors.length > 0) { 69 | errors.forEach((error) => { 70 | console.log(JSON.stringify(error)); 71 | const code = error.error.toUpperCase(); 72 | if (this.errorCodes.indexOf(code) === -1) { 73 | console.error("Unlisted error!"); 74 | messages.push(code); 75 | } else { 76 | if (error.field) { 77 | messages.push(error.field + 'の' + this.errorMessagesJP[code]); 78 | } else { 79 | messages.push(this.errorMessagesJP[code]); 80 | } 81 | } 82 | }.bind(this)); 83 | } else { 84 | try { 85 | messages.push(err.toString()); 86 | } catch(e) { 87 | messages.push("REALLY_UNKNOWN_ERROR"); 88 | } 89 | } 90 | if (!!res && res.status === 403) { 91 | this.criticalAction(); 92 | } 93 | console.log(JSON.stringify(messages)); 94 | return messages; 95 | } 96 | 97 | }; 98 | -------------------------------------------------------------------------------- /src/data/store.jsx: -------------------------------------------------------------------------------- 1 | import {createStore} from 'redux'; 2 | 3 | export const UPDATE_USERINFO = 'updateUserinfo'; 4 | export const UPDATE_TEAMINFO = 'updateTeaminfo'; 5 | export const UPDATE_PROBLEMS = 'updateProblems'; 6 | export const UPDATE_A_PROBLEM = 'updateAProblem'; 7 | export const UPDATE_TEAMLIST = 'updateTeamlist'; 8 | export const UPDATE_ANNOUNCEMENTS = 'updateAnnouncements'; 9 | export const UPDATE_SERVEREVENT = 'updateServerEvent'; 10 | export const UPDATE_CTFCONF = 'updateCTFConfigurations'; 11 | export const UPDATE_COUNTDOWN = 'updateCountdown'; 12 | export const SET_VISIBLE_CATEGORY = 'setVisibleCategory'; 13 | export const SET_VISIBLE_LEVEL = 'setVisibleLevel'; 14 | export const SET_HIDDEN_SOLVED = 'setHiddenSolved'; 15 | export const RESET_EVENTS = 'resetEvents'; 16 | export const EE = 'kkjjhlhlba'; 17 | 18 | const initialState = { 19 | point: 0, 20 | solved: 0, 21 | ranking: 0, 22 | easteregg: false, 23 | hiddenSolved: false, 24 | visibleCategory: -1, 25 | visibleLevel: -1, 26 | userInfo: {}, 27 | teamInfo: {}, 28 | problems: {}, 29 | solvedTeams: {}, 30 | teamList: [], 31 | announcements: [], 32 | config: {}, 33 | countdown: {}, 34 | events: {} 35 | }; 36 | 37 | const dataStore = (state=initialState, action) => { 38 | switch(action.type){ 39 | case UPDATE_USERINFO: 40 | state["userInfo"] = action.data; 41 | return state; 42 | case UPDATE_TEAMINFO: 43 | var solved = 0; 44 | action.data.questions.forEach((question) => { 45 | solved += question.flags; 46 | }); 47 | state["teamInfo"] = action.data; 48 | state["point"] = action.data.points; 49 | state["solved"] = solved; 50 | return state; 51 | case UPDATE_PROBLEMS: 52 | if (action.data.length) { 53 | action.data.forEach((problem) => { 54 | state["problems"][problem.id] = problem; 55 | }) 56 | } 57 | return state; 58 | case UPDATE_A_PROBLEM: 59 | if (action.data.id != undefined) { 60 | state["problems"][action.data.id] = action.data; 61 | } 62 | return state; 63 | case UPDATE_TEAMLIST: 64 | const teamlist = action.data.sort((current, next) => { 65 | if (current.points < next.points) 66 | return 1; 67 | if (current.points > next.points) 68 | return -1; 69 | if (new Date(current.last_score_time) > new Date(next.last_score_time)) 70 | return 1; 71 | return -1; 72 | }) 73 | state["teamList"] = teamlist; 74 | state["solvedTeams"] = {}; 75 | teamlist.forEach((team, index) => { 76 | if (team.id == state["userInfo"].team) { 77 | var solved = 0; 78 | team.questions.forEach((question) => { 79 | solved += question.flags; 80 | }); 81 | state["teamInfo"] = team; 82 | state["point"] = team.points; 83 | state["solved"] = solved; 84 | state["ranking"] = index + 1; 85 | } 86 | team.questions.forEach((question) => { 87 | state["solvedTeams"][question.id] = ~~state["solvedTeams"][question.id] + 1; 88 | }); 89 | }); 90 | return state; 91 | case UPDATE_ANNOUNCEMENTS: 92 | const announcements = action.data.sort((current, next) => { 93 | const day_current = new Date(current["updated_at"]); 94 | const day_next = new Date(next["updated_at"]); 95 | if (day_current < day_next) 96 | return 1; 97 | if (day_current > day_next) 98 | return -1; 99 | return 0; 100 | }) 101 | state["announcements"] = announcements; 102 | return state; 103 | case UPDATE_SERVEREVENT: 104 | switch (action.data.type) { 105 | case "update": 106 | switch (action.data.model) { 107 | case "notice": 108 | state["events"]["announcements"] = ~~state["events"]["announcements"] + 1; 109 | break; 110 | case "question": 111 | state["events"]["problems"] = ~~state["events"]["problems"] + 1; 112 | break; 113 | default: 114 | console.log(JSON.stringify(action.data)); 115 | } 116 | break; 117 | default: 118 | console.log(JSON.stringify(action.data)); 119 | } 120 | return state; 121 | case UPDATE_CTFCONF: 122 | action.data.forEach((config) => { 123 | switch (config.id) { 124 | case "start_at": 125 | state["config"]["start"] = new Date(config.value); 126 | break; 127 | case "end_at": 128 | state["config"]["end"] = new Date(config.value); 129 | break; 130 | } 131 | }); 132 | return state; 133 | case UPDATE_COUNTDOWN: 134 | state["countdown"] = action.data; 135 | return state; 136 | case SET_VISIBLE_CATEGORY: 137 | state["visibleLevel"] = -1; 138 | state["visibleCategory"] = action.data; 139 | return state; 140 | case SET_VISIBLE_LEVEL: 141 | state["visibleCategory"] = -1; 142 | state["visibleLevel"] = action.data; 143 | return state; 144 | case SET_HIDDEN_SOLVED: 145 | state["hiddenSolved"] = action.data; 146 | return state; 147 | case RESET_EVENTS: 148 | switch (action.data) { 149 | case "problems": 150 | state["events"]["problems"] = 0; 151 | break; 152 | case "announcements": 153 | state["events"]["announcements"] = 0; 154 | break; 155 | default: 156 | console.log(JSON.stringify(action.data)); 157 | } 158 | return state; 159 | case EE: 160 | state["easteregg"] = action.data; 161 | return state; 162 | default: 163 | console.log(JSON.stringify(action)); 164 | return state; 165 | } 166 | } 167 | 168 | export default createStore(dataStore); 169 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Router from 'react-router'; 3 | import {Provider} from 'react-redux'; 4 | import Store from './js/data/store'; 5 | import Login from './js/pages/login'; 6 | import Dashboard from './js/pages/dashboard'; 7 | import Problems from './js/pages/problems'; 8 | import Problem from './js/pages/problem'; 9 | import Ranking from './js/pages/ranking'; 10 | import Announcements from './js/pages/announcements'; 11 | import Main from './js/pages/main'; 12 | import {Top} from './js/pages/top'; 13 | var remote = require('remote'); 14 | var clipboard = remote.require('clipboard'); 15 | var DefaultRoute = Router.DefaultRoute; 16 | var Link = Router.Link; 17 | var Route = Router.Route; 18 | 19 | var routes = ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | 35 | // Patch for Electron on OS X Copy/Paste bug 36 | function CopyPasteFunc(e){ 37 | // Command key presses 38 | if (!e.ctrlKey && e.metaKey && !e.altKey && !e.shiftKey) { 39 | if (e.keyCode === 67) { 40 | // and key 'C' pressed 41 | var selectedText = window.getSelection().toString(); 42 | if (selectedText) { 43 | clipboard.writeText(selectedText); 44 | } 45 | } else if (e.keyCode === 86){ 46 | // and key 'V' pressed 47 | var activeElement = document.activeElement; 48 | var clipboardText = clipboard.readText(); 49 | if (clipboardText && activeElement && activeElement.type == "text") { 50 | var currentText = activeElement.value; 51 | var cursorPosition = activeElement.selectionStart; 52 | if (cursorPosition != activeElement.selectionEnd) { 53 | currentText = currentText.slice(0, cursorPosition) + currentText.slice(activeElement.selectionEnd); 54 | } 55 | activeElement.value = currentText.slice(0, cursorPosition) + clipboardText + currentText.slice(cursorPosition); 56 | activeElement.selectionStart = activeElement.selectionEnd = cursorPosition + clipboardText.length; 57 | } 58 | } 59 | } 60 | } 61 | document.addEventListener("keydown", CopyPasteFunc); 62 | 63 | Router.run(routes, (Handler, routerState) => { 64 | React.render(( 65 | 66 | {() => } 67 | 68 | ), document.getElementById("content")); 69 | }) 70 | -------------------------------------------------------------------------------- /src/pages/announcements.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Api from '../api/api' 3 | import {connect} from 'react-redux'; 4 | import {UPDATE_ANNOUNCEMENTS, RESET_EVENTS} from '../data/store' 5 | global.React = React; 6 | var md2react = require('md2react'); 7 | 8 | class Announcements extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | updated: null, 13 | error: null 14 | }; 15 | } 16 | componentWillMount() { 17 | // Tap tap API server 18 | Api.announcements((json) => { 19 | this.props.updateAnnouncements(json); 20 | this.setState({ 21 | updated: Date() 22 | }); 23 | }, (mes) => { 24 | this.setState({ 25 | error: mes 26 | }); 27 | }) 28 | this.props.resetEvents(); 29 | } 30 | clearError() { 31 | this.setState({ 32 | error: null 33 | }); 34 | } 35 | render() { 36 | const {announcements} = this.props; 37 | var contents = announcements.map((announcement, index) => { 38 | return ( 39 |
40 |
41 | 42 |
43 |
44 |
45 | {announcement["title"]} 46 |
47 | {new Date(announcement["updated_at"]).toTimeString()} 48 |
49 |
50 |
51 | {md2react(announcement["body"], {gfm: true, tables: true})} 52 |
53 |
54 |
55 |
56 |
57 | ); 58 | }); 59 | if (!contents) contents =
No announcement
60 | var errorMessage; 61 | if (this.state.error) { 62 | errorMessage = ( 63 |
64 | 65 |
66 | Error 67 |
68 |

{this.state.error[0]}

69 |
70 | ); 71 | } 72 | return ( 73 |
74 |
Last updated: {this.state.updated}
75 |
76 | {contents} 77 |
78 |
79 |
80 | {errorMessage} 81 |
82 | ); 83 | } 84 | }; 85 | 86 | export default connect( 87 | (state) => ({ 88 | announcements: state.announcements 89 | }), 90 | (dispatch) => ({ 91 | updateAnnouncements: (data) => dispatch({type: UPDATE_ANNOUNCEMENTS, data: data}), 92 | resetEvents: () => dispatch({type: RESET_EVENTS, data: "announcements"}) 93 | }) 94 | )(Announcements); 95 | -------------------------------------------------------------------------------- /src/pages/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Api from '../api/api' 3 | import {connect} from 'react-redux'; 4 | import {UPDATE_TEAMLIST} from '../data/store' 5 | 6 | class Dashboard extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | error: null 11 | }; 12 | } 13 | componentWillMount() { 14 | // Tap tap API server 15 | Api.teamlist((json) => { 16 | this.props.updateTeamlist(json); 17 | }, (mes) => { 18 | this.setState({ 19 | error: mes 20 | }); 21 | }) 22 | this.timer = setInterval(() => { 23 | this.setState({now: new Date()}) 24 | }.bind(this), 1000); 25 | } 26 | componentWillUnmount() { 27 | clearInterval(this.timer); 28 | } 29 | clearError() { 30 | this.setState({ 31 | error: null 32 | }); 33 | } 34 | render() { 35 | const {point, solved, teamlist, ranking, countdown} = this.props; 36 | var errorMessage; 37 | if (this.state.error) { 38 | errorMessage = ( 39 |
40 | 41 |
42 | Error 43 |
44 |

{this.state.error[0]}

45 |
46 | ); 47 | } 48 | return ( 49 |
50 |

Countdown

51 |
52 |
53 |
54 |
55 | {countdown.h || 'n/a'} 56 |
57 |
58 | Hours 59 |
60 |
61 |
62 |
63 | {countdown.m || 'n/a'} 64 |
65 |
66 | Minutes 67 |
68 |
69 |
70 |
71 | {countdown.s || 'n/a'} 72 |
73 |
74 | Seconds 75 |
76 |
77 |
78 |
79 |

Statistics

80 |
81 |
82 |
83 |
84 |

Ranking

85 |
86 | {ranking} 87 |
88 |
89 | of {teamlist.length} 90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |

Score

98 |
99 | {point} 100 |
101 |
102 | points 103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |

Flags

111 |
112 | {solved} 113 |
114 |
115 | captured 116 |
117 |
118 |
119 |
120 |
121 | {errorMessage} 122 |
123 | ); 124 | } 125 | }; 126 | 127 | export default connect( 128 | (state) => ({ 129 | solved: state.solved, 130 | point: state.point, 131 | teamlist: state.teamList, 132 | ranking: state.ranking, 133 | start: state.config.start, 134 | end: state.config.end, 135 | countdown: state.countdown 136 | }), 137 | (dispatch) => ({updateTeamlist: (data) => dispatch({type: UPDATE_TEAMLIST, data: data})}) 138 | )(Dashboard); 139 | -------------------------------------------------------------------------------- /src/pages/login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Api from '../api/api' 3 | import {connect} from 'react-redux'; 4 | import {UPDATE_USERINFO} from '../data/store' 5 | 6 | class Login extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | username: "", 11 | password: "", 12 | login_pending: false, 13 | signup_pending: false, 14 | error: false, 15 | message: "", 16 | success: false 17 | }; 18 | } 19 | componentWillMount() { 20 | document.body.style.backgroundColor = "#f7c5d2"; 21 | if (this.props && this.props.query && this.props.query.message) { 22 | this.setState({ 23 | error: true, 24 | message: this.props.query.message 25 | }); 26 | } 27 | } 28 | componentWillUnmount() { 29 | document.body.style.backgroundColor = null; 30 | } 31 | changeText(e) { 32 | var value = {} 33 | value[e.target.name] = e.target.value; 34 | this.setState(value); 35 | } 36 | tryLogin() { 37 | this.setState({ 38 | login_pending: true, 39 | error: false, 40 | success: false 41 | }); 42 | 43 | Api.login(this.state.username, this.state.password, (userinfo) => { 44 | this.props.saveUserinfo(userinfo); 45 | this.context.router.transitionTo("dashboard"); 46 | }, (mes) => { 47 | this.setState({ 48 | login_pending: false, 49 | error: true, 50 | message: mes[0] 51 | }); 52 | }) 53 | } 54 | signUp() { 55 | this.setState({ 56 | signup_pending: true, 57 | error: false, 58 | success: false 59 | }); 60 | 61 | Api.signup(this.state.username, this.state.password, (userinfo) => { 62 | this.setState({ 63 | signup_pending: false, 64 | success: true 65 | }); 66 | React.render(
New account created

Press "Login" to start CTF!

, document.querySelector('.ui.success.message')); 67 | }, (mes) => { 68 | this.setState({ 69 | signup_pending: false, 70 | error: true, 71 | message: mes[0] 72 | }); 73 | }) 74 | return true 75 | } 76 | render() { 77 | var size400 = { 78 | paddingTop: "20px", 79 | paddingBottom: "10px", 80 | maxWidth: "400px", 81 | maxHeight: "400px" 82 | }; 83 | var fullHeight = { 84 | height: "100%" 85 | }; 86 | return ( 87 |
88 |
89 | Lepus-CTF Bunner 90 |

91 |
92 | Welcome to Lepus-CTF 93 |
94 |

95 |
96 |
97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | 111 |
112 | 113 |
114 |
115 |
116 |

{this.state.message}

117 |
118 |
119 |
120 |
121 | ); 122 | } 123 | }; 124 | Login.contextTypes = { 125 | router: React.PropTypes.func.isRequired 126 | }; 127 | 128 | export default connect( 129 | (state) => ({}), 130 | (dispatch) => ({saveUserinfo: (data) => dispatch({type: UPDATE_USERINFO, data: data})}) 131 | )(Login); 132 | -------------------------------------------------------------------------------- /src/pages/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Api from '../api/api' 3 | import {connect} from 'react-redux'; 4 | import {UPDATE_TEAMINFO, UPDATE_SERVEREVENT, UPDATE_CTFCONF, UPDATE_COUNTDOWN, EE} from '../data/store' 5 | import Router from 'react-router'; 6 | var DefaultRoute = Router.DefaultRoute; 7 | var Link = Router.Link; 8 | var Route = Router.Route; 9 | var RouteHandler = Router.RouteHandler; 10 | 11 | class Main extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = {url: "", breakingnews: true, playOnce: false, error: null}; 15 | this.watchServerEvent(); 16 | this.getCTFConfigurations(); 17 | } 18 | static willTransitionTo(transition) { 19 | //TODO: check logon status 20 | var logon = true; 21 | if (!logon){ 22 | transition.redirect('/login'); 23 | } 24 | } 25 | updateTeaminfo() { 26 | Api.team(this.props.userinfo.team, (json) => { 27 | this.props.updateTeaminfo(json); 28 | }, (mes) => { 29 | this.setState({ 30 | error: mes 31 | }); 32 | }) 33 | } 34 | getCTFConfigurations() { 35 | Api.configurations((json) => { 36 | this.props.updateCTFConfigurations(json); 37 | }, (mes) => { 38 | this.setState({ 39 | error: mes 40 | }); 41 | }) 42 | } 43 | watchServerEvent() { 44 | var socket = require('socket.io-client'); 45 | var io = socket('SCORE_SERVER_URL', { 46 | secure: true, 47 | transports: ["websocket"] 48 | }); 49 | io.on('connect', () => { 50 | console.log("Socket.io connected!") 51 | }); 52 | io.on('event', (data) => { 53 | switch (data.type) { 54 | case "update": 55 | this.props.onReceiveServerEvent(data); 56 | switch (data.model) { 57 | case "notice": 58 | setTimeout(function() { 59 | Api.announcement(data.id, (json) => { 60 | var n = new Notification('Lepus-CTF - Announcements', { 61 | body: json.title 62 | }); 63 | n.onclick = () => { 64 | this.context.router.transitionTo("announcements"); 65 | }.bind(this); 66 | }.bind(this), (mes) => { 67 | var n = new Notification('Lepus-CTF - Announcements', { 68 | body: 'New 1 announcement' 69 | }); 70 | n.onclick = () => { 71 | this.context.router.transitionTo("announcements"); 72 | }.bind(this); 73 | }.bind(this)) 74 | }.bind(this), ~~(Math.random() * 1000)); 75 | break; 76 | case "question": 77 | console.log(JSON.stringify(data)); 78 | setTimeout(function() { 79 | Api.problem(data.id, (json) => { 80 | var n = new Notification('Lepus-CTF - Problems', { 81 | body: json.title + ' updated' 82 | }); 83 | n.onclick = () => { 84 | this.context.router.transitionTo("problems"); 85 | }.bind(this); 86 | }.bind(this), (mes) => { 87 | var n = new Notification('Lepus-CTF - Problems', { 88 | body: 'New 1 problem update' 89 | }); 90 | n.onclick = () => { 91 | this.context.router.transitionTo("problems"); 92 | }.bind(this); 93 | }.bind(this)) 94 | }.bind(this), ~~(Math.random() * 1000)); 95 | break; 96 | } 97 | break; 98 | case "answer": 99 | if (data.team === this.props.userinfo.team) { 100 | this.updateTeaminfo(); 101 | } 102 | break; 103 | case "youtube": 104 | this.setState({url: "https://www.youtube.com/embed/" + data["video_id"] + "?autoplay=1&controls=0&fs=0&showinfo=0&rel=0&disablekb=1&modestbranding=1"}); 105 | if (!this.state.breakingnews) { 106 | var n = new Notification('Lepus-CTF', { 107 | body: 'Breaking news!' 108 | }); 109 | n.onclick = () => { 110 | this.setState({playOnce: true}); 111 | }; 112 | } 113 | break; 114 | default: 115 | console.log(JSON.stringify(data)); 116 | } 117 | this.setState({ 118 | render: new Date() 119 | }); 120 | }.bind(this)); 121 | io.on('disconnect', () => { 122 | console.warn("Socket.io disconnected."); 123 | }); 124 | } 125 | componentWillMount() { 126 | document.body.style.backgroundColor = "#1abc9c"; 127 | setInterval(() => { 128 | if (!!this.props.start && !!this.props.end) { 129 | const now = new Date(); 130 | if (now - this.props.start < 0) { 131 | return; 132 | } else if (this.props.end - now < 0) { 133 | this.props.updateCountdown(null, null, null); 134 | return; 135 | } 136 | const diff = new Date(this.props.end - now); 137 | const time = diff.toUTCString().replace(/^.*(\d\d:\d\d:\d\d).*$/,'$1'); 138 | const h = time.replace(/^(\d\d):\d\d:\d\d$/,'$1'); 139 | const m = time.replace(/^\d\d:(\d\d):\d\d$/,'$1'); 140 | const s = time.replace(/^\d\d:\d\d:(\d\d)$/,'$1'); 141 | this.props.updateCountdown(h, m, s); 142 | } 143 | }.bind(this), 1000); 144 | Api.setCriticalAction((() => {this.context.router.transitionTo("login", {}, {message: "You need to re-login."})}.bind(this))); 145 | } 146 | componentWillUnmount() { 147 | document.body.style.backgroundColor = null; 148 | } 149 | unsetBreakingnews() { 150 | this.setState({url: null, breakingnews: false}) 151 | } 152 | closeBreakingnewsModal() { 153 | this.setState({url: null, playOnce: false}) 154 | } 155 | closeEEModal() { 156 | this.props.resetEE(); 157 | } 158 | clearError() { 159 | this.setState({ 160 | error: null 161 | }); 162 | } 163 | render() { 164 | var mainStyle = { 165 | height: "100%", 166 | padding: "20px", 167 | paddingLeft: "220px", 168 | }; 169 | var maxSize = { 170 | maxHeight: "100%", 171 | maxWidth: "100%", 172 | }; 173 | var centeredModal = { 174 | height: "70%", 175 | marginTop: "-20%", 176 | } 177 | const {point, solved, events, start, end, countdown} = this.props; 178 | var modal; 179 | if (this.state.url && (this.state.breakingnews || this.state.playOnce)) { 180 | modal = ( 181 |
182 |
183 | 184 |