├── front ├── static │ └── .gitkeep ├── .dockerignore ├── .eslintignore ├── build │ ├── logo.png │ ├── vue-loader.conf.js │ ├── build.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── prod.env.js │ ├── test.env.js │ ├── dev.env.js │ └── index.js ├── src │ ├── assets │ │ └── logo.png │ ├── App.vue │ ├── main.js │ └── components │ │ └── Echart.vue ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── Dockerfile ├── .babelrc ├── README.md ├── index.html ├── .eslintrc.js └── package.json ├── .gitignore ├── api ├── requirements.txt ├── Dockerfile └── app.py ├── parser ├── requirements.txt ├── Dockerfile ├── ws │ └── ws.py ├── parser.py ├── board_parser.py └── game.py ├── .env.example ├── nginx └── http.conf ├── LICENSE ├── docker-compose.yml ├── README.md └── docs └── README.ru.md /front/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | data/ -------------------------------------------------------------------------------- /front/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.2 2 | pymongo==3.12.1 -------------------------------------------------------------------------------- /front/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /front/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Red-Cadets/beard/HEAD/front/build/logo.png -------------------------------------------------------------------------------- /front/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /front/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Red-Cadets/beard/HEAD/front/src/assets/logo.png -------------------------------------------------------------------------------- /parser/requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==3.7.0 2 | requestium==0.1.9 3 | bs4==0.0.1 4 | requests>=2.20.0 5 | coloredlogs 6 | webdriver-manager 7 | websockets 8 | pymongo -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SCOREBOARD=http://6.0.0.1/board 2 | TEAM='Red Cadets' 3 | TYPE=hackerdom 4 | BOT_URL=http://bot/key 5 | ROUND_TIME=120 6 | EXTEND_ROUND=5 7 | MONGO_USER=parser 8 | MONGO_PASS=parser -------------------------------------------------------------------------------- /front/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /front/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip3 install -r requirements.txt 7 | 8 | COPY app.py . 9 | 10 | CMD [ "python3", "app.py"] 11 | 12 | -------------------------------------------------------------------------------- /front/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /front/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /front/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /front/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /front/Dockerfile: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:lts-alpine as build-stage 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY . . 7 | RUN npm run build 8 | 9 | # production-stage 10 | FROM nginx:stable-alpine as production-stage 11 | COPY --from=build-stage /app/dist /usr/share/nginx/html 12 | EXPOSE 80 13 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /parser/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | chromium \ 6 | python3-pip \ 7 | && apt-get clean \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | ENV DEBIAN_FRONTEND noninteractive 11 | ENV TZ Europe/Moscow 12 | 13 | WORKDIR /app 14 | 15 | COPY requirements.txt . 16 | 17 | RUN pip3 install -r requirements.txt --no-cache-dir 18 | 19 | COPY . . 20 | 21 | CMD [ "python3", "parser.py"] 22 | 23 | -------------------------------------------------------------------------------- /front/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /front/README.md: -------------------------------------------------------------------------------- 1 | # Parser 2 | 3 | > Parser frontend 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /front/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Parser 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /front/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /front/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import VueNativeSock from 'vue-native-websocket' 6 | import VueAxios from 'vue-axios' 7 | import axios from 'axios' 8 | 9 | Vue.use(VueNativeSock, `ws://${window.location.host}/ws/`, { 10 | reconnection: true, 11 | reconnectionAttempts: 5, 12 | reconnectionDelay: 500 13 | }) 14 | Vue.use(VueAxios, axios) 15 | 16 | Vue.config.productionTip = false 17 | 18 | /* eslint-disable no-new */ 19 | new Vue({ 20 | el: '#app', 21 | components: { App }, 22 | template: '' 23 | }) 24 | -------------------------------------------------------------------------------- /nginx/http.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { 4 | 5 | worker_connections 1024; 6 | } 7 | 8 | 9 | http { 10 | server { 11 | listen 80 default_server; 12 | listen [::]:80 default_server; 13 | 14 | client_max_body_size 100m; 15 | 16 | location /api/ { 17 | proxy_pass http://api:8888/api/; 18 | } 19 | 20 | location /ws/ { 21 | proxy_pass http://parser:9090; 22 | proxy_http_version 1.1; 23 | proxy_set_header Upgrade $http_upgrade; 24 | proxy_set_header Connection "upgrade"; 25 | } 26 | location / { 27 | proxy_pass http://front:80/; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /front/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Red Cadets 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /parser/ws/ws.py: -------------------------------------------------------------------------------- 1 | import websockets 2 | import threading 3 | import asyncio 4 | import json 5 | 6 | CLIENTS = set() 7 | 8 | 9 | async def handler(websocket): 10 | async for message in websocket: 11 | try: 12 | data = json.loads(message) 13 | except: 14 | return 15 | if data.get('type') == 'connect': 16 | CLIENTS.add(websocket) 17 | if data.get('type') == 'updated': 18 | for ws in CLIENTS: 19 | try: 20 | await ws.send(json.dumps(data.get('data'))) 21 | except: 22 | continue 23 | 24 | 25 | async def main(): 26 | async with websockets.serve(handler, "0.0.0.0", 9090): 27 | await asyncio.Future() 28 | 29 | 30 | async def updated(info='', team_info=''): 31 | async with websockets.connect('ws://127.0.0.1:9090') as websocket: 32 | if team_info.get('_id'): 33 | del(team_info['_id']) 34 | if info.get('_id'): 35 | del(info['_id']) 36 | await websocket.send(json.dumps({'type': 'updated', 'data': {'team_info': team_info, 'info': info}})) 37 | 38 | ws_server = threading.Thread(target=asyncio.run, args=(main(),), daemon=True) 39 | ws_server.start() 40 | -------------------------------------------------------------------------------- /front/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /front/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /api/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, jsonify 4 | from pymongo import MongoClient 5 | 6 | CONFIG = { 7 | "SCOREBOARD": os.getenv('SCOREBOARD', 'http://127.0.0.1:8090'), 8 | "TEAM": os.getenv('TEAM', 'Red Cadets'), 9 | "TYPE": os.getenv('TYPE', 'forcad'), 10 | "BOT_URL": os.getenv('BOT_URL', 'https://bot.example.com/key'), 11 | "MONGO_USER": os.getenv('MONGO_USER', 'parser'), 12 | "MONGO_PASS": os.getenv('MONGO_PASS', 'parser'), 13 | "ROUND_TIME": int(os.getenv('ROUND_TIME', '120')), 14 | "EXTEND_ROUND": int(os.getenv('EXTEND_ROUND', '50')) 15 | } 16 | 17 | app = Flask(__name__) 18 | 19 | mongo_client = MongoClient( 20 | f"mongodb://{CONFIG['MONGO_USER']}:{CONFIG['MONGO_PASS']}@mongo:27017/") 21 | db = mongo_client.parse 22 | 23 | #? Данные всех команд 24 | info = db.data 25 | 26 | #? Данные отслеживаемой команды 27 | teamInfo = db.team_info 28 | 29 | 30 | @app.route('/api/info') 31 | def index(): 32 | DATA = [] 33 | cursor = info.find({}) 34 | for document in cursor: 35 | del(document['_id']) 36 | DATA.append(document) 37 | return jsonify(DATA) 38 | 39 | 40 | @app.route('/api/team_info') 41 | def team_info(): 42 | DATA = [] 43 | cursor = teamInfo.find({}) 44 | for document in cursor: 45 | del(document['_id']) 46 | DATA.append(document) 47 | return jsonify(DATA) 48 | 49 | 50 | @app.route('/api/config') 51 | def config(): 52 | return jsonify(CONFIG) 53 | 54 | 55 | if __name__ == "__main__": 56 | app.run("0.0.0.0", 8888) 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | front: 5 | restart: unless-stopped 6 | build: 7 | context: ./front 8 | dockerfile: Dockerfile 9 | container_name: parser-front 10 | image: parser-front 11 | volumes: 12 | - ./front/:/app/ 13 | networks: 14 | - parser 15 | env_file: .env 16 | 17 | parser: 18 | restart: unless-stopped 19 | build: 20 | context: ./parser 21 | dockerfile: Dockerfile 22 | container_name: parser-scrapper 23 | image: parser-scrapper 24 | networks: 25 | - parser 26 | env_file: .env 27 | shm_size: 2gb 28 | depends_on: 29 | - mongo 30 | 31 | api: 32 | restart: unless-stopped 33 | build: 34 | context: ./api 35 | dockerfile: Dockerfile 36 | container_name: parser-api 37 | image: parser-api 38 | networks: 39 | - parser 40 | env_file: .env 41 | depends_on: 42 | - mongo 43 | 44 | mongo: 45 | image: mongo:4.4 46 | restart: unless-stopped 47 | container_name: parser-mongodb 48 | volumes: 49 | - ./data/mongodb:/data/db 50 | env_file: .env 51 | environment: 52 | MONGO_INITDB_ROOT_USERNAME: $MONGO_USER 53 | MONGO_INITDB_ROOT_PASSWORD: $MONGO_PASS 54 | networks: 55 | - parser 56 | 57 | nginx: 58 | image: nginx:1.17 59 | container_name: parser-nginx 60 | restart: unless-stopped 61 | volumes: 62 | - ./nginx/http.conf:/etc/nginx/nginx.conf 63 | ports: 64 | - 65005:80 65 | depends_on: 66 | - front 67 | networks: 68 | - parser 69 | 70 | networks: 71 | parser: 72 | name: parser-network 73 | driver: bridge -------------------------------------------------------------------------------- /front/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: '0.0.0.0', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /front/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /front/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /front/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parser", 3 | "version": "1.0.0", 4 | "description": "Parser frontend", 5 | "author": "Red Cadets ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "unit": "jest --config test/unit/jest.conf.js --coverage", 11 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 12 | "build": "node build/build.js" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.24.0", 16 | "echarts": "^5.2.2", 17 | "moment": "^2.29.1", 18 | "vue": "^2.5.2", 19 | "vue-axios": "^3.4.0", 20 | "vue-echarts": "^6.0.0", 21 | "vue-native-websocket": "^2.0.15" 22 | }, 23 | "devDependencies": { 24 | "@vue/composition-api": "^1.4.0", 25 | "autoprefixer": "^7.1.2", 26 | "babel-core": "^6.22.1", 27 | "babel-eslint": "^8.2.1", 28 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 29 | "babel-jest": "^21.0.2", 30 | "babel-loader": "^7.1.1", 31 | "babel-plugin-dynamic-import-node": "^1.2.0", 32 | "babel-plugin-syntax-jsx": "^6.18.0", 33 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 34 | "babel-plugin-transform-runtime": "^6.22.0", 35 | "babel-plugin-transform-vue-jsx": "^3.5.0", 36 | "babel-preset-env": "^1.3.2", 37 | "babel-preset-stage-2": "^6.22.0", 38 | "babel-register": "^6.22.0", 39 | "chalk": "^2.0.1", 40 | "chromedriver": "^2.27.2", 41 | "copy-webpack-plugin": "^4.0.1", 42 | "cross-spawn": "^5.0.1", 43 | "css-loader": "^0.28.0", 44 | "eslint": "^4.15.0", 45 | "eslint-config-standard": "^10.2.1", 46 | "eslint-friendly-formatter": "^3.0.0", 47 | "eslint-loader": "^1.7.1", 48 | "eslint-plugin-import": "^2.7.0", 49 | "eslint-plugin-node": "^5.2.0", 50 | "eslint-plugin-promise": "^3.4.0", 51 | "eslint-plugin-standard": "^3.0.1", 52 | "eslint-plugin-vue": "^4.0.0", 53 | "extract-text-webpack-plugin": "^3.0.0", 54 | "file-loader": "^1.1.4", 55 | "friendly-errors-webpack-plugin": "^1.6.1", 56 | "html-webpack-plugin": "^2.30.1", 57 | "jest": "^22.0.4", 58 | "jest-serializer-vue": "^0.3.0", 59 | "nightwatch": "^0.9.12", 60 | "node-notifier": "^5.1.2", 61 | "optimize-css-assets-webpack-plugin": "^3.2.0", 62 | "ora": "^1.2.0", 63 | "portfinder": "^1.0.13", 64 | "postcss-import": "^11.0.0", 65 | "postcss-loader": "^2.0.8", 66 | "postcss-url": "^7.2.1", 67 | "rimraf": "^2.6.0", 68 | "selenium-server": "^3.0.1", 69 | "semver": "^5.3.0", 70 | "shelljs": "^0.7.6", 71 | "uglifyjs-webpack-plugin": "^1.1.1", 72 | "url-loader": "^0.5.8", 73 | "vue-jest": "^1.0.2", 74 | "vue-loader": "^13.3.0", 75 | "vue-style-loader": "^3.0.1", 76 | "vue-template-compiler": "^2.5.2", 77 | "webpack": "^3.6.0", 78 | "webpack-bundle-analyzer": "^2.9.0", 79 | "webpack-dev-server": "^2.9.1", 80 | "webpack-merge": "^4.1.0" 81 | }, 82 | "engines": { 83 | "node": ">= 6.0.0", 84 | "npm": ">= 3.0.0" 85 | }, 86 | "browserslist": [ 87 | "> 1%", 88 | "last 2 versions", 89 | "not ie <= 8" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /front/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /parser/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from datetime import datetime 4 | from requestium import Session as Sess 5 | from webdriver_manager.chrome import ChromeDriverManager 6 | from webdriver_manager.utils import ChromeType 7 | from pymongo import MongoClient 8 | from bson.objectid import ObjectId 9 | import asyncio 10 | import coloredlogs 11 | import logging 12 | import os 13 | import re 14 | import time 15 | 16 | from ws import ws 17 | import game 18 | 19 | coloredlogs.install() 20 | 21 | def follow(game_obj, team_ip, driver, teamname): 22 | state = False 23 | while not state: 24 | state = game_obj.refresh(driver) 25 | time.sleep(2) 26 | if team_ip: 27 | return game_obj.get_delta_by_ip(team_ip) 28 | else: 29 | return game_obj.get_delta_by_name(teamname) 30 | 31 | INFO = {} 32 | 33 | SCOREBOARD = os.getenv('SCOREBOARD', 'http://6.0.0.1') 34 | HEADLESS = True if os.getenv('TYPE', 'hackerdom') == "forcad" else False 35 | TEAM = os.getenv('TEAM', 'Red Cadets') 36 | MONGO_USER = os.getenv('MONGO_USER', 'parser') 37 | MONGO_PASS = os.getenv('MONGO_PASS', 'parser') 38 | 39 | if re.search('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', TEAM) is None: 40 | TEAM_NAME = TEAM 41 | TEAM_IP = None 42 | else: 43 | TEAM_NAME = None 44 | TEAM_IP = TEAM 45 | 46 | if HEADLESS: 47 | s = Sess( 48 | ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install(), 49 | browser='chrome', 50 | default_timeout=15, 51 | webdriver_options={ 52 | 'arguments': [ 53 | 'headless', 54 | 'disable-dev-shm-using', 55 | 'no-sandbox']}) 56 | 57 | try: 58 | s.driver.get(SCOREBOARD) 59 | except Exception as e: 60 | logging.critical('Something went wrong: {} {}'.format(e, SCOREBOARD)) 61 | exit(1) 62 | 63 | driver = s.driver 64 | else: 65 | driver = None 66 | 67 | 68 | mongo_client = MongoClient( 69 | "mongodb://{}:{}@mongo:27017/".format(MONGO_USER, MONGO_PASS)) 70 | db = mongo_client.parse 71 | 72 | #? Данные всех команд 73 | info = db.data 74 | 75 | #? Данные отслеживаемой команды 76 | teamInfo = db.team_info 77 | 78 | AD = game.AD(TEAM_IP, driver, SCOREBOARD, TEAM_NAME) 79 | 80 | # ? Получение информации по названию команды либо IP адресу 81 | if TEAM_IP: 82 | AD.get_info_by_ip(TEAM_IP) 83 | elif TEAM_NAME: 84 | AD.get_info_by_name(TEAM_NAME) 85 | 86 | # ? Если нет информации о текущем раунде 87 | res = info.find_one({"round": AD.round}) 88 | if not res: 89 | INFO['teams_info'] = AD.dump() 90 | INFO['round'] = AD.round 91 | INFO['time'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 92 | INFO['_id'] = ObjectId() 93 | info.insert_one(INFO) 94 | logging.info('Inserted: {}'.format(AD.round)) 95 | 96 | # ? Отправить всем клиентам изменения 97 | asyncio.run(ws.updated()) 98 | 99 | while True: 100 | try: 101 | team_info = follow(AD, TEAM_IP, driver, TEAM_NAME) 102 | INFO['teams_info'] = AD.dump() 103 | INFO['round'] = AD.round 104 | INFO['time'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 105 | INFO['_id'] = ObjectId() 106 | info.insert_one(INFO) 107 | 108 | team_info['_id'] = ObjectId() 109 | teamInfo.insert_one(team_info) 110 | 111 | logging.info('Inserted: {}'.format(AD.round)) 112 | 113 | asyncio.run(ws.updated(INFO, team_info)) 114 | except Exception as e: 115 | logging.critical(e) 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Beard - Attack/Defense CTF scoreboard parser 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | Language: English | Русский 10 |

11 | 12 | Beard - a comfortable way to track progress of your team during A/D competitions 13 |

14 | 15 | 16 | 17 | 18 | # ✨ Features 19 | 20 | - Parsing of supported scoreboards (hackerdom/forcad) 21 | - Score graph of all teams with automatic scaling for your team 22 | - Primitive prediction of the score graph 23 | - Flag loss graph for each service 24 | - Graph of receiving flags for each service (similar to the effectiveness of exploits) 25 | - Telegram alerts about flag loss, service status, place changing (use [courier](https://github.com/Red-Cadets/courier)) 26 | 27 | ## 🛠 Supported scoreboards 28 | 29 | | **A/D framework** | Link | Status | Description 30 | | ------------------ | ---- | ------ | ----------- 31 | | ForcAD | https://github.com/pomo-mondreganto/ForcAD | ✅ | 32 | | HackerDom checksystem | https://github.com/HackerDom/checksystem | ✅ | parsing old-style view at /board 33 | 34 | ## 🙋 Table of Contents 35 | * 📖 [Fast Installation Guide](https://github.com/Red-Cadets/beard#-fast-installation-guide) 36 | * 🐋 [Docker Usage](https://github.com/Red-Cadets/beard#whale-docker) 37 | * 🖼️ [Gallery](https://github.com/Red-Cadets/beard#-gallery) 38 | * 🎪 [Community](https://github.com/Red-Cadets/beard#-community) 39 | * 📝 [TODO](https://github.com/Red-Cadets/beard#-todo) 40 | 41 | 42 | # 📖 Fast Installation Guide 43 | 44 | ## :whale: Docker 45 | 46 | Clone repository 47 | ```bash 48 | git clone https://github.com/Red-Cadets/beard.git 49 | ``` 50 | Go to folder: 51 | ```bash 52 | cd beard 53 | ``` 54 | Change .env with your settings: 55 | - `SCOREBOARD` - Scoreboard location. Example: `http://6.0.0.1/board` 56 | - `TEAM` - Team name or team IP to display information about. Example: `Red Cadets` or `10.10.1.15` 57 | - `TYPE` - Scoreboard type. Example: `forcad` or `hackerdom` 58 | - `BOT_URL` - Telegram bot api address (webhook) for notification. For easy bot integration, use [courier](https://github.com/Red-Cadets/courier). Message format: 59 | ```json 60 | { 61 | "message": "Notification here", 62 | "type": "markdown", 63 | "id": "parser", 64 | "to": "tg chat id here" 65 | } 66 | ``` 67 | 68 | - `ROUND_TIME` - Round time in seconds. For example: `120` 69 | - `EXTEND_ROUND` - The number of rounds to predict future graph. The prediction is based on the points of the last 5 rounds. For example: `10` 70 | - `MONGO_USER` - DB username. Например: `parser` 71 | - `MONGO_PASS` - DB password. Например: `parser` 72 | Run docker-compose: 73 | ```bash 74 | docker-compose up -d 75 | ``` 76 | and go to URL 77 | ```bash 78 | http://127.0.0.1:65005/ 79 | ``` 80 | 81 | ## 🖼️ Gallery 82 | 83 | || 84 | |:-------------------------:| 85 | |![Главная страница](https://i.ibb.co/SQrxpVD/Scores.png)| 86 | |Graph of scores of all teams on the scoreboard| 87 | |![Главная страница](https://i.ibb.co/Sc7vBzs/Echarts-lost.png) 88 | |Flag loss graph| 89 | |![Главная страница](https://i.ibb.co/JCQD2g6/Echarts-got.png)| 90 | |Graph of receiving flags| 91 | |![Главная страница](https://i.ibb.co/VCMzK05/image.png)| 92 | |Telegram alerts| 93 | # 🎪 Community 94 | 95 | If you have any feature suggestions or bugs, leave a Github issue. 96 | Open to pull requests and other forms of collaboration! 97 | 98 | We communicate over Telegram. [Click here](https://t.me/redcadets_chat) to join our Telegram community! 99 | 100 | ## 📝 TODO 101 | 102 | > Open to ideas! 103 | 104 | # ❤️ Thanks to 105 | 106 | Hackerdom parser is based on https://github.com/Vindori/hackerdom-board-parser -------------------------------------------------------------------------------- /docs/README.ru.md: -------------------------------------------------------------------------------- 1 | # Beard - Attack/Defense CTF scoreboard parser 2 | 3 |

4 | 5 | 6 | 7 | 8 |

9 | Язык: English | Русский 10 |

11 | 12 | Beard - инструмент для удобного отслеживания прогресса вашей команды во время A/D CTF соревнований. 13 | 14 | 15 | 16 | # ✨ Возможности 17 | 18 | - Парсинг поддерживаемых видов турнирных таблиц (hackerdom/forcad) 19 | - График очков всех команд с автоматическим масштабированием для вашей команды 20 | - Примитивное предсказание графика очков 21 | - График потерь флагов для каждого сервиса 22 | - График получения флагов для каждого сервиса (аналогично эффективности эксплойтов) 23 | - Оповещения в телеграм о потере флагов, статусе сервисов, изменении места команды (используйте [курьера](https://github.com/Red-Cadets/courier)) 24 | 25 | ## 🛠 Поддерживаемые турнирные таблицы 26 | 27 | | **A/D framework** | Ссылка | Статус | Описание 28 | | ------------------ | ---- | ------ | ----------- 29 | | ForcAD | https://github.com/pomo-mondreganto/ForcAD | ✅ | 30 | | HackerDom checksystem | https://github.com/HackerDom/checksystem | ✅ | парсинг старой версии на /board 31 | 32 | ## 🙋 Содержание 33 | * 📖 [Инструкция по быстрой установке](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-инструкция-по-быстрой-установке) 34 | * 🐋 [Docker](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#whale-docker) 35 | * 🖼️ [Галерея скриншотов](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-галерея) 36 | * 🎪 [Сообщество](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-сообщество) 37 | * 📝 [Планы на будущее](https://github.com/Red-Cadets/beard/blob/master/docs/README.ru.md#-планы-на-будущее) 38 | 39 | 40 | # 📖 Инструкция по быстрой установке] 41 | 42 | ## :whale: Docker 43 | 44 | Скачайте репозиторий 45 | ```bash 46 | git clone https://github.com/Red-Cadets/beard.git 47 | ``` 48 | Перейдите в директорию: 49 | ```bash 50 | cd beard 51 | ``` 52 | Измените .env с вашими настройками: 53 | - `HOST` - IP адрес или домен, на котором будет развёрнуто приложение. Данный параметр необходим для настройки CORS. Пример: `http://8.8.8.8` или `http://example.com` 54 | - `SCOREBOARD` - Адрес скорборда. Пример: `http://6.0.0.1` 55 | - `TEAM` - Название команды либо IP команды, информацию о которой необходимо отображать. Пример: `Red Cadets` или `10.10.1.15` 56 | - `TYPE` - Тип скорборда. Пример: `forcad` или `hackerdom` 57 | - `BOT_URL` - Адрес бота (вебхук) для уведомления о событиях. Формат сообщения: ```{"message": "Здесь уведомление", "type": "markdown", "id": "parser"}```. Для простой интеграции бота используйте [курьера](https://github.com/Red-Cadets/courier). Формат сообщения: 58 | ```json 59 | { 60 | "message": "Сообщение уведомления", 61 | "type": "markdown", 62 | "id": "parser", 63 | "to": "id телеграм чата" 64 | } 65 | ``` 66 | - `ROUND_TIME` - Время раунда в секундах. Например: `120` 67 | - `EXTEND_ROUND` - Количество раундов, на которое необходимо предсказать график очков всех команд. Предсказание происходит на основе очков крайних 5-ти раундов. Например: `10` 68 | - `MONGO_USER` - Пользователь БД. Например: `parser` 69 | - `MONGO_PASS` - Пароль пользователя БД. Например: `parser` 70 | 71 | Запустите docker-compose: 72 | ```bash 73 | docker-compose up -d 74 | ``` 75 | и перейдите по ссылке 76 | ```bash 77 | http://127.0.0.1:65005/ 78 | ``` 79 | 80 | ## 🖼️ Галерея 81 | 82 | || 83 | |:-------------------------:| 84 | |![Главная страница](https://i.ibb.co/SQrxpVD/Scores.png)| 85 | |График очков всех команд на турнирной таблице| 86 | |![Главная страница](https://i.ibb.co/Sc7vBzs/Echarts-lost.png) 87 | |График потери флагов| 88 | |![Главная страница](https://i.ibb.co/JCQD2g6/Echarts-got.png)| 89 | |График получения флагов| 90 | |![Главная страница](https://i.ibb.co/VCMzK05/image.png)| 91 | |Оповещения в телеграм| 92 | 93 | 94 | 95 | # 🎪 Сообщество 96 | 97 | Вы можете оставить информацию о багах или улучшениях в виде ишьюсов. 98 | Мы также открыты к вашим пулл-реквестам и любым видам взаимодействия! 99 | 100 | Мы взаимодействуем через Telegram. [Нажмите здесь](https://t.me/redcadets_chat), чтобы вступить в наше сообщество! 101 | 102 | ## 📝 Планы на будущее 103 | 104 | > Открыты к новым идеям! 105 | 106 | # ❤️ Благодарности 107 | 108 | Парсер турнирной таблицы Hackerdom основан на https://github.com/Vindori/hackerdom-board-parser 109 | -------------------------------------------------------------------------------- /front/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 79 | chunksSortMode: 'dependency' 80 | }), 81 | // keep module.id stable when vendor modules does not change 82 | new webpack.HashedModuleIdsPlugin(), 83 | // enable scope hoisting 84 | new webpack.optimize.ModuleConcatenationPlugin(), 85 | // split vendor js into its own file 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'vendor', 88 | minChunks (module) { 89 | // any required modules inside node_modules are extracted to vendor 90 | return ( 91 | module.resource && 92 | /\.js$/.test(module.resource) && 93 | module.resource.indexOf( 94 | path.join(__dirname, '../node_modules') 95 | ) === 0 96 | ) 97 | } 98 | }), 99 | // extract webpack runtime and module manifest to its own file in order to 100 | // prevent vendor hash from being updated whenever app bundle is updated 101 | new webpack.optimize.CommonsChunkPlugin({ 102 | name: 'manifest', 103 | minChunks: Infinity 104 | }), 105 | // This instance extracts shared chunks from code splitted chunks and bundles them 106 | // in a separate chunk, similar to the vendor chunk 107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 108 | new webpack.optimize.CommonsChunkPlugin({ 109 | name: 'app', 110 | async: 'vendor-async', 111 | children: true, 112 | minChunks: 3 113 | }), 114 | 115 | // copy custom static assets 116 | new CopyWebpackPlugin([ 117 | { 118 | from: path.resolve(__dirname, '../static'), 119 | to: config.build.assetsSubDirectory, 120 | ignore: ['.*'] 121 | } 122 | ]) 123 | ] 124 | }) 125 | 126 | if (config.build.productionGzip) { 127 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 128 | 129 | webpackConfig.plugins.push( 130 | new CompressionWebpackPlugin({ 131 | asset: '[path].gz[query]', 132 | algorithm: 'gzip', 133 | test: new RegExp( 134 | '\\.(' + 135 | config.build.productionGzipExtensions.join('|') + 136 | ')$' 137 | ), 138 | threshold: 10240, 139 | minRatio: 0.8 140 | }) 141 | ) 142 | } 143 | 144 | if (config.build.bundleAnalyzerReport) { 145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 147 | } 148 | 149 | module.exports = webpackConfig 150 | -------------------------------------------------------------------------------- /parser/board_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | import coloredlogs 4 | import logging 5 | 6 | from bs4 import BeautifulSoup 7 | from requests.exceptions import ConnectionError 8 | 9 | coloredlogs.install() 10 | 11 | 12 | def prettify(text): 13 | return text.strip().replace('\n', '').replace(' ', '') 14 | 15 | 16 | def remove_trash(text): 17 | trash = re.findall('\([+|-][0-9]+\)', text) 18 | for t in trash: 19 | text = text.replace(t, '') 20 | return text 21 | 22 | 23 | def return_status(status_code): 24 | if status_code == "status-110" or status_code == "status_" or 'rgb(255, 255, 0)' in status_code: 25 | return "CHECK FAILED" 26 | elif status_code == 'status-101' or status_code == "status_up" or 'rgb(125, 252, 116)' in status_code: 27 | return "UP" 28 | elif status_code == 'status-102' or status_code == "status_corrupt" or 'rgb(81, 145, 255)' in status_code: 29 | return "CORRUPT" 30 | elif status_code == 'status-103' or status_code == "status_mumble" or 'rgb(255, 145, 20)' in status_code: 31 | return "MUMBLE" 32 | elif status_code == 'status-104' or status_code == "status_down" or 'rgb(255, 91, 91);' in status_code: 33 | return "DOWN" 34 | else: 35 | return "ERROR, UNKNOWN STATUS CODE" 36 | 37 | 38 | def get_services(driver, soup): 39 | if driver: 40 | services = [] 41 | # * Ждать, пока страница прогрузится 42 | while not services: 43 | services = driver.find_elements_by_class_name("service-name") 44 | return [service.strip() for service in services[0].text.split('\n')] 45 | elif soup: 46 | return [service.text for service in soup.findAll('th', 'service_name')] 47 | else: 48 | logging.critical("Something went wrong [get-services]") 49 | exit(1) 50 | 51 | 52 | def init_patch(driver, soup): 53 | patch = {} 54 | services = get_services(driver, soup) 55 | for service in services: 56 | patch[service] = True 57 | return patch 58 | 59 | 60 | def get_current_round(driver, soup): 61 | if soup: 62 | current_round = re.findall( 63 | '[0-9]+', soup.find('div', attrs={'id': 'round'}).text.strip())[0] 64 | return int(current_round) 65 | elif driver: 66 | current_round = 0 67 | # * Ждать пока страница прогрузится 68 | while int(current_round) == 0: 69 | current_round = driver.re(r'Round: (\d+)')[0] 70 | return int(current_round) 71 | else: 72 | logging.critical("Something went wrong [get-current-round]") 73 | exit(1) 74 | 75 | 76 | def get_teams_info(driver, soup): 77 | if driver: 78 | teams_info = [] 79 | # * Ждать, пока страница прогрузится 80 | while not teams_info: 81 | teams_info = driver.find_elements_by_class_name("row")[1:] 82 | teams = [{ 83 | 'name': team.text.split('\n')[1].strip(), 84 | 'place': int(team.text.split('\n')[0].strip()), 85 | 'score': float(team.text.split('\n')[3].strip()), 86 | 'ip': team.text.split('\n')[2].strip(), 87 | 'services': get_services_info_forcad(team, driver) 88 | } for team in teams_info] 89 | return teams 90 | elif soup: 91 | services = get_services(driver, soup) 92 | teams_html = soup.findAll('tr', attrs={'class': 'team'})[1:] 93 | teams = \ 94 | [ 95 | { 96 | 'name': team.find('div', 'team_name').text.strip(), 97 | 'place': int(remove_trash(team.find('td', 'place').text.strip())), 98 | 'score': float(team.find('td', 'score').text.strip()), 99 | 'ip': team.find('div', 'team_server').text.strip(), 100 | 'services': get_services_info_hackerdom(team, services) 101 | } 102 | for team in teams_html 103 | ] 104 | return teams 105 | else: 106 | logging.critical("Something went wrong [get-teams-info]") 107 | exit(1) 108 | 109 | 110 | def get_services_info_forcad(team, driver): 111 | services = team.text.split('\n')[4:] 112 | statuses = get_status_info(driver, team) 113 | services_name = get_services(driver, None) 114 | services_info = [{ 115 | "name": services_name[i % len(services)], 116 | "title": None, 117 | "sla": float(services[i * 3].split(':')[1].strip()[:-1]), 118 | "flag_points": float(services[i * 3 + 1].split(':')[1].strip()), 119 | "flags": {'got': int(services[i * 3 + 2].split('/')[0][1:]), 'lost':int(services[i * 3 + 2].split('/')[1][1:])}, 120 | "status": statuses[services_name[i % len(services)]] 121 | } for i, _ in enumerate(services[::3])] 122 | return services_info 123 | 124 | 125 | def get_services_info_hackerdom(soup, services): 126 | services_html = soup.findAll('td', 'team_service') 127 | services_status = [service['class'][1] for service in services_html] 128 | services_title = [service['title'].strip() if service['title'] 129 | else None for service in services_html] 130 | services_sla = [prettify(sla.find('div', 'param_value').text) 131 | for sla in soup.findAll('div', 'sla')] 132 | services_fp = [prettify(fp.find('div', 'param_value').text) 133 | for fp in soup.findAll('div', 'fp')] 134 | services_flags = [prettify(flags.find('div', 'param_value').text).split( 135 | '/') for flags in soup.findAll('div', 'flags')] 136 | services_flags = [[abs(int(i)) for i in flags] if len(flags) == 2 else [ 137 | int(flags[0]), 0] for flags in services_flags] 138 | services_flags = [{'got': flags[0], 'lost': flags[1]} 139 | for flags in services_flags] 140 | services_info = \ 141 | [ 142 | { 143 | 'name': services[i % len(services)], 144 | 'status': service_info[0], 145 | 'sla': float(service_info[1][:-1]), 146 | 'flag_points': float(service_info[2]), 147 | 'flags': service_info[3], 148 | 'title': service_info[4] 149 | } 150 | for i, service_info in enumerate(zip(services_status, services_sla, services_fp, services_flags, services_title)) 151 | ] 152 | return services_info 153 | 154 | 155 | def get_status_info(driver, team): 156 | services_name = get_services(driver, None) 157 | services = team.find_elements_by_class_name("service-cell") 158 | return {services_name[number % len(services)]: return_status(code.get_attribute("style")) for number, code in enumerate(services)} 159 | 160 | 161 | def get_soup_by_address(address): 162 | if not address.startswith('http'): 163 | address = 'http://' + address 164 | try: 165 | html = requests.get(address) 166 | return BeautifulSoup(html.text, 'html.parser') 167 | 168 | except ConnectionError: 169 | logging.critical("Connection Error: " + address) 170 | exit(1) 171 | 172 | except Exception as e: 173 | logging.critical("Something went wrong: ".format(e)) 174 | exit(1) 175 | -------------------------------------------------------------------------------- /parser/game.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import coloredlogs 4 | import logging 5 | 6 | import board_parser 7 | 8 | coloredlogs.install() 9 | 10 | URL = os.getenv('BOT_URL', 'https://bot.example.com/key') 11 | 12 | 13 | class AD(object): 14 | def __init__(self, ip, driver, scoreboard, teamname): 15 | global info, delta, soup 16 | delta = [] 17 | self.ip = ip 18 | self.teamname = teamname 19 | self.scoreboard = scoreboard 20 | 21 | if not driver: 22 | soup = board_parser.get_soup_by_address(self.scoreboard) 23 | else: 24 | soup = None 25 | 26 | self.patch = board_parser.init_patch(driver, soup) 27 | 28 | self.round = board_parser.get_current_round(driver, soup) 29 | info = board_parser.get_teams_info(driver, soup) 30 | 31 | def get_info_by_ip(self, ip): 32 | for team in info: 33 | if team['ip'] == ip: 34 | return team 35 | logging.critical("Нет команды с IP {ip}".format(ip=ip)) 36 | 37 | def get_info_by_name(self, name): 38 | for team in info: 39 | if team['name'] == name: 40 | return team 41 | logging.critical("Нет команды с названием {}".format(name)) 42 | 43 | def dump(self): 44 | return info 45 | 46 | def get_delta_by_ip(self, ip): 47 | for team in delta: 48 | if team['ip'] == ip: 49 | return team 50 | logging.critical("Нет команды с IP {ip}".format(ip=ip)) 51 | 52 | def get_delta_by_name(self, name): 53 | for team in delta: 54 | if team['name'] == name: 55 | return team 56 | logging.critical("Нет команды с названием {name}".format(name=name)) 57 | 58 | def refresh(self, driver): 59 | global info 60 | if driver: 61 | driver.get(self.scoreboard) 62 | current_round = board_parser.get_current_round(driver, None) 63 | if self.round != current_round: 64 | new_info = board_parser.get_teams_info(driver, None) 65 | self.round = current_round 66 | self.__recalculate_delta(new_info) 67 | info = new_info 68 | return True 69 | else: 70 | return False 71 | else: 72 | new_soup = board_parser.get_soup_by_address(self.scoreboard) 73 | new_info = board_parser.get_teams_info(driver, new_soup) 74 | current_round = board_parser.get_current_round(driver, new_soup) 75 | if self.round != current_round: 76 | self.round = current_round 77 | self.__recalculate_delta(new_info) 78 | info = new_info.copy() 79 | return True 80 | else: 81 | return False 82 | 83 | def __recalculate_delta(self, new_info): 84 | global delta 85 | delta = [] 86 | for team_new in new_info: 87 | if self.ip: 88 | team_old = self.get_info_by_ip(team_new['ip']) 89 | else: 90 | team_old = self.get_info_by_name(team_new['name']) 91 | delta_services = {} 92 | for service_new, service_old in zip(team_new['services'], team_old['services']): 93 | name = service_new['name'] 94 | team_got_new_flags = service_new['flags']['got'] - \ 95 | service_old['flags']['got'] 96 | team_lost_new_flags = service_new['flags']['lost'] - \ 97 | service_old['flags']['lost'] 98 | delta_services[name] = { 99 | 'status': service_new['status'], 100 | 'title': service_new['title'], 101 | 'flags': { 102 | 'got': team_got_new_flags, 103 | 'lost': team_lost_new_flags 104 | }} 105 | 106 | if team_new['ip'] == self.ip or team_new['name'] == self.teamname: 107 | # * Уведомление о положении команды в рейтинге 108 | if team_old['place'] > team_new['place']: 109 | telegram_alert( 110 | 'PLACE', status='up', place_old=team_old['place'], place_new=team_new['place']) 111 | elif team_old['place'] < team_new['place']: 112 | telegram_alert( 113 | 'PLACE', status='down', place_old=team_old['place'], place_new=team_new['place']) 114 | 115 | # * Уведомление о статусе сервисов 116 | for service_new, service_old in zip(team_new['services'], team_old['services']): 117 | name = service_new['name'] 118 | title = service_new['title'] 119 | 120 | new_status = service_new['status'] 121 | old_status = service_old['status'] 122 | 123 | if soup: 124 | new_status = board_parser.return_status(new_status) 125 | old_status = board_parser.return_status(old_status) 126 | 127 | # * Если сервис не взлетел или не изменил своего состояния 128 | if new_status == old_status and new_status != 'UP': 129 | telegram_alert( 130 | 'STATUS', 131 | status='not change', 132 | now=new_status, 133 | service=name, 134 | title=title 135 | ) 136 | 137 | # * Если сервис взлетел или изменил состояние на UP 138 | if old_status != 'UP' and new_status == 'UP': 139 | telegram_alert( 140 | 'STATUS', 141 | status='up', 142 | now=new_status, 143 | service=name 144 | ) 145 | 146 | # * Если сервис не взлетел или изменил состояние не на UP 147 | if old_status != new_status and new_status != 'UP': 148 | telegram_alert( 149 | 'STATUS', 150 | status='down', 151 | now=new_status, 152 | service=name, 153 | title=title 154 | ) 155 | 156 | # * Уведомление о первой крови 157 | if int(delta_services[name]['flags']['lost']) != 0 and self.patch[name] == True: 158 | self.patch[name] = False 159 | telegram_alert('FB', service=name) 160 | 161 | # * Уведомление о прекращении потери флагов 162 | elif int(delta_services[name]['flags']['lost']) == 0 and self.patch[name] == False and new_status == 'UP': 163 | self.patch[name] = True 164 | telegram_alert('PATCH', service=name) 165 | 166 | delta.append({ 167 | 'round': self.round, 168 | 'name': team_new['name'], 169 | 'ip': team_new['ip'], 170 | 'place': team_new['place'], 171 | 'score': round(team_new['score'] - team_old['score'], 2), 172 | 'services': delta_services 173 | }) 174 | 175 | 176 | def telegram_alert(alert_type, **args): 177 | if alert_type == 'PLACE': 178 | requests.post(URL, json={ 179 | "message": "{} с *{}* на *{}* место".format('⬇ Наша команда спустилась' if args['status'] == 'down' else '⬆ Наша команда поднялась', args['place_old'], args['place_new']), 180 | "type": "markdown", 181 | "id": "parser" 182 | }) 183 | if alert_type == 'STATUS': 184 | if args['now'] == 'UP': 185 | simb = '*🟢 {} 🟢*\n' 186 | elif args['now'] == 'DOWN': 187 | simb = '*🔴 {} 🔴*\n' 188 | elif args['now'] == 'CORRUPT': 189 | simb = '*🔵 {} 🔵*\n' 190 | elif args['now'] == 'MUMBLE': 191 | simb = '*🟠 {} 🟠*\n' 192 | elif args['now'] == 'CHECK FAILED': 193 | simb = '*🟡 {} 🟡*\n' 194 | 195 | if args['status'] == 'down': 196 | otvet = "Сервису поплохело" 197 | if args['title']: 198 | otvet += "\n{}".format(args['title']) 199 | elif args['status'] == 'up': 200 | otvet = "Сервис снова жив" 201 | elif args['status'] == 'not change': 202 | otvet = "Сервису ВСЁ ЕЩЁ плохо" 203 | if args['title']: 204 | otvet += "\n *Check Error:* {}".format(args['title']) 205 | 206 | requests.post(URL, json={ 207 | "message": "{} {}".format(simb.format(args['service']), otvet), 208 | "type": "markdown", 209 | "id": "parser" 210 | }) 211 | if alert_type == 'FB': 212 | requests.post(URL, json={ 213 | "message": "🩸 Мы теряем флаги на сервисе *{}*".format(args['service']), 214 | "type": "markdown", 215 | "id": "parser" 216 | }) 217 | if alert_type == 'PATCH': 218 | requests.post(URL, json={ 219 | "message": "💎 Мы запатчили сервис *{}*".format(args['service']), 220 | "type": "markdown", 221 | "id": "parser" 222 | }) 223 | -------------------------------------------------------------------------------- /front/src/components/Echart.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 517 | 518 | 546 | --------------------------------------------------------------------------------