Начал переписывать

This commit is contained in:
saber-nyan 2019-09-24 00:36:27 +03:00
parent b8c21d3a86
commit e79f5cbce4
No known key found for this signature in database
GPG Key ID: 7B069E9C295E6B4A
33 changed files with 2382 additions and 2578 deletions

116
.gitignore vendored
View File

@ -1,21 +1,101 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-debug/
cmake-build-release/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
*.bak
/.tgitconfig
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
typings/
.npm
.eslintcache
.node_repl_history
*.tgz
.yarn-integrity
.env
.next
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
Session.vim
.netrwhist
*~
tags
[._]*.un~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
.org-id-locations
*_archive
*_flymake.*
/eshell/history
/eshell/lastdir
/elpa/
*.rel
/auto/
.cask/
dist/
flycheck_*.el
/server/
.projectile
.dir-locals.el
.fuse_hidden*
.directory
.Trash-*
.nfs*
$*$

View File

@ -0,0 +1,69 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="FORMATTER_TAGS_ENABLED" value="true" />
<option name="SOFT_MARGINS" value="90,120" />
<CssCodeStyleSettings>
<option name="HEX_COLOR_UPPER_CASE" value="true" />
</CssCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
</JSCodeStyleSettings>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="CoffeeScript">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="LESS">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SASS">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SCSS">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Stylus">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

9
.idea/discord.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="true" />
</component>
<component name="ProjectNotificationSettings">
<option name="askShowProject" value="false" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/packmate-frontend.iml" filepath="$PROJECT_DIR$/.idea/packmate-frontend.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -2,4 +2,7 @@ module.exports = {
presets: [
'@vue/app',
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
};

3028
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,26 +8,28 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.18.0",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.1",
"core-js": "^2.6.5",
"jquery": "^3.4.0",
"popper.js": "^1.15.0",
"sockjs-client": "^1.3.0",
"sockjs-client": "^1.4.0",
"vue": "^2.6.10",
"vue-infinite-loading": "^2.4.4",
"vue-router": "^3.0.3",
"vuex": "^3.0.1",
"vuex-persistedstate": "^2.5.4"
"vuex-persistedstate": "^2.5.4",
"vuex-shared-mutations": "^1.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.7.0",
"@vue/cli-plugin-eslint": "^3.7.0",
"@vue/cli-service": "^3.7.0",
"@babel/plugin-proposal-optional-chaining": "^7.6.0",
"@fortawesome/fontawesome-free": "^5.11.1",
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"vue-template-compiler": "^2.5.21"
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
@ -40,9 +42,9 @@
],
"rules": {
"indent": [
"warn",
"tab"
"off"
],
"no-mixed-spaces-and-tabs": "off",
"comma-dangle": [
"warn",
"always"

View File

@ -1,28 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<!--suppress HtmlUnknownTag -->
<html lang="ru">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon-16x16.png">
<link rel="manifest" href="<%= BASE_URL %>site.webmanifest">
<link rel="mask-icon" href="<%= BASE_URL %>safari-pinned-tab.svg" color="#44beff">
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono&amp;subset=cyrillic" rel="stylesheet">
<meta name="msapplication-TileColor" content="#afd9f0">
<meta name="theme-color" content="#afd9f0">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<title>Packmate</title>
<meta name="author" content="saber-nyan & serega6531">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
</head>
<body>
<noscript>
<strong>We're sorry but Packmate doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
<strong>Packmate не будет работать без JS, включи его!</strong>
</noscript>
<div id="app"></div>
</body>
</html>

View File

@ -1,11 +1,127 @@
<template>
<div id="app">
<router-view></router-view>
<Navbar></Navbar>
<div class="container-fluid">
<div class="row">
<transition name="fade" mode="out-in">
<router-view name="sidebar"></router-view>
</transition>
<!-- <Sidebar></Sidebar>-->
<main role="main" class="col-sm-9 ml-sm-auto px-4">
<transition name="fade" mode="out-in">
<router-view name="content"></router-view>
</transition>
</main>
</div>
</div>
<Settings></Settings>
</div>
</template>
<script>
import Navbar from './components/Navbar';
import Settings from './views/Settings';
export default {
components: {
Settings,
Navbar,
},
};
</script>
<style>
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v16/mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
html * {
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-size: .875rem;
overflow-y: scroll;
}
/*noinspection CssUnusedSymbol*/
.fade-enter-active, .fade-leave-active {
transition: opacity .3s;
}
/*noinspection CssUnusedSymbol*/
.fade-enter, .fade-leave-to {
opacity: 0;
}
[role="main"] {
padding-top: 55px; /* Space for fixed navbar */
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,97 +0,0 @@
<template>
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<form>
<div class="form-group">
<label for="inputServiceName">Имя сервиса</label>
<input type="text" class="form-control" id="inputServiceName" v-model="service.name">
</div>
<div class="form-group">
<label for="inputServicePort">Порт</label>
<input type="number" class="form-control" id="inputServicePort" v-model="service.port">
</div>
<button type="submit" @click.prevent="sendNewService()" class="btn btn-primary" style="margin-right: 10px;">
Сохранить
</button>
<button type="submit" @click.prevent="$emit('close')" class="btn btn-secondary">
Закрыть
</button>
</form>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "AddServiceModal",
data: function () {
return {
escapeKeyCallback: (e) => {
if (e.key === 'Escape') this.$emit('close');
},
service: {
name: '',
port: 0,
},
}
},
mounted() {
document.addEventListener('keyup', this.$data.escapeKeyCallback);
},
beforeDestroy() {
document.removeEventListener('keyup', this.$data.escapeKeyCallback);
},
methods: {
sendNewService() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.post('/service/', this.service)
.then(response => this.$emit('serviceAddDone', response.data))
.catch(error => console.error('Failed to add service', error));
},
},
}
</script>
<style scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0 auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
font-family: Helvetica, Arial, sans-serif;
}
</style>

View File

@ -1,102 +0,0 @@
<template>
<main class="col-md-9 ml-sm-auto col-lg-10 px-4">
<div class="custom-control custom-switch" style="margin-bottom: 10px;">
<input type="checkbox" class="custom-control-input" id="favSwitch" v-model="displayFavOnly">
<label class="custom-control-label" for="favSwitch">Только избранные</label>
</div>
<p>
<span style="background: rgba(224, 246, 255, 0.4); box-shadow: 0 0 5px 5px rgba(224, 246, 255, 0.4);">Ответ</span>, <span style="background: rgba(251, 233, 231, 0.4); box-shadow: 0 0 5px 5px rgba(251, 233, 231, 0.4);">запрос</span>.
</p>
<Packet v-for="packet in packets"
:key="packet.id"
:id="packet.id"
:content="packet.content"
:timestamp="packet.timestamp"
:isIncoming="packet.incoming"></Packet>
<infinite-loading @infinite="infiniteHandler" spinner="waveDots" ref="infiniteLoading"></infinite-loading>
</main>
</template>
<script>
import Packet from "@/components/Packet.vue";
import InfiniteLoading from "vue-infinite-loading";
import axios from "axios";
export default {
name: 'Content',
props: ['servicePort', 'streamId',],
data: function () {
return {
packets: [],
}
},
watch: {
'streamId': function () {
this.packets = [];
this.$refs.infiniteLoading.stateChanger.reset();
},
},
methods: {
infiniteHandler($state) {
if (!this.streamId) return $state.complete();
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
const packets = this.packets;
let startsFrom;
if (packets && packets.length && packets[packets.length - 1]) {
startsFrom = packets[packets.length - 1].id;
} else {
startsFrom = 0;
}
instance.post(`/packet/${this.streamId}`, {
direction: 'ASC',
startingFrom: startsFrom,
pageSize: parseInt(this.$store.state.pageSize),
}).then(response => {
const data = response.data;
if (data.length === 0) return $state.complete();
if (data[0] && this.packets[0] && data[0].id === this.packets[0].id) {
$state.complete();
} else {
this.packets.push(...data);
$state.loaded();
}
}).catch(error => {
console.error('Failed to load new portion of packets', error);
return $state.error();
})
},
},
computed: {
displayFavOnly: {
get() {
return this.$store.state.displayFavOnly;
},
set(value) {
this.$store.commit('setDisplayFavOnly', value);
this.$emit('displayFavChanged');
},
},
},
components: {Packet, InfiniteLoading,},
}
</script>
<style scoped>
main {
padding-top: 50px; /* Space for fixed navbar */
max-width: 80%;
margin-left: auto !important;
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<li class="nav-item" :class="{ 'active': subIsActive('/' + port) }">
<router-link class="nav-link" :to="{name: 'home', params: {servicePort: port}}">{{ name }} ({{ port }})
</router-link>
</li>
</template>
<script>
export default {
name: "CtfService",
methods: {
subIsActive(prefix) {
const paths = Array.isArray(prefix) ? prefix : [prefix,];
return paths.some(path => {
return this.$route.path.indexOf(path) === 0;
})
},
},
props: ['port', 'name',],
}
</script>
<style scoped>
</style>

View File

@ -1,52 +0,0 @@
<template>
<div class="dropdown-item" :style="'color:' + color"
@click="openPattern">
{{name}}: {{regex ? `/${value}/` : `'${value}'`}}
<div class="float-right">
<button @click.prevent.stop="deleteSelf()" class="btn btn-outline-danger btn-block btn-sm" type="button">x</button>
</div>
</div>
</template>
<script>
import axios from "axios";
import $ from "jquery";
export default {
name: "FlagPattern",
props: ['name', 'value', 'color', 'id', 'regex',],
methods: {
openPattern() {
this.$router.push({name: 'home', params: this.$route.params, query: {pId: this.id,},});
// $('.dropdown').trigger('hide.bs.dropdown')
$('.dropdown-toggle').dropdown('toggle')
},
deleteSelf() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.delete(`/pattern/${this.id}`)
.then(() => {
this.$destroy();
this.$el.parentNode.removeChild(this.$el); // FIXME: может сломать Vue, лучше бы это убрать...
})
.catch(error => console.error('failed to delete pattern', error));
},
},
}
</script>
<style scoped>
button {
position: relative;
top: -4px;
}
</style>

View File

@ -1,179 +1,106 @@
<template>
<nav class="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow navbar-expand-lg navbar-expand">
<!-- <router-link to="/" class="navbar-brand col-sm-3 col-md-2 mr-0">Packmate</router-link>-->
<div class="dropdown">
<form> <!-- Disable autohide -->
<button class="btn btn-dark dropdown-toggle" type="button" data-toggle="dropdown">
Паттерны
</button>
<div class="dropdown-menu">
<div class="input-group">
<input type="text" class="form-control" placeholder="Name" v-model="newPattern.name">
<input type="text" class="form-control" placeholder="Value" v-model="newPattern.value">
<input type="text" class="form-control" placeholder="#Color" v-model="newPattern.color">
<div class="input-group-append">
<button @click.prevent="createPattern()" class="btn btn-outline-primary" type="button">+
</button>
</div>
</div>
<div class="custom-control custom-switch" style="margin-left: 10px; margin-bottom: 8px;">
<input type="checkbox" class="custom-control-input"
id="isRegexCheckbox" v-model="newPattern.regex">
<label class="custom-control-label" for="isRegexCheckbox">Regex?</label>
</div>
<FlagPattern v-for="pattern in patterns"
:key="pattern.id"
:id="pattern.id"
:name="pattern.name"
:value="pattern.value"
:color="pattern.color"
:regex="pattern.regex"></FlagPattern>
</div>
</form>
<nav class="navbar navbar-dark navbar-expand fixed-top bg-dark flex-md-nowrap p-0 shadow">
<div class="navbar-brand col-sm-3 mr-0" style="cursor: pointer;">
Паттерны
<i class="fas fa-angle-down" style="margin-left: 0.5em"></i>
</div>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item" :class="{ 'active': !this.$route.params.servicePort }">
<router-link class="nav-link" to="/">Все</router-link>
</li>
<CtfService
v-for="service in ctfServices"
:key="service.port"
:port="service.port"
:name="service.name"></CtfService>
<li class="nav-item" style="margin-left: 10px;">
<button type="button" class="btn btn-link" @click="$emit('addServiceButtonClicked')">
<svg id="i-plus" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16"
fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M16 2 L16 30 M2 16 L30 16"></path>
</svg>
</button>
<div class="navbar-collapse collapse">
<ul class="navbar-nav px-3 mr-auto">
<li class="nav-item text-nowrap">
<router-link class="nav-link" to="/" exact>Все</router-link>
</li>
<NavbarService v-for="service in services"
:key="service.name"
:name="service.name"
:port="service.port"/>
</ul>
<div class="my-2 my-lg-0" style="margin-right: 10px;">
<button type="button" class="btn btn-link" @click="$emit('settingsButtonClicked')">
<svg id="i-settings" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="16" height="16"
fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M13 2 L13 6 11 7 8 4 4 8 7 11 6 13 2 13 2 19 6 19 7 21 4 24 8 28 11 25 13 26 13 30 19 30 19 26 21 25 24 28 28 24 25 21 26 19 30 19 30 13 26 13 25 11 28 8 24 4 21 7 19 6 19 2 Z"></path>
<circle cx="16" cy="16" r="4"></circle>
</svg>
</button>
<div class="my-2 my-lg-0 mr-3 navbar-cogs" style="cursor: pointer;"
@click.stop.prevent="showSettings">
<i class="fas fa-cogs"></i>
</div>
</div>
</nav>
</template>
<script>
import CtfService from "@/components/CtfService.vue";
import axios from "axios";
import $ from "jquery";
import FlagPattern from "@/components/FlagPattern.vue";
import NavbarService from './NavbarService';
export default {
name: "Navbar",
data: function () {
return {
ctfServices: [],
patterns: [],
newPattern: {
name: '',
value: '',
color: '',
regex: false,
export default {
name: 'Navbar',
data() {
return {
services: Array(),
};
},
mounted() {
this.updateServices();
},
methods: {
showSettings() {
this.$bvModal.show('settingsModal');
console.debug('Showing settings...');
},
updateServices() {
this.$http.get('service/')
.then(r => this.services = r.data)
.catch(e => {
this.$bvToast.toast(`Не удалось загрузить сервисы: ${e}`, {
title: 'Сбой',
variant: 'danger',
});
console.error('Failed to load services:', e);
});
},
}
},
methods: {
getCtfServices() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.get('/service/')
.then(response => (this.ctfServices = response.data))
.catch(error => console.error('Failed to load CTF services!', error))
},
getPatterns() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.get('/pattern/')
.then(response => (this.patterns = response.data))
.catch(error => console.error('Failed to load patterns!', error));
components: {
NavbarService,
},
createPattern() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.post('/pattern/', this.newPattern)
.then(this.getPatterns())
.catch(error => console.error('failed to create pattern', error));
},
},
mounted() {
this.getCtfServices();
this.getPatterns();
const dropdown = $('.dropdown');
dropdown.on('show.bs.dropdown', function () {
$(this).find('.dropdown-menu').first().stop(true, true).slideDown();
});
dropdown.on('hide.bs.dropdown', function () {
$(this).find('.dropdown-menu').first().stop(true, true).slideUp();
});
},
components: {FlagPattern, CtfService,},
}
};
</script>
<style scoped>
li {
margin-left: 10px;
.nav-link {
transition: all .3s;
}
svg {
position: absolute;
top: 50%;
bottom: 50%;
transform: translate(-50%, -50%);
.navbar-cogs > i {
transition: all .3s;
color: rgba(255, 255, 255, 0.7);
}
.dropdown-menu {
min-width: 300px;
.navbar-cogs > i:hover {
color: #FFF;
}
.input-group {
padding-left: 10px;
padding-right: 10px;
padding-bottom: 5px;
.navbar-brand {
padding-top: .5rem;
padding-bottom: .5rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
nav {
overflow-x: auto;
}
::-webkit-scrollbar {
height: 0.2em
}
::-webkit-scrollbar-button {
width: 0;
}
::-webkit-scrollbar-track-piece {
background: #F1F1F1
}
::-webkit-scrollbar-thumb {
background: #C1C1C1
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<router-link tag="li" class="nav-item text-nowrap edit-button" :to="{name:'stream', params: {servicePort: port}}">
<a class="nav-link">{{name}} #{{port}}</a> <!-- TODO: editing! -->
<a class="nav-link" style="cursor: pointer"> <!-- TODO -->
<i class="fas fa-pencil-alt"></i>
</a>
</router-link>
</template>
<script>
export default {
name: 'NavbarService',
props: {
name: String(),
port: Number(),
},
};
</script>
<style scoped>
.nav-link {
transition: all .3s;
}
.edit-button {
display: inherit;
}
</style>

View File

@ -1,97 +0,0 @@
<template>
<div class="packet" :class="{'packet-incoming': isIncoming}">
<div>#{{id}} at {{new Date(timestamp).toLocaleDateString('ru-RU', {month: '2-digit', day: '2-digit', hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
<button @click.prevent="copyRaw" class="btn btn-link">Copy HEX</button>
<button @click.prevent="copyText" class="btn btn-link">Copy text</button>
<button @click.prevent="copyPythonBytes" class="btn btn-link">Copy as Python bytes </button></div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-2 pb-2 mb-3 border-bottom">
<pre>{{ hexdata }}</pre>
</div>
</div>
</template>
<script>
export default {
name: 'Packet',
props: ['id', 'content', 'timestamp', 'isIncoming',],
methods: {
hexdump(buffer, blockSize, lineNumberBase) {
blockSize = parseInt(blockSize, 10) || 16;
lineNumberBase = parseInt(lineNumberBase, 10) || 10;
let lines = [];
const hex = "0123456789ABCDEF";
for (let b = 0; b < buffer.length; b += blockSize) {
let block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
let addr = ("0000000000" + b.toString(lineNumberBase)).slice(-10);
let codes = block.split('').map(ch => {
let code = ch.charCodeAt(0);
return " " + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
}).join("");
codes += " ..".repeat(blockSize - block.length);
// eslint-disable-next-line no-control-regex
let chars = block.replace(/[\x00-\x1F]/g, '.');
chars += " ".repeat(blockSize - block.length);
lines.push(addr + ":" + codes + " |" + chars + "|");
}
return lines.join("\n");
},
copyPythonBytes() {
const data = "b'" + atob(this.$props.content)
.split('')
.map((aChar) => {
return '\\x' + ('0' + aChar.charCodeAt(0).toString(16)).slice(-2);
})
.join('') + "'";
this.copyContent(data);
},
copyRaw() {
this.copyContent(Buffer.from(this.$props.content, 'base64').toString('hex'));
},
copyText() {
this.copyContent(atob(this.$props.content));
},
copyContent(data) {
const tempEl = document.createElement('textarea');
tempEl.value = data; // Chrome
tempEl.textContent = data; // Firefox
document.body.appendChild(tempEl);
tempEl.select();
const result = document.execCommand('copy');
document.body.removeChild(tempEl);
console.debug('Copy result is', result);
},
},
computed: {
hexdata() {
// const dataString = decodeURIComponent(escape(atob(this.$props.content)));
const dataString = atob(this.$props.content);
return this.hexdump(dataString, this.$store.state.hexdumpBlockSize, this.$store.state.hexdumpLineNumberBase);
},
},
}
</script>
<style scoped>
pre {
font-family: "Ubuntu Mono", "Lucida Console", monospace;
font-size: 100%;
color: black;
margin-bottom: 10px;
padding-bottom: 5px;
}
.packet {
background: rgba(224, 246, 255, 0.4);
box-shadow: 0 0 5px 5px rgba(224, 246, 255, 0.4);
}
.packet-incoming {
background: rgba(251, 233, 231, 0.4);
box-shadow: 0 0 5px 5px rgba(251, 233, 231, 0.4);
}
button {
padding: 0;
top: -0.15em;
position: relative;
margin-left: 5px;
}
</style>

View File

@ -1,147 +0,0 @@
<template>
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<form>
<div class="form-group">
<label for="inputApiUrl">URL бэкенда</label>
<input type="url" class="form-control" id="inputApiUrl" placeholder="URL" v-model="apiUrl">
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="inputApiLogin">Логин</label>
<input type="text" class="form-control" id="inputApiLogin" v-model="apiLogin">
</div>
<div class="form-group col-md-6">
<label for="inputApiPassword">Пароль</label>
<input type="password" class="form-control" id="inputApiPassword" v-model="apiPassword">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-5">
<label for="inputHexdumpBlockSize">Ширина блока HEX</label>
<input type="number" class="form-control" id="inputHexdumpBlockSize" v-model="hexdumpBlockSize">
</div>
<div class="form-group col-md-7">
<label for="inputHexdumpLineNumberBase">Номер строки</label>
<select id="inputHexdumpLineNumberBase" class="form-control" v-model="hexdumpLineNumberBase">
<option selected value="10">Десятичный</option>
<option value="16">Шестнадцатеричный</option>
</select>
</div>
</div>
<div class="form-group">
<label for="inputPageSize">Размер страницы</label>
<input type="number" class="form-control" id="inputPageSize" v-model="pageSize">
</div>
<button type="submit" @click.prevent="$emit('close')" class="btn btn-primary">
Закрыть
</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "SettingsModal",
data: function () {
return {
escapeKeyCallback: (e) => {
if (e.key === 'Escape') this.$emit('close');
},
}
},
mounted() {
document.addEventListener('keyup', this.$data.escapeKeyCallback);
},
beforeDestroy() {
document.removeEventListener('keyup', this.$data.escapeKeyCallback);
},
computed: {
apiUrl: {
get() {
return this.$store.state.apiUrl;
},
set(value) {
this.$store.commit('setApiUrl', value);
},
},
apiLogin: {
get() {
return this.$store.state.apiLogin;
},
set(value) {
this.$store.commit('setApiLogin', value);
},
},
apiPassword: {
get() {
return this.$store.state.apiPassword;
},
set(value) {
this.$store.commit('setApiPassword', value);
},
},
hexdumpBlockSize: {
get() {
return this.$store.state.hexdumpBlockSize;
},
set(value) {
this.$store.commit('setHexdumpBlockSize', value);
},
},
hexdumpLineNumberBase: {
get() {
return this.$store.state.hexdumpLineNumberBase;
},
set(value) {
this.$store.commit('setHexdumpLineNumberBase', value);
},
},
pageSize: {
get() {
return this.$store.state.pageSize;
},
set(value) {
this.$store.commit('setPageSize', value);
},
},
},
}
</script>
<style scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 500px;
margin: 0 auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
font-family: Helvetica, Arial, sans-serif;
}
</style>

View File

@ -1,222 +1,117 @@
<template>
<nav class="col-md-2 d-md-block bg-light sidebar">
<nav class="col-sm-3 d-none d-sm-block bg-light sidebar">
<div class="m-2">
<button type="button" class="btn btn-sm btn-outline-success">
<i class="fas fa-pause"></i> <!-- TODO! -->
</button>
<button type="button" class="btn btn-sm ml-1"
:class="this.$store.state.displayFavoritesOnly ? 'btn-danger' : 'btn-outline-danger'"
@click.stop.prevent="toggleFavorites">
<i class="fas fa-star"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-info" style="float: right;">
<i class="fas fa-angle-double-up"></i>
</button>
</div>
<div class="sidebar-sticky">
<ul class="nav flex-column">
<Stream
v-for="stream in streams"
:key="stream.id"
:id="stream.id"
:proto="stream.protocol"
:servicePort="stream.service.port"
:foundPatterns="stream.foundPatterns"
:startTime="stream.startTimestamp"
:endTime="stream.endTimestamp"
:favorite="stream.favorite"
:ttl="stream.ttl"></Stream>
<infinite-loading @infinite="infiniteHandler" spinner="waveDots" ref="infiniteLoading"></infinite-loading>
<!-- <li class="nav-item">-->
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
<!-- </li>-->
<!-- <li class="nav-item">-->
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
<!-- </li>-->
<!-- <li class="nav-item">-->
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
<!-- </li>-->
<!-- <li class="nav-item">-->
<!-- <a class="nav-link" href="#">SOME ITEM</a>-->
<!-- </li>-->
<SidebarStream v-for="stream in streams"
:key="stream.id"
:stream="stream"></SidebarStream>
<infinite-loading @infinite="infiniteLoadingHandler" ref="infiniteLoader"></infinite-loading>
</ul>
</div>
</nav>
</template>
<script>
import Stream from "@/components/Stream.vue";
import InfiniteLoading from "vue-infinite-loading";
import axios from "axios";
import SidebarStream from './SidebarStream';
export default {
name: 'Sidebar',
props: ['servicePort', 'streamId',],
data: function () {
return {
streams: [],
navigationKeysCallback: (e) => {
if (!e.ctrlKey) return;
if (e.key === 'ArrowUp') {
e.preventDefault();
const index = this.$data.streams.findIndex(e => e.id === this.$props.streamId);
const newStream = this.$data.streams[index - 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'home',
params: {servicePort: this.$props.servicePort, streamId: newId,},
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'ArrowDown') {
e.preventDefault();
const index = this.$data.streams.findIndex(e => e.id === this.$props.streamId);
const newStream = this.$data.streams[index + 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'home',
params: {servicePort: this.$props.servicePort, streamId: newId,},
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'Home') {
const newStream = this.$data.streams[0];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'home',
params: {servicePort: this.$props.servicePort, streamId: newId,},
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
} else if (e.key === 'End') {
const newStream = this.$data.streams[this.$data.streams.length - 1];
if (!newStream) return;
const newId = newStream.id;
this.$router.push({
name: 'home',
params: {servicePort: this.$props.servicePort, streamId: newId,},
});
document.getElementById(`stream-${newId}`).scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
},
}
},
watch: {
'servicePort': function () {
this.rerender();
export default {
name: 'Sidebar',
props: ['servicePort', 'streamId',],
data() {
return {
streams: [],
};
},
'$route.query.pId': function () {
this.rerender();
},
},
methods: {
infiniteHandler($state) {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
const streams = this.streams;
let startsFrom;
if (streams && streams.length && streams[streams.length - 1]) {
startsFrom = streams[streams.length - 1].id;
} else {
startsFrom = Number.MAX_SAFE_INTEGER; // FIXME -_-
}
instance.post(`/stream/${this.$props.servicePort || 'all'}`, {
direction: 'DESC',
startingFrom: startsFrom,
pageSize: parseInt(this.$store.state.pageSize),
pattern: this.$route.query.pId ? {id: this.$route.query.pId,} : null,
favorites: this.$store.state.displayFavOnly,
}).then(response => {
const data = response.data;
if (data.length === 0) return $state.complete();
if (data[0] && this.streams[0] && data[0].id === this.streams[0].id) { // FIXME: проверка довольно колхозная
$state.complete();
methods: {
infiniteLoadingHandler($state) {
const ourStreams = this.streams;
let startsFrom;
if (ourStreams?.length && ourStreams[ourStreams.length - 1]) {
startsFrom = ourStreams[ourStreams.length - 1].id;
} else {
this.streams.push(...data);
$state.loaded();
startsFrom = Number.MAX_SAFE_INTEGER; // FIXME: rewrite backend?
}
}).catch((error) => {
console.error('Failed to load new portion of streams', error);
return $state.error();
});
this.$http.post(`/stream/${this.$props.servicePort || 'all'}`, {
direction: 'DESC',
startingFrom: startsFrom,
pageSize: this.$store.state.pageSize,
pattern: null, // TODO
favorites: this.$store.state.displayFavoritesOnly,
}).then(r => {
const data = r.data;
if (data?.length === 0) return $state.complete();
if (data[0]?.id === this.streams[0]?.id) {
return $state.complete();
}
this.streams.push(...data);
return $state.loaded();
}).catch(e => {
console.error('Failed to load portion of streams:', e);
return $state.error();
});
},
toggleFavorites() {
this.$store.commit('toggleDisplayFavoritesOnly');
},
},
onGotNewStream(stream) {
if (this.$route.query.pId && !stream.foundPatterns.some(e => e.id === this.$route.query.pId)) {
return;
}
if (this.streams.length > this.$store.state.pageSize * 2) {
this.streams.pop();
}
this.streams.unshift(stream);
components: {
SidebarStream,
// InfiniteLoading,
},
rerender() {
this.streams = [];
this.$refs.infiniteLoading.stateChanger.reset();
},
},
components: {Stream, InfiniteLoading,},
mounted() {
document.addEventListener('keydown', this.$data.navigationKeysCallback);
},
beforeDestroy() {
document.removeEventListener('keydown', this.$data.navigationKeysCallback);
},
}
};
</script>
<style>
<style scoped>
.sidebar {
display: block !important;
flex: 0 0 16.666667%;
max-width: 20.666667%;
position: fixed !important;
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 40px 0 0 0 !important;
padding: 40px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
height: 100%;
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 40px);
height: calc(100vh - 85px);
/*padding-top: .5rem;*/
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@supports ((position: -webkit-sticky) or (position: sticky)) {
@supports (position: sticky) {
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<li class="nav-item">
<a class="nav-link" href="#">Huh?!</a>
</li>
</template>
<script>
export default {
name: 'SidebarStream',
};
</script>
<style scoped>
.nav-link {
font-weight: 500;
color: #333;
}
/*noinspection CssUnusedSymbol*/
.nav-link.active {
color: #007BFF;
}
</style>

View File

@ -1,106 +0,0 @@
<template>
<li :id="`stream-${id}`" class="nav-item" :class="{highlight: favorite0}" style="transition: all .5s;">
<router-link class="nav-link" active-class="active"
:to="{ name: 'home', params: { servicePort: servicePort, streamId: id}, query: $route.query }">
<button class="btn-text" @click.prevent="toggleStar">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32" width="16" height="16" fill="none" class="star" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2">
<path d="M16 2 L20 12 30 12 22 19 25 30 16 23 7 30 10 19 2 12 12 12 Z" style="transition: all .5s;" :fill="favorite0 ? 'red' : 'white'"></path>
</svg>
</button>
#{{id}} {{proto}} <span v-if="ttl">TTL {{ttl}}</span><br/>
{{new Date(startTime).toLocaleDateString('ru-RU', {month: '2-digit', day: '2-digit', hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
- {{new Date(endTime).toLocaleTimeString('ru-RU', {hour:'2-digit', minute: '2-digit', second: '2-digit'})}}
<span v-for="pattern in foundPatterns"
:key="pattern.id"
:style="'color:' + pattern.color"
>
{{pattern.name}}
</span>
</router-link>
</li>
</template>
<script>
import axios from "axios";
export default {
name: "Stream",
props: ['id', 'proto', 'startTime', 'endTime', 'servicePort', 'foundPatterns', 'favorite', 'ttl',],
data: function () {
return {
favorite0: this.favorite,
}
},
methods: {
toggleStar() {
const instance = axios.create({
baseURL: this.$store.state.apiUrl,
auth: {
username: this.$store.state.apiLogin,
password: this.$store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
});
instance.post(`/stream/${this.id}/${this.favorite ? 'unfavorite' : 'favorite'}`)
.then(() => this.favorite0 = !this.favorite0)
.catch(e => console.error('Failed to toggle favorite!', e));
},
},
}
</script>
<style scoped>
button {
padding: 0;
top: -0.1em;
position: relative;
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.btn-text {
border: none;
background-color: inherit;
/*padding: 14px 28px;*/
/*font-size: 16px;*/
cursor: pointer;
display: inline-block;
}
.star {
stroke: #bfbfbf;
transition: all .5s;
}
.star:hover {
stroke: red;
}
.highlight {
background: rgba(255, 119, 182, 0.3);
box-shadow: 0 0 1px 1px rgba(255, 119, 182, 0.3);
}
</style>

View File

@ -1,17 +1,50 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css'
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import Axios from 'axios';
import InfiniteLoading from 'vue-infinite-loading';
import App from './App.vue';
import router from './router';
import store from './store';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import '@fortawesome/fontawesome-free/css/all.css';
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
Vue.use(InfiniteLoading, {
slots: {
'no-results': 'Нет результатов...',
'no-more': 'Данных больше нет',
error: 'Произошла ошибка!',
errorBtnText: 'Повторить',
},
props: {
spinner: 'waveDots',
},
});
const axiosInstance = Axios.create({
// baseURL: '/api', // TODO: edit for release!
baseURL: 'http://192.168.79.131:8080/api',
auth: {
username: store.state.apiLogin,
password: store.state.apiPassword,
},
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
timeout: 3000,
withCredentials: true,
maxContentLength: 20000,
});
// noinspection JSUnusedGlobalSymbols
Vue.prototype.$http = axiosInstance;
// noinspection JSUnusedGlobalSymbols
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
// TODO: вынести наконец инстанс axios сюда!

View File

@ -1,16 +1,32 @@
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Vue from 'vue';
import Router from 'vue-router';
import ContentStream from './views/ContentStream';
import Sidebar from './components/Sidebar';
Vue.use(Router);
export default new Router({
routes: [
// {
// path: '/',
// name: 'nothing',
// component: ContentNothing,
// },
{
path: '/:servicePort?/:streamId?',
name: 'home',
component: Home,
name: 'stream',
props: true,
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "stream" */ './views/ContentStream'),
// component: ContentStream,
components: {
sidebar: Sidebar,
content: ContentStream,
},
},
],
})
linkActiveClass: 'active',
});

View File

@ -1,27 +1,38 @@
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import Vue from 'vue';
import Vuex from 'vuex';
import createMutationsSharer from 'vuex-shared-mutations';
import createPersistedState from 'vuex-persistedstate';
Vue.use(Vuex);
// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
export default new Vuex.Store({
state: {
apiUrl: 'http://127.0.0.1/api',
apiLogin: 'BinaryBears',
apiPassword: '123456',
hexdumpBlockSize: 16,
hexdumpLineNumberBase: 10,
pageSize: 25,
displayFavOnly: false,
displayFavoritesOnly: false,
},
mutations: {
setApiUrl: (state, payload) => state.apiUrl = payload,
setApiLogin: (state, payload) => state.apiLogin = payload,
setApiPassword: (state, payload) => state.apiPassword = payload,
setHexdumpBlockSize: (state, payload) => state.hexdumpBlockSize = payload,
setHexdumpLineNumberBase: (state, payload) => state.hexdumpLineNumberBase = payload,
setPageSize: (state, payload) => state.pageSize = payload,
setDisplayFavOnly: (state, payload) => state.displayFavOnly = payload,
setApiLogin: (s, p) => s.apiLogin = p,
setApiPassword: (s, p) => s.apiPassword = p,
setHexdumpBlockSize: (s, p) => s.hexdumpBlockSize = p,
setHexdumpLineNumberBase: (s, p) => s.hexdumpLineNumberBase = p,
setPageSize: (s, p) => s.pageSize = p,
// eslint-disable-next-line no-unused-vars
toggleDisplayFavoritesOnly: (s, p) => s.displayFavoritesOnly = !s.displayFavoritesOnly,
},
plugins: [createPersistedState(),],
})
actions: {},
plugins: [
createPersistedState(),
createMutationsSharer({
// eslint-disable-next-line no-unused-vars
predicate: (mutation, state) => {
console.debug('Got mutation:', mutation);
return mutation?.type !== 'setDisplayFavoritesOnly';
},
}),
],
});

View File

@ -0,0 +1,16 @@
<template>
<div>
Some stream #{{ streamId }} at port #{{ servicePort }}
</div>
</template>
<script>
export default {
props: ['servicePort', 'streamId',],
name: 'ContentStream',
};
</script>
<style scoped>
</style>

View File

@ -1,114 +0,0 @@
<template>
<div>
<Navbar @settingsButtonClicked="showSettingsModal = true" @addServiceButtonClicked="showAddServiceModal = true" ref="navbar"></Navbar>
<div class="container-fluid">
<div class="row">
<Sidebar ref="sidebar" :servicePort="servicePort" :streamId="streamId"></Sidebar>
<!--suppress HtmlDeprecatedTag -->
<Content :servicePort="servicePort" :streamId="streamId" @displayFavChanged="reloadSidebar()"></Content>
</div>
</div>
<transition name="modal">
<SettingsModal v-if="showSettingsModal" @close="showSettingsModal = false"></SettingsModal>
</transition>
<transition name="modal">
<AddServiceModal v-if="showAddServiceModal"
@serviceAddDone="$refs.navbar.ctfServices.push($event); showAddServiceModal = false"
@close="showAddServiceModal = false"></AddServiceModal>
</transition>
</div>
</template>
<script>
import SettingsModal from "@/components/SettingsModal.vue";
import Navbar from "@/components/Navbar.vue";
import Sidebar from "@/components/Sidebar.vue";
import Content from "@/components/Content.vue";
import AddServiceModal from "@/components/AddServiceModal.vue";
import SockJS from "sockjs-client";
export default {
name: 'Home',
data: function () {
return {
showSettingsModal: false,
showAddServiceModal: false,
socket: null,
navbarTimer: null,
}
},
props: ['servicePort', 'streamId',],
methods: {
reloadSidebar() {
this.$refs.sidebar.rerender();
},
reloadNavbar() {
this.$refs.navbar.getCtfServices();
this.$refs.navbar.getPatterns();
},
connectWs() {
const wsUrl = this.$store.state.apiUrl + '/ws';
// FIXME: нельзя посылать заголовок авторизации (может появиться окно с авторизацией, что не круто),
// FIXME: нужно либо убрать его на эндпоинте, либо использовать другой способ
// FIXME: (https://groups.google.com/forum/#!topic/sockjs/4gmZy8XZFIY)
this.socket = new SockJS(wsUrl);
this.socket.onopen = function () {
console.info('WS connected');
};
this.socket.onclose = function (ev) {
console.info('WS disconnected', ev.code, ev.reason);
if (ev.code === 1008) { // FIXME: выключить security timeout (This connection was established under an authenticated HTTP session that has ended.)
console.info('Security timeout, reconnecting...');
this.connectWs();
}
};
this.socket.onmessage = ev => {
const parsed = JSON.parse(ev.data);
const currentPort = parseInt(this.$route.params.servicePort, 10);
if (!(currentPort === undefined || currentPort === parsed.service.port)) {
return;
}
if (!this.$store.state.displayFavOnly) {
this.$refs.sidebar.onGotNewStream(parsed);
}
};
this.socket.onerror = function (ev) {
console.warn('WS err', ev);
};
},
},
mounted() {
this.connectWs();
this.navbarTimer = setInterval(this.reloadNavbar, 5000); // TODO: некрасиво, лучше ловить через WS
},
beforeDestroy() {
this.socket.close();
clearInterval(this.navbarTimer);
},
components: {
AddServiceModal,
SettingsModal,
Navbar,
Sidebar,
Content,
},
}
</script>
<style scoped>
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

102
src/views/Settings.vue Normal file
View File

@ -0,0 +1,102 @@
<!--suppress JSUnresolvedVariable -->
<template>
<b-modal id="settingsModal" title="Настройки" centered scrollable ok-only>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="Логин для подключения к API"
label="Логин"
label-for="settings-apiLogin">
<b-form-input id="settings-apiLogin" v-model="apiLogin"></b-form-input>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="Пароль для подключения к API"
label="Пароль"
label-for="settings-apiPassword">
<b-form-input type="password" id="settings-apiPassword" v-model="apiPassword"></b-form-input>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="Количество байт для отображения в бинарном представлении пакета"
label="Ширина блока HEX"
label-for="settings-hexdumpBlockSize">
<b-form-input type="number" id="settings-hexdumpBlockSize" v-model.number="hexdumpBlockSize"></b-form-input>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="Система счисления, используемая в номерах строк бинарного представления пакета"
label="Нумерация строк"
label-for="settings-hexdumpLineNumberBase">
<b-form-select id="settings-hexdumpLineNumberBase" v-model="hexdumpLineNumberBase">
<option :value="10" selected>Десятичная</option>
<option :value="16">Шестнадцатеричная</option>
</b-form-select>
</b-form-group>
<b-form-group
label-cols-sm="4"
label-cols-lg="3"
description="Количество стримов для загрузки за раз"
label="Размер страницы"
label-for="settings-pageSize">
<b-form-input type="number" id="settings-pageSize" v-model.number="pageSize"></b-form-input>
</b-form-group>
</b-modal>
</template>
<script>
export default {
name: 'Settings',
computed: {
apiLogin: {
get() {
return this.$store.state.apiLogin;
},
set(v) {
this.$store.commit('setApiLogin', v);
this.$http.defaults.auth.username = v;
},
},
apiPassword: {
get() {
return this.$store.state.apiPassword;
},
set(v) {
this.$store.commit('setApiPassword', v);
this.$http.defaults.auth.password = v;
},
},
hexdumpBlockSize: {
get() {
return this.$store.state.hexdumpBlockSize;
},
set(v) {
this.$store.commit('setHexdumpBlockSize', v);
},
},
hexdumpLineNumberBase: {
get() {
return this.$store.state.hexdumpLineNumberBase;
},
set(v) {
this.$store.commit('setHexdumpLineNumberBase', v);
},
},
pageSize: {
get() {
return this.$store.state.pageSize;
},
set(v) {
this.$store.commit('setPageSize', v);
},
},
},
};
</script>
<style scoped>
</style>

View File

@ -1,3 +1,3 @@
module.exports = {
publicPath: '',
lintOnSave: false,
};