feat: complete basic functions

Support http,dns and mysql connection. Support custom http, dns response. Support dns rebinding.
Support mysql load local files and jdbc deserialize exploit.
This commit is contained in:
Li4n0
2021-04-21 18:55:16 +08:00
parent e9db8ddb24
commit 3559894acc
114 changed files with 41617 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
.DS_Store
node_modules
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+24
View File
@@ -0,0 +1,24 @@
# revsuit-frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}#add-rule[data-v-0f53e934],#add-rule[data-v-56172c6c],#add-rule[data-v-d55a8788]{margin-bottom:10px}
+1
View File
@@ -0,0 +1 @@
body[data-v-1963d804],html[data-v-1963d804]{height:100%;margin:0}.fade-transform-enter-active[data-v-1963d804],.fade-transform-leave-active[data-v-1963d804]{transition:all .3s;opacity:0}.fade-transform-enter[data-v-1963d804],.fade-transform-leave[data-v-1963d804]{opacity:0}.fade-transform-enter-to[data-v-1963d804]{opacity:0}.ant-menu-item>span>a[data-v-1963d804]{color:hsla(0,0%,100%,.65)}.ant-menu-item-selected>span>a[data-v-1963d804]{color:#fff}#nav[data-v-1963d804]{height:100%}#nav .trigger[data-v-1963d804]{font-size:18px;line-height:64px;padding:0 24px;cursor:pointer;transition:color .3s}#nav .trigger[data-v-1963d804]:hover{color:#1890ff}#nav .logo[data-v-1963d804]{height:32px;background:#0a1d2d;margin:16px;text-align:center;font-size:1.2rem;color:#fff;padding-bottom:5px;border-bottom:2px solid #b6befa}
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>revsuit-frontend</title><link href="css/about.css" rel="prefetch"><link href="js/about.js" rel="prefetch"><link href="css/app.css" rel="preload" as="style"><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but revsuit-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
package frontend
import (
"embed"
)
//go:embed dist
var FS embed.FS
+27246
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
{
"name": "revsuit-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"ant-design-vue": "^1.2.4",
"axios": "^0.21.1",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-cli-plugin-ant-design": "^1.0.1",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<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="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+134
View File
@@ -0,0 +1,134 @@
<template>
<a-layout id="nav">
<a-layout-sider v-model="collapsed" :trigger="null" collapsible>
<div class="logo"><b>R</b><span v-if="!collapsed"><b>ev</b>Suit</span></div>
<a-menu theme="dark" mode="inline" :selectedKeys="[this.$route.path]" :open-keys.sync='openKeys'>
<!-- <a-menu-item key="/">-->
<!-- <router-link to="/">-->
<!-- <a-icon type="dashboard"/>-->
<!-- <span>Dashboard</span>-->
<!-- </router-link>-->
<!-- </a-menu-item>-->
<a-sub-menu key="logs">
<span slot="title"><a-icon type="bar-chart"/><span>Logs</span></span>
<a-menu-item key="/logs/http">
<router-link to="/logs/http">HTTP Logs</router-link>
</a-menu-item>
<a-menu-item key="/logs/dns">
<router-link to="/logs/dns">DNS Logs</router-link>
</a-menu-item>
<a-menu-item key="/logs/mysql">
<router-link to="/logs/mysql">MySQL Logs</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="rules">
<span slot="title"><a-icon type="radar-chart"/><span>Rules</span></span>
<a-menu-item key="/rules/http">
<router-link to="/rules/http">HTTP Rules</router-link>
</a-menu-item>
<a-menu-item key="/rules/dns">
<router-link to="/rules/dns">DNS Rules</router-link>
</a-menu-item>
<a-menu-item key="/rules/mysql">
<router-link to="/rules/mysql">MySQL Rules</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0">
<a-icon
class="trigger"
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click="() => (collapsed = !collapsed)"
/>
</a-layout-header>
<a-layout-content
:style="{ margin: '24px 16px', padding: '24px', borderRadius: '20px',background: '#fff', minHeight: 'initial' }"
>
<transition name="fade-transform">
<router-view></router-view>
</transition>
</a-layout-content>
</a-layout>
<Auth></Auth>
</a-layout>
</template>
<script>
import Auth from '@/components/Auth'
export default {
data() {
return {
collapsed: false,
openKeys: ['logs', "rules"],
};
},
components: {
Auth
}
};
</script>
<style scoped>
html, body {
height: 100%;
margin: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .3s;
opacity: 0;
}
.fade-transform-enter {
opacity: 0;
}
.fade-transform-leave {
opacity: 0;
}
.fade-transform-leave-to {
}
.fade-transform-enter-to {
opacity: 0;
}
.ant-menu-item > span > a {
color: rgba(255, 255, 255, 0.65);
}
.ant-menu-item-selected > span > a {
color: white;
}
#nav {
height: 100%;
}
#nav .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#nav .trigger:hover {
color: #1890ff;
}
#nav .logo {
height: 32px;
background: #0a1d2d;
margin: 16px;
text-align: center;
font-size: 1.2rem;
color: white;
padding-bottom: 5px;
border-bottom: 2px solid #b6befa;
}
</style>
+17
View File
@@ -0,0 +1,17 @@
import axios from 'axios'
const service = axios.create({
baseURL: '/revsuit/api/', // api的base_url
timeout: 5000 // request timeout
})
service.interceptors.request.use(function (config) {
const token = localStorage.getItem("token")
if (token !== null) {
config.headers['Token'] = token
}
// 在发送请求之前做些什么
return config
})
export default service
+11
View File
@@ -0,0 +1,11 @@
import request from './index'
export function ping() {
return request({
url: '/ping',
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+34
View File
@@ -0,0 +1,34 @@
import request from './index'
export function getHttpRecord(params) {
return request({
url: '/record/http',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getDnsRecord(params) {
return request({
url: '/record/dns',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getMysqlRecord(params) {
return request({
url: '/record/mysql',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+101
View File
@@ -0,0 +1,101 @@
import request from './index'
export function getHttpRule(params) {
return request({
url: '/rule/http',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function upsertHttpRule(data) {
return request({
url: '/rule/http',
data: data,
method: 'post',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function deleteHttpRule(data) {
return request({
url: '/rule/http',
data: data,
method: 'delete',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getDnsRule(params) {
return request({
url: '/rule/dns',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function upsertDnsRule(data) {
return request({
url: '/rule/dns',
data: data,
method: 'post',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function deleteDnsRule(data) {
return request({
url: '/rule/dns',
data: data,
method: 'delete',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getMysqlRule(params) {
return request({
url: '/rule/mysql',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function upsertMysqlRule(data) {
return request({
url: '/rule/mysql',
data: data,
method: 'post',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function deleteMysqlRule(data) {
return request({
url: '/rule/mysql',
data: data,
method: 'delete',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+63
View File
@@ -0,0 +1,63 @@
<template>
<div>
<a-modal
title="Auth with token"
:visible="visible"
:confirm-loading="confirmLoading"
@cancel="cancel"
@ok="auth"
>
<a-input v-model.lazy="token" placeholder="Your token"/>
</a-modal>
</div>
</template>
<script>
import {store} from "@/main";
import {ping} from "@/api/ping"
export default {
data() {
return {
confirmLoading: false,
token: localStorage.getItem("token")
};
},
computed: {
visible: function () {
return !store.authed
},
},
watch: {
token: function (val) {
localStorage.setItem("token", val)
}
},
methods: {
auth() {
this.confirmLoading = true;
ping().then(() => {
store.authed = true;
this.confirmLoading = false;
}).catch(e => {
if (e.response.status === 403) {
this.$notification.error({
message: 'Wrong token',
description:
'Your token is wrong, please check your server config file.',
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 2.5
});
}
this.confirmLoading = false;
})
},
cancel() {
store.authed = true;
}
},
};
</script>
+103
View File
@@ -0,0 +1,103 @@
<template>
<div>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="Name" :rules="rules.name" prop="name">
<a-input v-model="form.name"
placeholder="Please enter rule name"
:readOnly="readOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item :rules="rules.flagFormat" prop="flag_format">
<span slot="label">
Flag Format&nbsp;
<a-tooltip title="1. Only when the request contains content that satisfies the flag format, the request will be captured.
2. Please use regular expression syntax.
3. The character '*' means to capture all requests.
4. Advanced usage: When the format uses grouping, the platform will only notify the user or push to the client when the first group appears for the first time.">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input
v-model="form.flag_format"
style="width: 100%"
placeholder="please enter flag format"
:readOnly="readOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item prop="rank">
<span slot="label">
Rank
<a-tooltip title="When request match multiple rules, high-rank rules will be matched first">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input-number style="width: 100%"
v-model="form.rank"
v-decorator="['rank']"
:disabled="readOnly"
placeholder="0"
>
</a-input-number>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-model-item>
<div class="ant-form-item-label">
<label for="push-to-client">Push to Client
<a-tooltip placement="topLeft" title="Whether push to client when capture flag with this rule.">
<a-icon type="question-circle"/>
</a-tooltip>
</label>
</div>
<a-switch v-model="form.push_to_client" id="push-to-client" :disabled="readOnly"/>
</a-form-model-item>
</a-col>
<a-col :span="12">
<a-form-model-item>
<div class="ant-form-item-label">
<label for="notice">Notice
<a-tooltip placement="topLeft" title="Whether notice with bot when capture flag with this rule.">
<a-icon type="question-circle"/>
</a-tooltip>
</label>
</div>
<a-switch v-model="form.notice" id="notice" :disabled="readOnly"/>
</a-form-model-item>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: "BasicRule",
data() {
return {
rules: {
name: [
{required: true, message: 'Please input rule name', trigger: 'blur'},
],
flagFormat: [
{required: true, message: 'Please input flag format', trigger: 'blur'},
]
},
}
},
props: ['form', 'readOnly']
}
</script>
<style scoped>
</style>
+17
View File
@@ -0,0 +1,17 @@
import Vue from 'vue';
import Antd from 'ant-design-vue';
import App from './App';
import router from "@/router";
import 'ant-design-vue/dist/antd.css';
import '@/utils/index'
Vue.config.productionTip = false;
Vue.use(Antd);
export const store = Vue.observable({authed: localStorage.getItem("token")})
/* eslint-disable no-new */
new Vue({
router: router,
render: h => h(App)
}).$mount('#app')
+4
View File
@@ -0,0 +1,4 @@
import Vue from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
Vue.use(Antd)
+49
View File
@@ -0,0 +1,49 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/logs/http',
name: 'HttpLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Http')
},
{
path: '/logs/dns',
name: 'DnsLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Dns')
},
{
path: '/logs/mysql',
name: 'MysqlLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql')
},
{
path: '/rules/http',
name: 'HttpRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Http')
},
{
path: '/rules/dns',
name: 'DnsRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Dns')
},
{
path: '/rules/mysql',
name: 'MysqlRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql')
}
]
const router = new VueRouter({
routes
})
export default router
+20
View File
@@ -0,0 +1,20 @@
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
}
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
</div>
</template>
<script>
// @ is an alias to /src
export default {
name: 'Home',
components: {
}
}
</script>
+174
View File
@@ -0,0 +1,174 @@
<template>
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
>
Search
</a-button>
<a-button size="small" style="width: 90px" @click="() =>{
clearFilters();
delete filters[column.dataIndex];
fetch()
}">
Reset
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="time" slot-scope="time">
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
</span>
</a-table>
</template>
<style>
.gray-table-row {
background-color: #f5f5f5;
}
</style>
<script>
import {getDnsRecord} from '@/api/record'
import {store} from '@/main'
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'REQUEST TIME',
dataIndex: 'request_time',
key: 'request_time',
scopedSlots: {customRender: 'time'},
},
{
title: 'RULE',
dataIndex: 'rule_name',
key: 'rule_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG',
dataIndex: 'flag',
key: 'flag',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'DOMAIN',
dataIndex: 'domain',
key: 'domain',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'REMOTE IP',
key: 'remote_ip',
dataIndex: 'remote_ip',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'IP AREA',
key: 'ip_area',
dataIndex: 'ip_area'
}
];
export default {
name: 'DnsLogs',
data() {
return {
data: [],
pagination: {current: 1},
filters: {},
order: "desc",
loading: false,
columns,
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getDnsRecord(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
console.error(e)
}
})
}
},
mounted() {
this.fetch({page: "1"});
},
}
</script>
+205
View File
@@ -0,0 +1,205 @@
<template>
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
>
Search
</a-button>
<a-button size="small" style="width: 90px" @click="() =>{
clearFilters();
delete filters[column.dataIndex];
fetch()
}">
Reset
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="time" slot-scope="time">
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
</span>
<span slot="method" slot-scope="method">
<a-tag
:color="colors[method]"
>
{{ method.toUpperCase() }}
</a-tag>
</span>
<code slot="expandedRowRender" slot-scope="record" style="margin: 0">
<b style="color: gray">RAW REQUEST:</b><br>
<hr/>
<span style="white-space: pre-line">{{ record.raw_request }}</span>
</code>
</a-table>
</template>
<style>
.gray-table-row {
background-color: #f5f5f5;
}
</style>
<script>
import {getHttpRecord} from '@/api/record'
import {store} from '@/main'
const colors = {
"GET": "green",
"POST": "red",
"HEAD": "pink",
"PUT": "geekblue",
"OPTIONS": "cyan",
"DELETE": "purple",
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'REQUEST TIME',
dataIndex: 'request_time',
key: 'request_time',
scopedSlots: {customRender: 'time'},
},
{
title: 'RULE',
dataIndex: 'rule_name',
key: 'rule_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG',
dataIndex: 'flag',
key: 'flag',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'METHOD',
dataIndex: 'method',
key: 'method',
scopedSlots: {
customRender: 'method',
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'PATH',
dataIndex: 'path',
key: 'path',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'REMOTE IP',
key: 'remote_ip',
dataIndex: 'remote_ip',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'IP AREA',
key: 'ip_area',
dataIndex: 'ip_area'
}
];
export default {
name: 'HttpLogs',
data() {
return {
data: [],
pagination: {current: 1},
filters: {},
loading: false,
columns,
colors
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getHttpRecord(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
console.error(e)
}
})
}
},
mounted() {
this.fetch({page: "1"});
},
}
</script>
+232
View File
@@ -0,0 +1,232 @@
<template>
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
>
<div v-if="record.files.length" slot="expandedRowRender" slot-scope="record" style="margin: 0">
<b v-if="record.files.length" style="color: gray">FILES:</b><br>
<a v-for="file in record.files" :key="file.name+record.id" :href="'/revsuit/api/file/mysql/'+file.id" target="_blank">{{ file.name }} </a>
</div>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
>
Search
</a-button>
<a-button size="small" style="width: 90px" @click="() =>{
clearFilters();
delete filters[column.dataIndex];
fetch()
}">
Reset
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="time" slot-scope="time">
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
</span>
<span slot="loadData" slot-scope="loadData">
<a-tag v-if="loadData"
color="green"
>True</a-tag><a-tag v-else color="red">False</a-tag>
</span>
<span slot="fileNum" slot-scope="files">
<a-tag v-if="files.length>=3"
color="purple"
>{{ files.length }}</a-tag>
<a-tag v-else :color="colors[files.length]">
{{ files.length }}
</a-tag>
</span>
</a-table>
</template>
<style>
.gray-table-row {
background-color: #f5f5f5;
}
</style>
<script>
import {getMysqlRecord} from '@/api/record'
import {store} from '@/main'
const colors = [
"geekblue",
"blue",
"pink",
]
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'REQUEST TIME',
dataIndex: 'request_time',
key: 'request_time',
scopedSlots: {customRender: 'time'},
},
{
title: 'RULE',
dataIndex: 'rule_name',
key: 'rule_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG',
dataIndex: 'flag',
key: 'flag',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'USER',
dataIndex: 'username',
key: 'username',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'LOAD DATA',
dataIndex: 'load_local_data',
key: 'load_local_data',
scopedSlots: {
customRender: "loadData",
}
},
{
title: 'FILE NUM',
dataIndex: 'files',
key: 'files',
scopedSlots: {
customRender: "fileNum",
}
},
{
title: 'CLIENT NAME',
dataIndex: 'client_name',
key: 'client_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'CLIENT OS',
dataIndex: 'client_os',
key: 'client_os',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'REMOTE IP',
key: 'remote_ip',
dataIndex: 'remote_ip',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'IP AREA',
key: 'ip_area',
dataIndex: 'ip_area'
}
];
export default {
name: 'MysqlLogs',
data() {
return {
data: [],
pagination: {current: 1},
filters: {},
order: "desc",
loading: false,
columns,
colors
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getMysqlRecord(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
console.error(e)
}
})
}
},
mounted() {
this.fetch({page: "1"});
},
}
</script>
+429
View File
@@ -0,0 +1,429 @@
<template xmlns:a-col="http://www.w3.org/1999/html">
<div>
<a-button id="add-rule" type="primary" @click="addRule">
<a-icon type="plus"/>
New Rule
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' Dns rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@close="closeDrawer"
>
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
<BasicRule :form="form" :readOnly="formReadOnly"/>
<a-row :gutter="24">
<a-col :span="12">
<a-form-model-item label="Type" :rules="rules.type">
<a-select style="width: 100%"
v-model="form.type"
v-decorator="['type']"
:disabled="formReadOnly"
placeholder="A"
>
<a-select-option :value=1>A</a-select-option>
<a-select-option :value=28>AAAA</a-select-option>
<a-select-option :value=5>CNAME</a-select-option>
<a-select-option :value=16>TXT</a-select-option>
<a-select-option :value=2>NS</a-select-option>
<a-select-option :value=99>REBINDING</a-select-option>
</a-select>
</a-form-model-item>
</a-col>
<a-col :span="12">
<a-form-model-item label="TTL" :rules="rules.ttl">
<a-input-number style="width: 100%"
v-model="form.ttl"
v-decorator="['ttl']"
:disabled="formReadOnly"
placeholder="10"
:value="form.type === 99 ? form.ttl = 0 : null"
>
</a-input-number>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="Value" :rules="rules.value">
<a-input
v-model="form.value"
style="width: 100%"
:placeholder="form.type === 99 ?
'8.8.8.8,127.0.0.1' : 'please enter resolve value'
"
:readOnly="formReadOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
Cancel
</a-button>
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
Submit
</a-button>
</div>
</a-drawer>
<!-- rule table -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
>
Search
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="rank" slot-scope="rank">
<a-tag
:color="'#'+(0x2db7f5+rank*80).toString(16)"
>
{{ rank }}
</a-tag>
</span>
<span slot="type" slot-scope="type">
<a-tag
:color="colors[type]"
>
{{ resolveTypes[type] }}
</a-tag>
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
<span slot="valueRender" slot-scope="values">
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span>
<span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="-->
<!-- color: #67C23A;-->
<!-- background-color: transparent;-->
<!-- border-color: #67C23A;-->
<!-- text-shadow: none;-->
<!-- margin-right: 10px;-->
<!--" size="small" ghost>View</a-button>-->
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteRule(record,index)"
>
<a-button type="danger" size="small" ghost>Delete</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</template>
<style scoped>
#add-rule {
margin-bottom: 10px;
}
</style>
<script>
import {getDnsRule, upsertDnsRule, deleteDnsRule} from '@/api/rule'
import {store} from '@/main'
import BasicRule from "@/components/BasicRule";
const VIEW = "View"
const EDIT = "Edit"
const CREATE = "Create"
const colors = {
1: "geekblue",
28: "green",
5: "red",
16: "pink",
2: "purple",
99: "orange",
}
const resolveTypes = {
1: "A",
28: "AAAA",
5: "CNAME",
16: "TXT",
2: "NS",
99: "RB",
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
},
{
title: 'RANK',
dataIndex: 'rank',
key: 'rank',
scopedSlots: {
customRender: 'rank',
},
},
{
title: 'TYPE',
dataIndex: 'type',
key: 'type',
scopedSlots: {
customRender: 'type',
},
},
{
title: 'TTL',
dataIndex: 'ttl',
key: 'ttl',
},
{
title: 'VALUE',
dataIndex: 'value',
key: 'value',
scopedSlots: {
customRender: 'valueRender',
}
},
{
title: 'PUSH TO CLIENT',
dataIndex: 'push_to_client',
key: 'push_to_client',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'NOTICE',
dataIndex: 'notice',
key: 'notice',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'Action',
key: 'action',
scopedSlots: {customRender: 'action'},
},
];
const rules = {
value: [{required: false, message: "please enter resolve value"}],
type: [{required: false, message: "please enter resolve type"}],
ttl: [{required: false, message: "please enter resolve ttl"}]
}
export default {
name: 'DnsRules',
data() {
return {
data: [],
formVisible: false,
pagination: {current: 1},
filters: {},
loading: false,
columns,
colors,
resolveTypes,
form: {},
rules: rules,
formReadOnly: false,
formAction: "", // View ,Create or Edit
}
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getDnsRule(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
this.$notification.error({
message: 'Unknown error: ' + e.response.status,
style: {
width: '100px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
}
})
},
clickSwitch(record, prop) {
record[prop] = !record[prop]
upsertDnsRule(record).then().catch(e => {
this.$notification.error({
message: 'Edit failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
addRule() {
this.form = {}
this.showForm(CREATE)
},
viewRule(record) {
this.form = record
this.showForm(VIEW)
},
editRule(record) {
this.form = JSON.parse(JSON.stringify(record))
this.showForm(EDIT)
},
deleteRule(record, index) {
deleteDnsRule(record).then(() => {
this.data.splice(index, 1)
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
showForm(action) {
this.formAction = action
this.formReadOnly = action === VIEW;
this.formVisible = true;
},
closeDrawer() {
this.formVisible = false;
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
upsertDnsRule(this.form).then(() => {
this.closeDrawer()
this.fetch({page: this.pagination.current});
this.$notification.info({
message: 'Success',
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 2.5
});
}).catch(e => {
this.$notification.error({
message: this.formAction + ' failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
})
},
handleCancel() {
this.form = {}
this.closeDrawer()
}
},
mounted() {
this.fetch({page: "1"});
},
components: {
BasicRule,
}
}
</script>
+502
View File
@@ -0,0 +1,502 @@
<template xmlns:a-col="http://www.w3.org/1999/html">
<div>
<a-button id="add-rule" type="primary" @click="addRule">
<a-icon type="plus"/>
New Rule
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' HTTP rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@close="closeDrawer"
>
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
<BasicRule :form="form" :readOnly="formReadOnly"/>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item :rules="rules.response_status_code"
prop="response_status_code">
<span slot="label">
Response Status Code
<a-tooltip
title="Number between 100-600, or template such as ${query.var_name}/${body.var_name}/${header.var_name}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input v-model="form.response_status_code"
style="width: 100%"
placeholder="200"
:disabled="formReadOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item>
<span slot="label">
Response Headers
<a-tooltip
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input-group compact v-for="headerKey in headerKeys" :key="headerKey">
<a-auto-complete v-model="form['Header-'+headerKey]"
style="width: 47%;margin-bottom: 5px"
v-decorator="['Header-'+headerKey,]"
:dataSource="headerSet"
:filterOption="filterOption"
:defaultOpen="false"
placeholder="Header"
:disabled="formReadOnly"
></a-auto-complete>
<a-input v-model="form['Value-'+headerKey]"
@focus="()=>{ !formReadOnly&&(headerKey === headerKeys[headerKeys.length-1]) && form['Header-'+headerKey] ? addHeader(): null}"
style="width: 53%"
v-decorator="['Value-'+headerKey,]"
placeholder="Value"
:disabled="formReadOnly"
>
<a-icon slot="addonAfter"
class="dynamic-delete-button"
type="minus-circle-o"
@click="() => !formReadOnly? removeHeader(headerKey):null"
/>
</a-input>
</a-input-group>
</a-form-model-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item>
<span slot="label">
Response Body
<a-tooltip
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-textarea v-model="form.response_body"
placeholder="Hello RevSuit!"
:readOnly="formReadOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
Cancel
</a-button>
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
Submit
</a-button>
</div>
</a-drawer>
<!-- rule table -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
>
Search
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="rank" slot-scope="rank">
<a-tag
:color="'#'+(0x2db7f5+rank*80).toString(16)"
>
{{ rank }}
</a-tag>
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
<span slot="action" slot-scope="text,record,index">
<a-button @click="viewRule(record)" style="
color: #67C23A;
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteRule(record,index)"
>
<a-button type="danger" size="small" ghost>Delete</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</template>
<style scoped>
#add-rule {
margin-bottom: 10px;
}
</style>
<script>
import {getHttpRule, upsertHttpRule, deleteHttpRule} from '@/api/rule'
import {store} from '@/main'
import BasicRule from "@/components/BasicRule";
const VIEW = "View"
const EDIT = "Edit"
const CREATE = "Create"
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
},
{
title: 'RANK',
dataIndex: 'rank',
key: 'rank',
scopedSlots: {
customRender: "rank"
}
},
{
title: 'PUSH TO CLIENT',
dataIndex: 'push_to_client',
key: 'push_to_client',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'NOTICE',
dataIndex: 'notice',
key: 'notice',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'Action',
key: 'action',
scopedSlots: {customRender: 'action'},
},
];
const rules = {
response_status_code: [{
validator: (rule, code, callback) => {
console.log(code)
if (code === undefined || (!isNaN(code) && 100 < code && code < 600)) {
return callback()
} else if (/\${(query|body|header)\..+?}/.test(code)) {
return callback()
} else {
return callback(new Error("please input legal response status code value"))
}
}, trigger: 'blur'
}]
}
export default {
name: 'HttpRules',
data() {
return {
data: [],
formVisible: false,
pagination: {current: 1},
filters: {},
loading: false,
columns,
form: {},
rules: rules,
formReadOnly: false,
formAction: "", // View ,Create or Edit
headerKeys: [1],
headerSet: ["Accept-Patch",
"Accept-Ranges",
"Age",
"Allow",
"Cache-Control",
"Connection",
"Content-Disposition",
"Content-Encoding",
"Content-Language",
"Content-Length",
"Content-Location",
"Content-Range",
"Content-Type",
"Date",
"Delta-Base",
"ETag",
"Expires",
"Last-Modified",
"Link",
"Location",
"Pragma",
"Proxy-Authenticate",
"Public-Key-Pins",
"Retry-After",
"Server",
"Set-Cookie",
"Strict-Transport-Security",
"Transfer-Encoding",
"Upgrade",
"Vary",
"Via",
"Warning",
"WWW-Authenticate",
"Content-Security-Policy",
"Refresh",
"X-Powered-By",
"X-Request-ID",
"X-UA-Compatible",
"X-XSS-Protection",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers"],
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getHttpRule(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
this.$notification.error({
message: 'Unknown error: ' + e.response.status,
style: {
width: '100px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
}
})
},
clickSwitch(record, prop) {
record[prop] = !record[prop]
upsertHttpRule(record).catch(e => {
this.$notification.error({
message: 'Edit failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
addRule() {
this.form = {}
this.showForm(CREATE)
},
viewRule(record) {
this.form = record
this.showForm(VIEW)
},
editRule(record) {
this.form = JSON.parse(JSON.stringify(record))
this.showForm(EDIT)
},
deleteRule(record, index) {
deleteHttpRule(record).then(() => {
this.data.splice(index, 1)
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
showForm(action) {
this.formAction = action
this.formReadOnly = action === VIEW;
this.formVisible = true;
for (let k in this.form.response_headers) {
this.form["Header-" + this.headerKeys.length] = k
this.form["Value-" + this.headerKeys.length] = this.form.response_headers[k]
this.addHeader()
}
if (this.formReadOnly) {
this.removeHeader(this.headerKeys.length)
}
},
closeDrawer() {
this.formVisible = false;
this.headerKeys = [1]
},
addHeader() {
this.headerKeys.push(this.headerKeys[this.headerKeys.length - 1] + 1)
},
removeHeader(key) {
if (this.headerKeys.length > 1) {
this.headerKeys.splice(this.headerKeys.indexOf(key), 1)
}
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
);
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
let form = {}
let headers = {}
for (let k in this.form) {
if (k.indexOf("Header-") === 0) {
let i = k.substr("Header-".length)
if (this.form["Value-" + i]) {
headers[this.form[k]] = this.form["Value-" + i]
}
} else if (k.indexOf("Value-") === -1) {
form[k] = this.form[k]
}
}
form.response_headers = headers
upsertHttpRule(form).then(() => {
this.closeDrawer()
this.fetch({page: this.pagination.current});
this.$notification.info({
message: 'Success',
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 2.5
});
}).catch(e => {
this.$notification.error({
message: this.formAction + ' failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
} else {
return false;
}
});
},
handleCancel() {
this.form = {}
this.closeDrawer()
}
},
mounted() {
this.fetch({page: "1"});
},
components: {
BasicRule,
}
}
</script>
+433
View File
@@ -0,0 +1,433 @@
<template xmlns:a-col="http://www.w3.org/1999/html">
<div>
<a-button id="add-rule" type="primary" @click="addRule">
<a-icon type="plus"/>
New Rule
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' MySQL rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@close="closeDrawer"
>
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
<BasicRule :form="form" :readOnly="formReadOnly"/>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="Files" :rules="rules.files">
<a-input
v-model="form.files"
style="width: 100%"
placeholder="please enter file name,use ';' to split multiple file names"
:readOnly="formReadOnly"
:disabled="form.exploit_jdbc_client"
/>
</a-form-model-item>
</a-col>
</a-row>
<a-row>
<a-form-model-item>
<div class="ant-form-item-label">
<label for="exploit-jdbc-client">Exploit Jdbc Client
<a-tooltip placement="topLeft" title="Whether test to exploit jdbc client.">
<a-icon type="question-circle"/>
</a-tooltip>
</label>
</div>
<a-switch v-model="form.exploit_jdbc_client" id="exploit-jdbc-client" :disabled="formReadOnly"/>
</a-form-model-item>
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="Payload">
<a-input-group compact v-for="payloadKey in payloadKeys" :key="payloadKey">
<a-input v-model="form['Key-'+payloadKey]"
style="width: 47%;margin-bottom: 5px"
v-decorator="['Key-'+payloadKey,]"
:defaultOpen="false"
placeholder="Key"
:disabled="!form.exploit_jdbc_client"
:readOnly="formReadOnly"
></a-input>
<a-input v-model="form['Value-'+payloadKey]"
@focus="()=>{ !formReadOnly&&(payloadKey === payloadKeys[payloadKeys.length-1]) && form['Key-'+payloadKey] ? addPayload(): null}"
style="width: 53%"
v-decorator="['Value-'+payloadKey,]"
placeholder="Base64 encoded payload Value"
:disabled="!form.exploit_jdbc_client"
:readOnly="formReadOnly"
>
<a-icon slot="addonAfter"
class="dynamic-delete-button"
type="minus-circle-o"
@click="() => form.exploit_jdbc_client? removePayload(payloadKey):null"
/>
</a-input>
</a-input-group>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
Cancel
</a-button>
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
Submit
</a-button>
</div>
</a-drawer>
<!-- rule table -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
>
Search
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="rank" slot-scope="rank">
<a-tag
:color="'#'+(0x2db7f5+rank*80).toString(16)"
>
{{ rank }}
</a-tag>
</span>
<span slot="files" slot-scope="files">
<!-- eslint-disable-next-line-->
{{ files.length > 30 ? files.substr(0, 30) + "..." : files }}
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
<span slot="action" slot-scope="text,record,index">
<a-button @click="viewRule(record)" style="
color: #67C23A;
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteRule(record,index)"
>
<a-button type="danger" size="small" ghost>Delete</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</template>
<style scoped>
#add-rule {
margin-bottom: 10px;
}
</style>
<script>
import {getMysqlRule, upsertMysqlRule, deleteMysqlRule} from '@/api/rule'
import {store} from '@/main'
import BasicRule from "@/components/BasicRule";
const VIEW = "View"
const EDIT = "Edit"
const CREATE = "Create"
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
},
{
title: 'RANK',
dataIndex: 'rank',
key: 'rank',
scopedSlots: {
customRender: "rank"
}
},
{
title: 'FILES',
dataIndex: 'files',
key: 'files',
scopedSlots: {
customRender: "files"
}
},
{
title: 'PUSH TO CLIENT',
dataIndex: 'push_to_client',
key: 'push_to_client',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'NOTICE',
dataIndex: 'notice',
key: 'notice',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'Action',
key: 'action',
scopedSlots: {customRender: 'action'},
},
];
const rules = {
files: [{required: false, message: "please enter file name"}],
}
export default {
name: 'MysqlRules',
data() {
return {
data: [],
formVisible: false,
pagination: {current: 1},
filters: {},
loading: false,
columns,
form: {},
rules: rules,
payloadKeys: [1],
formReadOnly: false,
formAction: "", // View ,Create or Edit
}
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getMysqlRule(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
this.$notification.error({
message: 'Unknown error: ' + e.response.status,
style: {
width: '100px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
}
})
},
clickSwitch(record, prop) {
record[prop] = !record[prop]
upsertMysqlRule(record).then().catch(e => {
this.$notification.error({
message: 'Edit failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
addRule() {
this.form = {}
this.showForm(CREATE)
},
viewRule(record) {
this.form = record
this.showForm(VIEW)
},
editRule(record) {
this.form = JSON.parse(JSON.stringify(record))
this.showForm(EDIT)
},
deleteRule(record, index) {
deleteMysqlRule(record).then(() => {
this.data.splice(index, 1)
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
showForm(action) {
this.formAction = action
this.formReadOnly = action === VIEW;
this.formVisible = true;
for (let k in this.form.payloads) {
this.form["Key-" + this.payloadKeys.length] = k
this.form["Value-" + this.payloadKeys.length] = this.form.payloads[k]
this.addPayload()
}
if (this.formReadOnly) {
this.removePayload(this.payloadKeys.length)
}
},
closeDrawer() {
this.formVisible = false;
this.payloadKeys = [1]
},
addPayload() {
this.payloadKeys.push(this.payloadKeys[this.payloadKeys.length - 1] + 1)
},
removePayload(key) {
if (this.payloadKeys.length > 1) {
this.payloadKeys.splice(this.payloadKeys.indexOf(key), 1)
}
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
let payloads = {}
let form = {}
for (let k in this.form) {
if (k.indexOf("Key-") === 0) {
let i = k.substr("Key-".length)
if (this.form["Value-" + i]) {
payloads[this.form[k]] = this.form["Value-" + i]
}
} else if (k.indexOf("Value-") === -1) {
form[k] = this.form[k]
}
}
form.payloads = payloads
upsertMysqlRule(form).then(() => {
this.closeDrawer()
this.fetch({page: this.pagination.current});
this.$notification.info({
message: 'Success',
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 2.5
});
}).catch(e => {
this.$notification.error({
message: this.formAction + ' failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
})
},
handleCancel() {
this.form = {}
this.closeDrawer()
}
},
mounted() {
this.fetch({page: "1"});
},
components: {
BasicRule,
}
}
</script>
+22
View File
@@ -0,0 +1,22 @@
// vue.config.js
const path = require('path');
// eslint-disable-next-line no-unused-vars
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:10000',
pathRewrite: {
'^/revsuit/api': '/revsuit/api'
}
}
},
},
filenameHashing: false,
publicPath: ''
}