release revsuit vBeta0.1.0 (#15)

Co-authored-by: E99p1ant <[email protected]>
This commit is contained in:
Li4n0
2021-05-16 12:11:35 +08:00
committed by GitHub
co-authored by E99p1ant
parent b39ee101f9
commit 5b63e90390
98 changed files with 2357 additions and 623 deletions
+77
View File
@@ -0,0 +1,77 @@
on:
release:
types: [ published ]
name: Build
jobs:
release-linux:
name: release linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: compile
env:
CGO_ENABLE: 1
CGO_LDFLAGS: -static
run: |
GOARCH="amd64" go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_linux_amd64" ./cmd/revsuit/revsuit.go
GOARCH="386" go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_linux_i386" ./cmd/revsuit/revsuit.go
GOARCH="arm" go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_linux_arm" ./cmd/revsuit/revsuit.go
GOARCH="arm64" go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_linux_arm64" ./cmd/revsuit/revsuit.go
- name: Upload the artifacts
uses: li4n0/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: bin/*
tag: ${{ github.ref }}
overwrite: true
file_glob: true
release-darwin-amd64:
name: release darwin/amd64
runs-on: macos-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: compile
env:
CGO_ENABLE: 1
run: |
GOARCH="amd64" go build -v -ldflags="-s -w" -trimpath -o "bin/revsuit_darwin_amd64" ./cmd/revsuit/revsuit.go
- name: Upload the artifacts
uses: li4n0/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: bin/*
tag: ${{ github.ref }}
overwrite: true
file_glob: true
release-windows:
name: release windows
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
- name: compile
env:
CGO_ENABLE: 1
CGO_LDFLAGS: -static
run: |
env GOARCH=amd64 go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_windows_amd64.exe" ./cmd/revsuit/revsuit.go
env GOARCH=386 go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_windows_i386.exe" ./cmd/revsuit/revsuit.go
env GOARCH=arm go build -v -ldflags="-s -w -extldflags=-static" -trimpath -o "bin/revsuit_windows_arm.exe" ./cmd/revsuit/revsuit.go
- name: Upload the artifacts
uses: li4n0/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: bin/*
tag: ${{ github.ref }}
overwrite: true
file_glob: true
-1
View File
@@ -1 +0,0 @@
.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 -1
View File
@@ -1 +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} .custom-icons-list[data-v-05a908c2] .anticon{margin-right:6px}body[data-v-6db702b8],html[data-v-6db702b8]{height:100%;margin:0}.fade-transform-enter-active[data-v-6db702b8],.fade-transform-leave-active[data-v-6db702b8]{transition:all .3s;opacity:0}.fade-transform-enter[data-v-6db702b8],.fade-transform-leave[data-v-6db702b8]{opacity:0}.fade-transform-enter-to[data-v-6db702b8]{opacity:0}.ant-menu-item>span>a[data-v-6db702b8]{color:hsla(0,0%,100%,.65)}.ant-menu-item-selected>span>a[data-v-6db702b8]{color:#fff}#nav[data-v-6db702b8]{height:100%}#nav .trigger[data-v-6db702b8]{font-size:18px;line-height:64px;padding:0 24px;cursor:pointer;transition:color .3s}#nav .trigger[data-v-6db702b8]:hover{color:#1890ff}#nav .logo[data-v-6db702b8]{height:32px;background:#0a1d2d;margin:16px;text-align:center;font-size:1.2rem;color:#fff;padding-bottom:5px;border-bottom:2px solid #b6befa}.copyright[data-v-6db702b8]{color:#888;text-align:right;margin-right:1rem}
+1
View File
@@ -0,0 +1 @@
#add-rule[data-v-e8e0136c]{margin-bottom:10px}
+1
View File
@@ -0,0 +1 @@
#add-rule[data-v-2586daf9]{margin-bottom:10px}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}
+1
View File
@@ -0,0 +1 @@
#add-rule[data-v-487bb706]{margin-bottom:10px}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}
+1
View File
@@ -0,0 +1 @@
#add-rule[data-v-169b853c]{margin-bottom:10px}.full-screen-icon[data-v-169b853c]{position:absolute;z-index:5;color:#9e9e9e;right:1px;font-size:1rem;cursor:pointer;transition:font-size .1s}.full-screen-icon[data-v-169b853c]:hover{font-size:1.1rem;transition:font-size .1s}
+1
View File
@@ -0,0 +1 @@
#add-rule[data-v-98f944a6]{margin-bottom:10px}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}
+1
View File
@@ -0,0 +1 @@
.gray-table-row{background-color:#f5f5f5}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

+1 -1
View File
@@ -1 +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> <!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"><title>RevSuit Control Panel</title><link href="css/chunk-012c9c90.css" rel="prefetch"><link href="css/chunk-0155152b.css" rel="prefetch"><link href="css/chunk-1da7f23b.css" rel="prefetch"><link href="css/chunk-2e835d68.css" rel="prefetch"><link href="css/chunk-330a7e95.css" rel="prefetch"><link href="css/chunk-3d13e56e.css" rel="prefetch"><link href="css/chunk-d4730ae2.css" rel="prefetch"><link href="css/chunk-e00d17cc.css" rel="prefetch"><link href="css/chunk-e039e75a.css" rel="prefetch"><link href="css/chunk-fef965c2.css" rel="prefetch"><link href="js/chunk-012c9c90.js" rel="prefetch"><link href="js/chunk-0155152b.js" rel="prefetch"><link href="js/chunk-08f9fbe8.js" rel="prefetch"><link href="js/chunk-1da7f23b.js" rel="prefetch"><link href="js/chunk-2e835d68.js" rel="prefetch"><link href="js/chunk-330a7e95.js" rel="prefetch"><link href="js/chunk-3d13e56e.js" rel="prefetch"><link href="js/chunk-b241c712.js" rel="prefetch"><link href="js/chunk-d4730ae2.js" rel="prefetch"><link href="js/chunk-e00d17cc.js" rel="prefetch"><link href="js/chunk-e039e75a.js" rel="prefetch"><link href="js/chunk-fef965c2.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
+1 -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
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
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
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
File diff suppressed because one or more lines are too long
+21 -22
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico">-->
<title><%= htmlWebpackPlugin.options.title %></title> <title>RevSuit Control Panel</title>
</head> </head>
<body> <body>
<noscript> <noscript>
+48 -16
View File
@@ -45,6 +45,12 @@
<router-link to="/rules/ftp">FTP Rules</router-link> <router-link to="/rules/ftp">FTP Rules</router-link>
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
<a-menu-item key="/settings">
<router-link to="/settings">
<a-icon type="setting"/>
Settings
</router-link>
</a-menu-item>
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<a-layout> <a-layout>
@@ -54,8 +60,7 @@
:type="collapsed ? 'menu-unfold' : 'menu-fold'" :type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click="() => (collapsed = !collapsed)" @click="() => (collapsed = !collapsed)"
/> />
<!-- <transition name="list1">--> <div v-if="isLogMode" style="float: right; min-width:50% ;padding: 12px 0;line-height: 24px;">
<div v-if="isLogMode" style="float: right; width:45% ;padding: 12px 0;line-height: 24px;">
<a-row :gutter="24" type="flex"> <a-row :gutter="24" type="flex">
<a-col :span="21"> <a-col :span="21">
<a-form-model v-show="showSettings" ref="settings" layout="inline"> <a-form-model v-show="showSettings" ref="settings" layout="inline">
@@ -84,7 +89,6 @@
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
<!-- </transition>-->
</a-layout-header> </a-layout-header>
<a-layout-content <a-layout-content
:style="{ margin: '24px 16px', padding: '24px', borderRadius: '20px',background: '#fff', minHeight: 'initial' }" :style="{ margin: '24px 16px', padding: '24px', borderRadius: '20px',background: '#fff', minHeight: 'initial' }"
@@ -93,6 +97,12 @@
<router-view ref='content'/> <router-view ref='content'/>
</transition> </transition>
</a-layout-content> </a-layout-content>
<div class="copyright">
<p class="">
RevSuit Current Version: {{ this.version }} &copy;2021 <a href="https://github.com/Li4n0">Li4n0</a>. <a
href="https://github.com/Li4n0/revsuit">GitHub</a>
</p>
</div>
</a-layout> </a-layout>
<Auth></Auth> <Auth></Auth>
</a-layout> </a-layout>
@@ -100,15 +110,21 @@
<script> <script>
import Auth from '@/components/Auth' import Auth from '@/components/Auth'
import RuleIcon from "@/components/Icon"; import RuleIcon from "@/components/Icon";
import {getVersion} from "@/api/auth";
import {store} from "@/main";
export default { export default {
data() { data() {
return { return {
store,
autoRefresh: localStorage.getItem("autoRefresh") === "true", autoRefresh: localStorage.getItem("autoRefresh") === "true",
refreshInterval: localStorage.getItem("refreshInterval"), refreshInterval: localStorage.getItem("refreshInterval") ? localStorage.getItem("refreshInterval") : 5,
pageSize: store.pageSize,
collapsed: false, collapsed: false,
showSettings: false, showSettings: false,
openKeys: ['logs', "rules"], openKeys: ['logs'],
version: "",
}; };
}, },
computed: { computed: {
@@ -124,12 +140,18 @@ export default {
this.timer = setInterval(() => { this.timer = setInterval(() => {
this.$refs.content.fetch() this.$refs.content.fetch()
}, this.refreshInterval * 1000) }, this.refreshInterval * 1000)
},
GetVersion() {
getVersion().then(res => {
this.version = res.data.result
})
} }
}, },
mounted() { mounted() {
if (this.autoRefresh && this.isLogMode) { if (this.autoRefresh && this.isLogMode) {
this.timing() this.timing()
} }
this.GetVersion()
}, },
destroyed() { destroyed() {
clearInterval(this.timer) clearInterval(this.timer)
@@ -155,6 +177,21 @@ export default {
clearInterval(this.timer) clearInterval(this.timer)
this.timing() this.timing()
localStorage.setItem('refreshInterval', val) localStorage.setItem('refreshInterval', val)
},
pageSize(val) {
if (val <= 0 || val > 100) {
val = 10
}
store.pageSize = val
localStorage.setItem('pageSize', val)
},
'store.authed'() {
this.$refs.content.fetch()
},
'store.pageSize'() {
this.$refs.content.pagination.pageSize = store.pageSize
localStorage.setItem("pageSize", store.pageSize.toString())
this.$refs.content.fetch()
} }
}, },
components: { components: {
@@ -169,17 +206,6 @@ html, body {
margin: 0; margin: 0;
} }
.fade-enter.fade-enter-active,
.fade-appear.fade-appear-active {
-webkit-animation-name: none;
animation-name: none;
}
.fade-leave.fade-leave-active {
-webkit-animation-name: none;
animation-name: none;
}
/* fade-transform */ /* fade-transform */
.fade-transform-leave-active, .fade-transform-leave-active,
.fade-transform-enter-active { .fade-transform-enter-active {
@@ -236,4 +262,10 @@ html, body {
padding-bottom: 5px; padding-bottom: 5px;
border-bottom: 2px solid #b6befa; border-bottom: 2px solid #b6befa;
} }
.copyright {
color: #888;
text-align: right;
margin-right: 1rem;
}
</style> </style>
+10
View File
@@ -10,3 +10,13 @@ export function auth(token) {
} }
}) })
} }
export function getVersion() {
return request({
url: '/version',
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300
}
})
}
+4 -1
View File
@@ -2,7 +2,10 @@ import axios from 'axios'
const service = axios.create({ const service = axios.create({
baseURL: '/revsuit/api/', // api的base_url baseURL: '/revsuit/api/', // api的base_url
timeout: 5000 // request timeout timeout: 5000, // request timeout
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
export default service export default service
+5 -19
View File
@@ -5,9 +5,7 @@ export function getHttpRecord(params) {
url: '/record/http', url: '/record/http',
params: params, params: params,
method: 'get', method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -15,10 +13,7 @@ export function getDnsRecord(params) {
return request({ return request({
url: '/record/dns', url: '/record/dns',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -26,10 +21,7 @@ export function getMysqlRecord(params) {
return request({ return request({
url: '/record/mysql', url: '/record/mysql',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -37,10 +29,7 @@ export function getRmiRecord(params) {
return request({ return request({
url: '/record/rmi', url: '/record/rmi',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -48,9 +37,6 @@ export function getFtpRecord(params) {
return request({ return request({
url: '/record/ftp', url: '/record/ftp',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
+15 -60
View File
@@ -4,10 +4,7 @@ export function getHttpRule(params) {
return request({ return request({
url: '/rule/http', url: '/rule/http',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -15,10 +12,7 @@ export function upsertHttpRule(data) {
return request({ return request({
url: '/rule/http', url: '/rule/http',
data: data, data: data,
method: 'post', method: 'post'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -27,10 +21,7 @@ export function deleteHttpRule(data) {
return request({ return request({
url: '/rule/http', url: '/rule/http',
data: data, data: data,
method: 'delete', method: 'delete'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -38,10 +29,7 @@ export function getDnsRule(params) {
return request({ return request({
url: '/rule/dns', url: '/rule/dns',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -49,10 +37,7 @@ export function upsertDnsRule(data) {
return request({ return request({
url: '/rule/dns', url: '/rule/dns',
data: data, data: data,
method: 'post', method: 'post'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -60,10 +45,7 @@ export function deleteDnsRule(data) {
return request({ return request({
url: '/rule/dns', url: '/rule/dns',
data: data, data: data,
method: 'delete', method: 'delete'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -71,10 +53,7 @@ export function getMysqlRule(params) {
return request({ return request({
url: '/rule/mysql', url: '/rule/mysql',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -82,10 +61,7 @@ export function upsertMysqlRule(data) {
return request({ return request({
url: '/rule/mysql', url: '/rule/mysql',
data: data, data: data,
method: 'post', method: 'post'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -93,10 +69,7 @@ export function deleteMysqlRule(data) {
return request({ return request({
url: '/rule/mysql', url: '/rule/mysql',
data: data, data: data,
method: 'delete', method: 'delete'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -104,10 +77,7 @@ export function getRmiRule(params) {
return request({ return request({
url: '/rule/rmi', url: '/rule/rmi',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -115,10 +85,7 @@ export function upsertRmiRule(data) {
return request({ return request({
url: '/rule/rmi', url: '/rule/rmi',
data: data, data: data,
method: 'post', method: 'post'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -126,10 +93,7 @@ export function deleteRmiRule(data) {
return request({ return request({
url: '/rule/rmi', url: '/rule/rmi',
data: data, data: data,
method: 'delete', method: 'delete'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -137,10 +101,7 @@ export function getFtpRule(params) {
return request({ return request({
url: '/rule/ftp', url: '/rule/ftp',
params: params, params: params,
method: 'get', method: 'get'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -148,10 +109,7 @@ export function upsertFtpRule(data) {
return request({ return request({
url: '/rule/ftp', url: '/rule/ftp',
data: data, data: data,
method: 'post', method: 'post'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
@@ -159,9 +117,6 @@ export function deleteFtpRule(data) {
return request({ return request({
url: '/rule/ftp', url: '/rule/ftp',
data: data, data: data,
method: 'delete', method: 'delete'
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
}) })
} }
+85
View File
@@ -0,0 +1,85 @@
import request from './index'
export function getHttpConfig() {
return request({
url: '/setting/getHttpConfig',
})
}
export function updateHttpConfig(data) {
return request({
url: '/setting/updateHttpConfig',
method: "post",
data: data
})
}
export function getDnsConfig() {
return request({
url: '/setting/getDnsConfig',
})
}
export function updateDnsConfig(data) {
return request({
url: '/setting/updateDnsConfig',
method: "post",
data: data,
})
}
export function getRmiConfig() {
return request({
url: '/setting/getRmiConfig',
})
}
export function updateRmiConfig(data) {
return request({
url: '/setting/updateRmiConfig',
method: "post",
data: data,
})
}
export function getMySQLConfig() {
return request({
url: '/setting/getMySQLConfig',
})
}
export function updateMySQLConfig(data) {
return request({
url: '/setting/updateMySQLConfig',
method: "post",
data: data,
})
}
export function getFtpConfig() {
return request({
url: '/setting/getFtpConfig',
})
}
export function updateFtpConfig(data) {
return request({
url: '/setting/updateFtpConfig',
method: "post",
data: data,
})
}
export function getNoticeConfig() {
return request({
url: '/setting/getNoticeConfig',
})
}
export function updateNoticeConfig(data) {
return request({
url: '/setting/updateNoticeConfig',
method: "post",
data: data,
})
}
+54
View File
@@ -0,0 +1,54 @@
<template>
<a-spin :spinning="spinning">
<a-form-model :model="form" ref="form" layout="vertical">
<a-row :gutter="24" v-for="(value,key) in form" :key="key">
<a-col :span="24">
<a-form-model-item :label="key+':'" :prop="key">
<a-switch v-if="key==='Enable'" v-model="form[key]" size="large">
<a-icon slot="checkedChildren" type="check"/>
<a-icon slot="unCheckedChildren" type="close"/>
</a-switch>
<a-select v-else-if="key==='LogLevel'"
v-model="form[key]"
>
<a-select-option value='debug'>DEBUG</a-select-option>
<a-select-option value='info'>INFO</a-select-option>
<a-select-option value='warning'>WARNING</a-select-option>
<a-select-option value='error'>ERROR</a-select-option>
<a-select-option value='fatal'>FATAL</a-select-option>
</a-select>
<a-input :disabled="key ==='Addr' && parent==='Http'" v-else v-model="form[key]"/>
</a-form-model-item>
</a-col>
</a-row>
<a-space size="middle" v-if="Object.keys(form).length">
<a-button @click="this.$parent.getConfig">Cancel</a-button>
<a-popconfirm
title="Are you sure update the config?
It will stop running for at least two seconds, and may fail to restart."
ok-text="Yes"
cancel-text="No"
@confirm="this.$parent.updateConfig"
>
<a-button type="danger">Update</a-button>
</a-popconfirm>
</a-space>
</a-form-model>
</a-spin>
</template>
<script>
export default {
name: "SettingForm",
data() {
return {
parent: this.$parent.$options.name
}
},
props: ['form', 'spinning'],
}
</script>
<style scoped>
</style>
+1
View File
@@ -10,6 +10,7 @@ Vue.config.productionTip = false;
Vue.use(Antd); Vue.use(Antd);
export const store = Vue.observable({ export const store = Vue.observable({
authed: true, authed: true,
pageSize: localStorage.getItem("pageSize") ? parseInt(localStorage.getItem("pageSize")) : 10
}) })
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
+9 -2
View File
@@ -1,6 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -8,8 +7,9 @@ const routes = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Home',
component: Home redirect: '/logs/http'
}, },
// logs router
{ {
path: '/logs/http', path: '/logs/http',
name: 'HttpLogs', name: 'HttpLogs',
@@ -35,6 +35,7 @@ const routes = [
name: 'FtpLogs', name: 'FtpLogs',
component: () => import( '../views/logs/Ftp') component: () => import( '../views/logs/Ftp')
}, },
// rules router
{ {
path: '/rules/http', path: '/rules/http',
name: 'HttpRules', name: 'HttpRules',
@@ -59,6 +60,12 @@ const routes = [
path: '/rules/ftp', path: '/rules/ftp',
name: 'FtpRules', name: 'FtpRules',
component: () => import( '../views/rules/Ftp') component: () => import( '../views/rules/Ftp')
},
//Settings router
{
path: '/settings',
name: 'Settings',
component: () => import( '../views/settings/Settings')
} }
] ]
+8 -8
View File
@@ -103,7 +103,12 @@ export default {
return { return {
store, store,
data: [], data: [],
pagination: {current: 1}, pagination: {
current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
},
filters: {}, filters: {},
order: "desc", order: "desc",
loading: false, loading: false,
@@ -122,6 +127,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -129,7 +135,6 @@ export default {
let result = res.data.result let result = res.data.result
this.data = result.data this.data = result.data
const pagination = {...this.pagination}; const pagination = {...this.pagination};
pagination.total = result.count; pagination.total = result.count;
this.pagination = pagination; this.pagination = pagination;
this.loading = false this.loading = false
@@ -146,13 +151,8 @@ export default {
mounted() { mounted() {
this.fetch(); this.fetch();
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
FilterDropdown FilterDropdown
} },
} }
</script> </script>
+8 -10
View File
@@ -44,7 +44,7 @@
</span> </span>
<span slot="file" slot-scope="file"> <span slot="file" slot-scope="file">
<a-tag <a-tag
v-if="file.id" v-if="file"
color="#f5222d" color="#f5222d"
><a target="_blank" :href="'/revsuit/api/file/ftp/'+file.id">TRUE</a> </a-tag> ><a target="_blank" :href="'/revsuit/api/file/ftp/'+file.id">TRUE</a> </a-tag>
<a-tag v-else color="#722ed1"> <a-tag v-else color="#722ed1">
@@ -190,7 +190,12 @@ export default {
return { return {
store, store,
data: [], data: [],
pagination: {current: 1}, pagination: {
current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
},
filters: {}, filters: {},
order: "desc", order: "desc",
loading: false, loading: false,
@@ -199,9 +204,6 @@ export default {
}; };
}, },
methods: { methods: {
aa(file) {
console.log(file)
},
handleTableChange(pagination, filters, sorter) { handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination}; const pager = {...this.pagination};
pager.current = pagination.current; pager.current = pagination.current;
@@ -213,6 +215,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -237,11 +240,6 @@ export default {
mounted() { mounted() {
this.fetch(); this.fetch();
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
FilterDropdown FilterDropdown
} }
+7 -6
View File
@@ -135,7 +135,12 @@ export default {
return { return {
store, store,
data: [], data: [],
pagination: {current: 1}, pagination: {
current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
},
filters: {}, filters: {},
order: "desc", order: "desc",
loading: false, loading: false,
@@ -155,6 +160,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -179,11 +185,6 @@ export default {
mounted() { mounted() {
this.fetch(); this.fetch();
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
FilterDropdown FilterDropdown
} }
+7 -6
View File
@@ -178,7 +178,12 @@ export default {
return { return {
store, store,
data: [], data: [],
pagination: {current: 1}, pagination: {
current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
},
filters: {}, filters: {},
order: "desc", order: "desc",
loading: false, loading: false,
@@ -198,6 +203,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -222,11 +228,6 @@ export default {
mounted() { mounted() {
this.fetch(); this.fetch();
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
FilterDropdown FilterDropdown
} }
+7 -7
View File
@@ -104,8 +104,12 @@ export default {
return { return {
store, store,
data: [], data: [],
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
order: "desc", order: "desc",
loading: false, loading: false,
columns, columns,
@@ -123,6 +127,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -147,11 +152,6 @@ export default {
mounted() { mounted() {
this.fetch(); this.fetch();
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
FilterDropdown FilterDropdown
} }
+7 -7
View File
@@ -291,8 +291,12 @@ export default {
store, store,
data: [], data: [],
formVisible: false, formVisible: false,
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
loading: false, loading: false,
columns, columns,
colors, colors,
@@ -315,6 +319,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -430,11 +435,6 @@ export default {
mounted() { mounted() {
this.fetch({page: "1"}); this.fetch({page: "1"});
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
BasicRule, BasicRule,
} }
+7 -7
View File
@@ -229,8 +229,12 @@ export default {
store, store,
data: [], data: [],
formVisible: false, formVisible: false,
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
loading: false, loading: false,
columns, columns,
form: {}, form: {},
@@ -250,6 +254,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -365,11 +370,6 @@ export default {
mounted() { mounted() {
this.fetch({page: "1"}); this.fetch({page: "1"});
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
BasicRule, BasicRule,
} }
+7 -7
View File
@@ -288,8 +288,12 @@ export default {
store, store,
data: [], data: [],
formVisible: false, formVisible: false,
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
loading: false, loading: false,
columns, columns,
form: {}, form: {},
@@ -357,6 +361,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -512,11 +517,6 @@ export default {
mounted() { mounted() {
this.fetch({page: "1"}); this.fetch({page: "1"});
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
BasicRule, BasicRule,
} }
+7 -7
View File
@@ -251,8 +251,12 @@ export default {
store, store,
data: [], data: [],
formVisible: false, formVisible: false,
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
loading: false, loading: false,
columns, columns,
form: {}, form: {},
@@ -274,6 +278,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -420,11 +425,6 @@ export default {
mounted() { mounted() {
this.fetch({page: "1"}); this.fetch({page: "1"});
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
BasicRule, BasicRule,
} }
+7 -7
View File
@@ -188,8 +188,12 @@ export default {
store, store,
data: [], data: [],
formVisible: false, formVisible: false,
pagination: {current: 1}, pagination: {
filters: {}, current: 1, showSizeChanger: true, pageSize: store.pageSize,
onShowSizeChange: (current, size) => {
store.pageSize = size
}
}, filters: {},
loading: false, loading: false,
columns, columns,
form: {}, form: {},
@@ -209,6 +213,7 @@ export default {
let params = { let params = {
...this.filters, ...this.filters,
page: this.pagination.current, page: this.pagination.current,
pageSize: this.pagination.pageSize,
order: this.order order: this.order
} }
this.loading = true; this.loading = true;
@@ -323,11 +328,6 @@ export default {
mounted() { mounted() {
this.fetch({page: "1"}); this.fetch({page: "1"});
}, },
watch: {
'store.authed'() {
this.fetch()
}
},
components: { components: {
BasicRule, BasicRule,
} }
+102
View File
@@ -0,0 +1,102 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getDnsConfig, updateDnsConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Dns",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getDnsConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateDnsConfig(this.form).then(
() => {
setTimeout(() => {
getDnsConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options carefully."
}
this.form = res.data
this.status = ""
this.title = ""
this.subTitle = ""
}
)
}, 3000)
}
).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getFtpConfig, updateFtpConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Ftp",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getFtpConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateFtpConfig(this.form).then(
() => {
setTimeout(() => {
getFtpConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options carefully."
}
this.status = ""
this.title = ""
this.subTitle = ""
this.form = res.data
}
)
}, 3000)
}
).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+99
View File
@@ -0,0 +1,99 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getHttpConfig, updateHttpConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Http",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getHttpConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateHttpConfig(this.form).then(
() => {
setTimeout(() => {
getHttpConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options."
}
this.form = res.data
this.status = ""
this.title = ""
this.subTitle = ""
}
).catch(() => {
this.status = "error"
this.title = "Updating the configuration seems to have failed, and may cause the platform to exit abnormally, please restart manually."
this.subTitle = "Please check your config options carefully."
})
}, 3000)
}
).catch(() => {
this.status = "error"
this.title = "Updating the configuration seems to have failed, and may cause the platform to exit abnormally, please restart manually."
this.subTitle = "Please check your config options carefully."
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getMySQLConfig, updateMySQLConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Mysql",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getMySQLConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateMySQLConfig(this.form).then(
() => {
setTimeout(() => {
getMySQLConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options carefully."
}
this.form = res.data
this.status = ""
this.title = ""
this.subTitle = ""
}
)
}, 3000)
}
).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getNoticeConfig, updateNoticeConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Notice",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getNoticeConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateNoticeConfig(this.form).then(
() => {
setTimeout(() => {
getNoticeConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options carefully."
}
this.form = res.data
this.status = ""
this.title = ""
this.subTitle = ""
}
)
}, 3000)
}
).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<div>
<SettingForm :form="form" :spinning="spinning"></SettingForm>
<a-result v-if="status"
:status="status"
:title="title"
:sub-title="subTitle"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
import {getRmiConfig, updateRmiConfig} from "@/api/settings"
import SettingForm from "@/components/SettingForm";
export default {
name: "Rmi",
data() {
return {
form: {},
spinning: false,
status: "",
title: "",
subTitle: "",
errors: []
}
},
methods: {
getConfig() {
getRmiConfig().then(res => {
this.form = res.data
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
updateConfig() {
this.spinning = true
let targetConfig = JSON.stringify(this.form)
updateRmiConfig(this.form).then(
() => {
setTimeout(() => {
getRmiConfig().then((res) => {
this.spinning = false
let nowConfig = JSON.stringify(res.data)
if (nowConfig !== targetConfig) {
this.status = "warning"
this.title = "Updating the configuration seems to have failed"
this.subTitle = "Please check your config options carefully."
}
this.form = res.data
this.status = ""
this.title = ""
this.subTitle = ""
}
)
}, 3000)
}
).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
},
mounted() {
this.getConfig()
},
components: {
SettingForm
}
}
</script>
<style scoped>
</style>
+61
View File
@@ -0,0 +1,61 @@
<template>
<div>
<a-space size="middle">
<a-upload
name="rules"
action="/revsuit/api/setting/importRules"
@change="handleChange"
:showUploadList="false"
>
<a-button type="primary" icon="upload">Import</a-button>
</a-upload>
<a-button type="primary" icon="download" onclick="window.open('/revsuit/api/setting/exportRules')">Export
</a-button>
</a-space>
<a-result v-if="status"
:status="status"
:title="title"
>
<template #extra>
<a-row>
<a-col :span="12" :offset="7">
<div style="text-align: left">
<li v-for="err in errors" :key="err">{{ err }}</li>
</div>
</a-col>
</a-row>
</template>
</a-result>
</div>
</template>
<script>
export default {
name: "Rules",
data() {
return {
status: "",
title: "",
errors: []
}
},
methods: {
handleChange(info) {
let resp = info.file.response
if (info.file.status === 'done' || info.file.status === 'error') {
if (resp.error) {
this.status = "warning"
} else {
this.status = "success"
}
this.title = resp.result
this.errors = resp.error
}
},
}
}
</script>
<style scoped>
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div>
<a-card
:tab-list="tabList"
:active-tab-key="tabKey"
@tabChange="key => this.tabKey = key"
style="width: 100%;">
<http v-if="tabKey==='HTTP'"></http>
<dns v-else-if="tabKey==='DNS'"></dns>
<rmi v-else-if="tabKey==='RMI'"></rmi>
<mysql v-else-if="tabKey==='MYSQL'"></mysql>
<ftp v-else-if="tabKey==='FTP'"></ftp>
<notice v-else-if="tabKey==='NOTICE'"></notice>
<rules v-else-if="tabKey==='RULES'"></rules>
</a-card>
</div>
</template>
<script>
import Http from "./Http"
import Dns from "./Dns"
import Rmi from "./Rmi"
import Mysql from "./Mysql"
import Ftp from "./Ftp"
import Notice from "./Notice"
import Rules from "./Rules"
export default {
name: "Settings",
data() {
return {
tabList: [
{key: "HTTP", tab: "PLATFORM|HTTP"},
{key: "DNS", tab: "DNS"},
{key: "RMI", tab: "RMI"},
{key: "MYSQL", tab: "MYSQL"},
{key: "FTP", tab: "FTP"},
{key: "NOTICE", tab: "NOTICE"},
{key: "RULES", tab: "RULES"}
],
tabKey: "HTTP",
}
}
,
methods: {},
components: {
Http,
Dns,
Rmi,
Mysql,
Ftp,
Notice,
Rules,
}
}
</script>
<style scoped>
</style>
+1
View File
@@ -17,6 +17,7 @@ module.exports = {
} }
}, },
}, },
productionSourceMap:false,
runtimeCompiler: true, runtimeCompiler: true,
filenameHashing: false, filenameHashing: false,
publicPath: '' publicPath: ''
+1
View File
@@ -26,6 +26,7 @@ require (
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 // indirect golang.org/x/sys v0.0.0-20210217105451-b926d437f341 // indirect
google.golang.org/protobuf v1.25.0 // indirect google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.4 gorm.io/gorm v1.21.4
unknwon.dev/clog/v2 v2.2.0 unknwon.dev/clog/v2 v2.2.0
+9
View File
@@ -1 +1,10 @@
package cli package cli
const banner = `
____ _____ _ __
/ __ \___ _ __/ ___/__ __(_) /_
/ /_/ / _ \ | / /\__ \/ / / / / __/
/ _, _/ __/ |/ /___/ / /_/ / / /_
/_/ |_|\___/|___//____/\__,_/_/\__/
v%s
https://revsuit.pro`
+26 -6
View File
@@ -1,16 +1,21 @@
package cli package cli
import ( import (
"fmt"
"os" "os"
"sort" "sort"
"github.com/li4n0/revsuit/pkg/server" "github.com/li4n0/revsuit/pkg/server"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
func Start() { func Start() {
_ = log.NewConsole(100,
log.ConsoleConfig{
Level: log.LevelInfo,
})
app := &cli.App{ app := &cli.App{
Name: "RevSuit", Name: "RevSuit",
Usage: "An Open-Sourced Reverse Platform Designed for Receive Various Kinds of Connection", Usage: "An Open-Sourced Reverse Platform Designed for Receive Various Kinds of Connection",
@@ -23,21 +28,32 @@ func Start() {
Name: "token", Name: "token",
Usage: "token used to manage platform", Usage: "token used to manage platform",
}, },
&cli.StringFlag{
Name: "flags",
Usage: "for http and dns connection, platform will only record the connection those match these regex flags. * meaning record all",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "db", Name: "db",
Usage: "database file path", Usage: "database file path",
}, },
&cli.StringFlag{
Name: "log",
Usage: "specify a log level from debug, info, warn, error, fatal. (default: info)",
},
&cli.PathFlag{
Name: "config",
Usage: "load configuration from FILE (default: config.yaml)",
},
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var configFile = "config.yaml"
if c.Path("config") != "" {
configFile = c.Path("config")
}
conf := &server.Config{} conf := &server.Config{}
if content, err := os.ReadFile("config.yaml"); err == nil { if content, err := os.ReadFile(configFile); err == nil {
if err := yaml.Unmarshal(content, conf); err != nil { if err := yaml.Unmarshal(content, conf); err != nil {
return err return err
} }
} else if os.IsNotExist(err) && configFile == "config.yaml" {
log.Warn("Generate default configurations to config.yaml, please configure and run again.")
return os.WriteFile("config.yaml", configTemplate, 0644)
} else { } else {
return err return err
} }
@@ -50,6 +66,10 @@ func Start() {
if c.String("db") != "" { if c.String("db") != "" {
conf.Database = c.String("db") conf.Database = c.String("db")
} }
if c.String("log") != "" {
conf.Database = c.String("log")
}
fmt.Printf(banner, server.VERSION)
server.New(conf).Run() server.New(conf).Run()
return nil return nil
}, },
@@ -20,7 +20,6 @@ ftp:
addr: :21 addr: :21
pasv_ip: 127.0.0.1 # your public network ip pasv_ip: 127.0.0.1 # your public network ip
pasv_port: 2020 pasv_port: 2020
notice: notice:
dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token} dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token}
lark: https://open.feishu.cn/open-apis/bot/v2/hook/{token} lark: https://open.feishu.cn/open-apis/bot/v2/hook/{token}
+8
View File
@@ -0,0 +1,8 @@
package cli
import (
_ "embed"
)
//go:embed config.tpl.yaml
var configTemplate []byte
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"sync" "sync"
"github.com/li4n0/revsuit/internal/record" "github.com/li4n0/revsuit/internal/record"
"github.com/pkg/errors"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
@@ -14,6 +15,7 @@ var (
// Bot is used to send notice // Bot is used to send notice
type Bot interface { type Bot interface {
name() string
notice(record.Record) error notice(record.Record) error
buildPayload(record.Record) string buildPayload(record.Record) string
} }
@@ -41,7 +43,7 @@ func (a *Announcer) AddBot(b Bot) *Announcer {
func Notice(r record.Record) { func Notice(r record.Record) {
for _, bot := range announcer.Bots { for _, bot := range announcer.Bots {
if err := bot.notice(r); err != nil { if err := bot.notice(r); err != nil {
log.Warn(err.Error()) log.Warn(errors.Wrap(err, "%s notice failed").Error(), bot.name())
} }
} }
} }
+4
View File
@@ -33,6 +33,10 @@ type dingPayload struct {
At []dingAt `json:"at"` At []dingAt `json:"at"`
} }
func (d *DingTalk) name() string {
return "DingTalk"
}
func (d *DingTalk) buildPayload(r record.Record) string { func (d *DingTalk) buildPayload(r record.Record) string {
payload := dingPayload{ payload := dingPayload{
MsgType: "markdown", MsgType: "markdown",
+7 -3
View File
@@ -41,7 +41,11 @@ type larkPayload struct {
Card larkCard `json:"card"` Card larkCard `json:"card"`
} }
func (d *Lark) buildPayload(r record.Record) string { func (l *Lark) name() string {
return "Lark"
}
func (l *Lark) buildPayload(r record.Record) string {
payload := larkPayload{ payload := larkPayload{
MsgType: "interactive", MsgType: "interactive",
Card: larkCard{ Card: larkCard{
@@ -69,8 +73,8 @@ func (d *Lark) buildPayload(r record.Record) string {
return string(p) return string(p)
} }
func (d *Lark) notice(r record.Record) error { func (l *Lark) notice(r record.Record) error {
resp, err := http.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r))) resp, err := http.Post(l.URL, "application/json", strings.NewReader(l.buildPayload(r)))
if err != nil { if err != nil {
return errors.Wrap(err, "HTTP request") return errors.Wrap(err, "HTTP request")
} }
+7 -3
View File
@@ -36,7 +36,11 @@ type slackPayload struct {
Attachments []slackAttachments `json:"attachments"` Attachments []slackAttachments `json:"attachments"`
} }
func (d *Slack) buildPayload(r record.Record) string { func (s *Slack) name() string {
return "Slack"
}
func (s *Slack) buildPayload(r record.Record) string {
payload := slackPayload{ payload := slackPayload{
Attachments: []slackAttachments{ Attachments: []slackAttachments{
{ {
@@ -67,8 +71,8 @@ func (d *Slack) buildPayload(r record.Record) string {
return string(p) return string(p)
} }
func (d *Slack) notice(r record.Record) error { func (s *Slack) notice(r record.Record) error {
resp, err := http.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r))) resp, err := http.Post(s.URL, "application/json", strings.NewReader(s.buildPayload(r)))
if err != nil { if err != nil {
return errors.Wrap(err, "HTTP request") return errors.Wrap(err, "HTTP request")
} }
+4
View File
@@ -27,6 +27,10 @@ type weixinPayload struct {
Markdown weixinMarkdown `json:"markdown"` Markdown weixinMarkdown `json:"markdown"`
} }
func (w *Weixin) name() string {
return "Weixin"
}
func (w *Weixin) buildPayload(r record.Record) string { func (w *Weixin) buildPayload(r record.Record) string {
payload := weixinPayload{ payload := weixinPayload{
ToUser: "@all", ToUser: "@all",
+4 -4
View File
@@ -15,8 +15,8 @@ import (
) )
const ( const (
COPY_WRITE_URL = "https://qqwry.mirror.noc.one/copywrite.rar" CopyWriteUrl = "https://qqwry.mirror.noc.one/copywrite.rar"
QQWRY_URL = "https://qqwry.mirror.noc.one/qqwry.rar" QqwryUrl = "https://qqwry.mirror.noc.one/qqwry.rar"
) )
func get(url string) (b []byte, err error) { func get(url string) (b []byte, err error) {
@@ -70,14 +70,14 @@ func download() (err error) {
wg.Add(2) wg.Add(2)
go func() { go func() {
defer wg.Done() defer wg.Done()
if copyWriteData, err = get(COPY_WRITE_URL); err != nil { if copyWriteData, err = get(CopyWriteUrl); err != nil {
return return
} }
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
if qqwryData, err = get(QQWRY_URL); err != nil { if qqwryData, err = get(QqwryUrl); err != nil {
return return
} }
}() }()
+4
View File
@@ -13,6 +13,10 @@ var wry *qqwry.QQwry
var once sync.Once var once sync.Once
func init() { func init() {
_ = log.NewConsole(100,
log.ConsoleConfig{
Level: log.LevelInfo,
})
info, err := os.Stat("qqwry.dat") info, err := os.Stat("qqwry.dat")
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
+7 -7
View File
@@ -14,15 +14,15 @@ type Rule interface {
} }
type BaseRule struct { type BaseRule struct {
Rule `gorm:"-" json:"-"` Rule `gorm:"-" json:"-" yaml:"-"`
ID uint `gorm:"primarykey" form:"id" json:"id"` ID uint `gorm:"primarykey" form:"id" json:"id" yaml:"-"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at" yaml:"-"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at" yaml:"-"`
Name string `gorm:"index;unique;not null;" form:"name" json:"name"` Name string `gorm:"index;unique;not null;" form:"name" json:"name"`
FlagFormat string `gorm:"unique;not null;" form:"flag_format" json:"flag_format"` FlagFormat string `gorm:"unique;not null;" form:"flag_format" json:"flag_format" yaml:"flag_format"`
flagCatcher *regexp.Regexp `gorm:"-" json:"-"` flagCatcher *regexp.Regexp `gorm:"-" json:"-"`
Rank int `gorm:"default:0" json:"rank" form:"rank"` Rank int `gorm:"default:0" json:"rank" form:"rank"`
PushToClient bool `gorm:"default:false;not null;" form:"push_to_client" json:"push_to_client"` PushToClient bool `gorm:"default:false;not null;" form:"push_to_client" json:"push_to_client" yaml:"push_to_client"`
Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"` Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"`
} }
@@ -51,7 +51,7 @@ func (br BaseRule) Match(s string) (flag, flagGroup string, vars map[string]stri
} }
flag = matched[0] flag = matched[0]
if len(matched) > 1 && len(groupNames) == 0 { if len(matched) > 1 && groupNames[1] == "" {
flagGroup = matched[1] flagGroup = matched[1]
} }
+87 -54
View File
@@ -11,12 +11,15 @@ import (
"github.com/li4n0/revsuit/internal/recycler" "github.com/li4n0/revsuit/internal/recycler"
"github.com/li4n0/revsuit/internal/rule" "github.com/li4n0/revsuit/internal/rule"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/pkg/errors"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
type Server struct { type Server struct {
Config
rules []*Rule rules []*Rule
rulesLock sync.RWMutex rulesLock sync.RWMutex
livingLock sync.Mutex
} }
var ( var (
@@ -27,7 +30,7 @@ var (
func GetServer() *Server { func GetServer() *Server {
once.Do(func() { once.Do(func() {
server = &Server{rulesLock: sync.RWMutex{}} server = &Server{rulesLock: sync.RWMutex{}, livingLock: sync.Mutex{}}
}) })
return server return server
} }
@@ -42,17 +45,51 @@ func (s *Server) updateRules() error {
db := database.DB.Model(new(Rule)) db := database.DB.Model(new(Rule))
defer s.rulesLock.Unlock() defer s.rulesLock.Unlock()
s.rulesLock.Lock() s.rulesLock.Lock()
return db.Order("rank desc").Find(&s.rules).Error return errors.Wrap(db.Order("rank desc").Find(&s.rules).Error, "DNS update rules error")
} }
func (s *Server) Run() { func newSet(_rule *Rule, name, value, ip string, _type newdns.Type) []newdns.Set {
set := []newdns.Set{
{
Name: name,
Type: _type,
Records: func() []newdns.Record {
switch _rule.Type {
case newdns.TXT:
return []newdns.Record{{Data: []string{value}}}
case newdns.CNAME, newdns.NS:
return []newdns.Record{{Address: value + "."}}
case newdns.REBINDING:
if err := s.updateRules(); err != nil { // Get rebinding ip list
log.Fatal(err.Error()) values, ok := rebindingCache.Get(ip)
if !ok {
rebindingCache.Set(ip, strings.Split(value, ","), cache.DefaultExpiration)
values = strings.Split(value, ",")
} }
//create new dns zone with root domain //Choose and delete first ip
newZone := func(name string) *newdns.Zone { value := values.([]string)[0]
if len(values.([]string)) > 1 {
rebindingCache.Set(ip, values.([]string)[1:len(values.([]string))], cache.DefaultExpiration)
} else {
rebindingCache.Delete(ip)
}
log.Trace("DNS rebinding client[ip:%v] to %v", ip, value)
return []newdns.Record{{Address: value}}
default:
return []newdns.Record{{Address: value}}
}
}(),
TTL: _rule.TTL * time.Second,
},
}
return set
}
// newZone creates new dns zone with root domain
func (s *Server) newZone(name string) *newdns.Zone {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
recycler.Recycle(err) recycler.Recycle(err)
@@ -61,13 +98,11 @@ func (s *Server) Run() {
domain := strings.TrimSuffix(name, ".") domain := strings.TrimSuffix(name, ".")
frags := strings.Split(domain, ".") frags := strings.Split(domain, ".")
zoneName := "" zoneName := name
if len(frags) >= 2 { if len(frags) >= 2 {
zoneName = strings.Join(frags[len(frags)-2:], ".") + "." zoneName = strings.Join(frags[len(frags)-2:], ".") + "."
} else {
zoneName = name
} }
return &newdns.Zone{ zone := &newdns.Zone{
Name: zoneName, Name: zoneName,
MasterNameServer: "ns1.hostmaster.com.", MasterNameServer: "ns1.hostmaster.com.",
AllNameServers: []string{ AllNameServers: []string{
@@ -98,11 +133,11 @@ func (s *Server) Run() {
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count) database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 { if count <= 1 {
r.PushToClient() r.PushToClient()
log.Trace("DNS record[id%d] has been put to client message queue", r.ID) log.Trace("DNS record[id:%d, flagGroup:%s] has been put to client message queue", r.ID, flagGroup)
} }
} else { } else {
r.PushToClient() r.PushToClient()
log.Trace("DNS record[id%d] has been put to client message queue", r.ID) log.Trace("DNS record[id:%d, flag:%s] has been put to client message queue", r.ID, flag)
} }
} }
@@ -110,7 +145,7 @@ func (s *Server) Run() {
if _rule.Notice { if _rule.Notice {
go func() { go func() {
r.Notice() r.Notice()
log.Trace("DNS record[id%d] notice has been sent", r.ID) log.Trace("DNS record[id:%d] notice has been sent", r.ID)
}() }()
} }
@@ -121,62 +156,60 @@ func (s *Server) Run() {
_type = newdns.A _type = newdns.A
} }
return []newdns.Set{ return newSet(_rule, name, value, ip, _type), nil
{
Name: name,
Type: _type,
Records: func() []newdns.Record {
switch _rule.Type {
case newdns.TXT:
return []newdns.Record{{Data: []string{value}}}
case newdns.CNAME, newdns.NS:
return []newdns.Record{{Address: value + "."}}
case newdns.REBINDING:
// Get rebinding ip list
values, ok := rebindingCache.Get(ip)
if !ok {
rebindingCache.Set(ip, strings.Split(value, ","), cache.DefaultExpiration)
values = strings.Split(value, ",")
}
//Choose and delete first ip
value := values.([]string)[0]
if len(values.([]string)) > 1 {
rebindingCache.Set(ip, values.([]string)[1:len(values.([]string))], cache.DefaultExpiration)
} else {
rebindingCache.Delete(ip)
}
log.Trace("DNS rebinding client(ip:%v) to %v", ip, value)
return []newdns.Record{{Address: value}}
default:
return []newdns.Record{{Address: value}}
}
}(),
TTL: _rule.TTL * time.Second,
},
}, nil
} }
} }
return nil, nil return nil, nil
}, },
} }
return zone
}
func (s *Server) Stop() {
log.Info("DNS server is stopping...")
s.Enable = false
s.livingLock.Unlock()
}
func (s *Server) Run() {
s.Enable = true
s.livingLock.Lock()
defer func() {
if s.Enable {
log.Error("DNS Server exited unexpectedly")
}
s.Enable = false
s.livingLock.Unlock()
}()
if err := s.updateRules(); err != nil {
log.Error(err.Error())
return
} }
// create server // create server
server := newdns.NewServer(newdns.Config{ server := newdns.NewServer(newdns.Config{
Handler: func(name string) (*newdns.Zone, error) { Handler: func(name string) (*newdns.Zone, error) {
return newZone(name), nil return s.newZone(name), nil
}, },
}) })
// run server // run server
log.Info("Starting DNS Server at :53") log.Info("Starting DNS Server at :53")
err := server.Run(":53") go func() {
if err != nil { s.livingLock.Lock()
log.Fatal(err.Error()) if !s.Enable {
server.Close()
} }
}()
err := server.Run(":53")
defer server.Close()
if err != nil {
log.Error(err.Error())
return
}
} }
+13 -4
View File
@@ -47,11 +47,20 @@ func ListRecords(c *gin.Context) {
res []Record res []Record
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&dnsRecord); err != nil { if err := c.ShouldBind(&dnsRecord); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
} }
@@ -74,7 +83,7 @@ func ListRecords(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -84,10 +93,10 @@ func ListRecords(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
+27 -18
View File
@@ -13,7 +13,7 @@ import (
) )
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule `yaml:""`
Type newdns.Type `gorm:"default:1" form:"type" json:"type"` Type newdns.Type `gorm:"default:1" form:"type" json:"type"`
Value string `form:"value" json:"value"` Value string `form:"value" json:"value"`
TTL time.Duration `gorm:"ttl;default:10" form:"ttl" json:"ttl"` TTL time.Duration `gorm:"ttl;default:10" form:"ttl" json:"ttl"`
@@ -23,7 +23,7 @@ func (Rule) TableName() string {
return "dns_rules" return "dns_rules"
} }
// New dns rule struct // NewRule new dns rule struct
func NewRule(name, flagFormat, value string, pushToClient, notice bool, _type newdns.Type, ttl time.Duration) *Rule { func NewRule(name, flagFormat, value string, pushToClient, notice bool, _type newdns.Type, ttl time.Duration) *Rule {
return &Rule{ return &Rule{
BaseRule: rule.BaseRule{ BaseRule: rule.BaseRule{
@@ -38,7 +38,7 @@ func NewRule(name, flagFormat, value string, pushToClient, notice bool, _type ne
} }
} }
// Create or update the dns rule in database and ruleSet // CreateOrUpdate creates or updates the dns rule in database and ruleSet
func (r *Rule) CreateOrUpdate() (err error) { func (r *Rule) CreateOrUpdate() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Clauses(clause.OnConflict{ err = db.Clauses(clause.OnConflict{
@@ -62,7 +62,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
return err return err
} }
// Delete the dns rule in database and ruleSet // Delete deletes the dns rule in database and ruleSet
func (r *Rule) Delete() (err error) { func (r *Rule) Delete() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Delete(r).Error err = db.Delete(r).Error
@@ -73,19 +73,28 @@ func (r *Rule) Delete() (err error) {
return err return err
} }
// List all dns rules those satisfy the filter // ListRules lists all dns rules those satisfy the filter
func ListRules(c *gin.Context) { func ListRules(c *gin.Context) {
var ( var (
dnsRule Rule dnsRule Rule
res []Rule res []Rule
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&dnsRule); err != nil { if err := c.ShouldBind(&dnsRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -101,7 +110,7 @@ func ListRules(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -111,10 +120,10 @@ func ListRules(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -127,7 +136,7 @@ func ListRules(c *gin.Context) {
}) })
} }
// Create or update dns rule from user submit // UpsertRules creates or updates dns rule from user submit
func UpsertRules(c *gin.Context) { func UpsertRules(c *gin.Context) {
var ( var (
dnsRule Rule dnsRule Rule
@@ -137,7 +146,7 @@ func UpsertRules(c *gin.Context) {
if err := c.ShouldBind(&dnsRule); err != nil { if err := c.ShouldBind(&dnsRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -150,16 +159,16 @@ func UpsertRules(c *gin.Context) {
if err := dnsRule.CreateOrUpdate(); err != nil { if err := dnsRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
if update { if update {
log.Trace("DNS rule[id%d] has been updated", dnsRule.ID) log.Trace("DNS rule[id:%d] has been updated", dnsRule.ID)
} else { } else {
log.Trace("DNS rule[id%d] has been created", dnsRule.ID) log.Trace("DNS rule[id:%d] has been created", dnsRule.ID)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -169,14 +178,14 @@ func UpsertRules(c *gin.Context) {
}) })
} }
// Delete dns rule from user submit // DeleteRules Delete dns rule from user submit
func DeleteRules(c *gin.Context) { func DeleteRules(c *gin.Context) {
var dnsRule Rule var dnsRule Rule
if err := c.ShouldBind(&dnsRule); err != nil { if err := c.ShouldBind(&dnsRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -185,13 +194,13 @@ func DeleteRules(c *gin.Context) {
if err := dnsRule.Delete(); err != nil { if err := dnsRule.Delete(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
log.Trace("DNS rule[id%d] has been deleted", dnsRule.ID) log.Trace("DNS rule[id:%d] has been deleted", dnsRule.ID)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "succeed", "status": "succeed",
+125 -73
View File
@@ -1,6 +1,7 @@
package ftp package ftp
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"go/types" "go/types"
@@ -12,11 +13,10 @@ import (
"time" "time"
"github.com/li4n0/revsuit/internal/database" "github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/file"
"github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/recycler" "github.com/li4n0/revsuit/internal/recycler"
"github.com/li4n0/revsuit/internal/rule" "github.com/li4n0/revsuit/internal/rule"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/pkg/errors"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
@@ -24,6 +24,7 @@ type Server struct {
Config Config
rules []*Rule rules []*Rule
rulesLock sync.RWMutex rulesLock sync.RWMutex
livingLock sync.Mutex
dataChannel chan map[string]interface{} dataChannel chan map[string]interface{}
} }
@@ -64,7 +65,12 @@ func (s *Server) updateRules() error {
db := database.DB.Model(new(Rule)) db := database.DB.Model(new(Rule))
defer s.rulesLock.Unlock() defer s.rulesLock.Unlock()
s.rulesLock.Lock() s.rulesLock.Lock()
return db.Order("rank desc").Find(&s.rules).Error return errors.Wrap(db.Order("rank desc").Find(&s.rules).Error, "FTP update rules error")
}
func getClientPasvConnAddress(ip, port string) string {
dataPort, _ := strconv.Atoi(port)
return fmt.Sprintf("%s:%d", ip, dataPort+1)
} }
func (s *Server) authenticate(user, password string) (_rule *Rule, flag, flagGroup string, vars map[string]string) { func (s *Server) authenticate(user, password string) (_rule *Rule, flag, flagGroup string, vars map[string]string) {
@@ -99,7 +105,28 @@ func (s *Server) getPasvAddressFromCache(ip, pasvAddressTpl string) (pasvAddress
return pasvAddress return pasvAddress
} }
const (
NeedAccount = "332 Need account for login.\r\n"
PasswordPlease = "331 password please - version check\r\n"
PasswordError = "331 please specify the password\r\n"
UserLogged = "230 User logged in\r\n"
NoSuchFile = "550 %s: No such file or directory.\r\n"
CommandNotFound = "500 '%s': command not understood.\r\n"
EnteringPassiveMode = "227 Entering Passive Mode (%s,%v,%d)\r\n"
OpeningBinaryMode = "150 Opening BINARY mode data connection for '%s' (%d bytes).\r\n"
OpeningBinaryModeUpload = "150 Opening BINARY mode data connection for '%s'.\r\n"
TransferComplete = "226 Transfer complete.\r\n"
Goodbye = "221 Goodbye.\r\n"
DirectoryChanged = "250 Directory successfully changed.\r\n"
CurrentDirectory = "257 \"%s\" is the current directory\r\n"
)
func (s *Server) handleConnection(conn net.Conn) { func (s *Server) handleConnection(conn net.Conn) {
defer func() {
if err := recover(); err != nil {
recycler.Recycle(err)
}
}()
log.Trace("New FTP connection from addr [%s]", conn.RemoteAddr()) log.Trace("New FTP connection from addr [%s]", conn.RemoteAddr())
defer func() { defer func() {
_ = conn.Close() _ = conn.Close()
@@ -117,9 +144,9 @@ func (s *Server) handleConnection(conn net.Conn) {
} }
ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String()) ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
dataPort, _ := strconv.Atoi(port) clientPasvConnAddress := getClientPasvConnAddress(ip, port)
clientPasvConnAddress := fmt.Sprintf("%s:%d", ip, dataPort+1)
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
connBuf := bufio.NewWriter(conn)
var user, password, method, flag, flagGroup, pasvAddress, filename string var user, password, method, flag, flagGroup, pasvAddress, filename string
status := CRASHED status := CRASHED
@@ -148,23 +175,23 @@ loop:
log.Trace("FTP connection[%s] exec command: %s", conn.RemoteAddr(), strings.TrimRight(buf.String(), "\r\n")) log.Trace("FTP connection[%s] exec command: %s", conn.RemoteAddr(), strings.TrimRight(buf.String(), "\r\n"))
if _rule == nil && cmd != "USER" && cmd != "PASS" { if _rule == nil && cmd != "USER" && cmd != "PASS" {
_, _ = conn.Write([]byte("332 Need account for login.\r\n")) _, _ = connBuf.WriteString(NeedAccount)
break loop break loop
} }
switch cmd { switch cmd {
case "USER": case "USER":
user = args user = args
_, _ = conn.Write([]byte("331 password please - version check\r\n")) _, _ = connBuf.WriteString(PasswordPlease)
case "PASS": case "PASS":
password = args password = args
if _rule, flag, flagGroup, vars = s.authenticate(user, password); _rule == nil { if _rule, flag, flagGroup, vars = s.authenticate(user, password); _rule == nil {
_, _ = conn.Write([]byte("331 please specify the password\r\n")) _, _ = connBuf.WriteString(PasswordError)
break loop break loop
} }
log.Trace("FTP connection[%s] matched rule[rule_name: %s, flag: %s]", conn.RemoteAddr(), _rule.Name, flag) log.Trace("FTP connection[%s] matched rule[rule_name: %s, flag: %s]", conn.RemoteAddr(), _rule.Name, flag)
_, _ = conn.Write([]byte("230 User logged in\r\n")) _, _ = connBuf.WriteString(UserLogged)
if pasvAddress = s.getPasvAddressFromCache(ip, _rule.PasvAddress); pasvAddress == "" { if pasvAddress = s.getPasvAddressFromCache(ip, _rule.PasvAddress); pasvAddress == "" {
pasvAddress = fmt.Sprintf("%s:%d", s.PasvIP, s.PasvPort) pasvAddress = fmt.Sprintf("%s:%d", s.PasvIP, s.PasvPort)
@@ -174,13 +201,13 @@ loop:
case "SIZE": case "SIZE":
path += strings.TrimLeft(args, "/") path += strings.TrimLeft(args, "/")
if _rule == nil || isRedirect || len(_rule.Data) == 0 { if _rule == nil || isRedirect || len(_rule.Data) == 0 {
_, _ = conn.Write([]byte(fmt.Sprintf("550 %s: No such file or directory.\r\n", args))) _, _ = connBuf.WriteString(fmt.Sprintf(NoSuchFile, args))
break break
} }
_, _ = conn.Write([]byte(fmt.Sprintf("213 %d\r\n", len(_rule.Data)))) _, _ = connBuf.WriteString(fmt.Sprintf("213 %d\r\n", len(_rule.Data)))
case "EPSV", "EPRT", "PORT": case "EPSV", "EPRT", "PORT":
// refuse to use EPSV/EPRT/PORT in order to make the client to use PASV mode. // refuse to use EPSV/EPRT/PORT in order to make the client to use PASV mode.
_, _ = conn.Write([]byte(fmt.Sprintf("500 '%s': command not understood.\r\n", cmd))) _, _ = connBuf.WriteString(fmt.Sprintf(CommandNotFound, cmd))
case "PASV": case "PASV":
//Just so that ide does not prompt that there may be a nil value //Just so that ide does not prompt that there may be a nil value
if _rule != nil { if _rule != nil {
@@ -188,17 +215,17 @@ loop:
pasvAddress := rule.CompileTpl(pasvAddress, vars) pasvAddress := rule.CompileTpl(pasvAddress, vars)
pasvIP, pasvPort, err := net.SplitHostPort(pasvAddress) pasvIP, pasvPort, err := net.SplitHostPort(pasvAddress)
if err != nil { if err != nil {
log.Warn("FTP failed to split rule[id%d] pasv_address(%s) :%s", _rule.ID, pasvAddress, err) log.Warn("FTP failed to split rule[id:%d] pasv_address(%s) :%s", _rule.ID, pasvAddress, err)
break break
} }
port, err := strconv.Atoi(pasvPort) port, err := strconv.Atoi(pasvPort)
if err != nil { if err != nil {
log.Warn("FTP failed to convert rule[id%d] pasv_port(%s) :%s", _rule.ID, pasvPort, err) log.Warn("FTP failed to convert rule[id:%d] pasv_port(%s) :%s", _rule.ID, pasvPort, err)
break break
} }
ret := fmt.Sprintf("227 Entering Passive Mode (%s,%v,%d)\r\n", strings.ReplaceAll(pasvIP, ".", ","), float64(port/256), port%256) ret := fmt.Sprintf(EnteringPassiveMode, strings.ReplaceAll(pasvIP, ".", ","), float64(port/256), port%256)
_, _ = conn.Write([]byte(ret)) _, _ = connBuf.WriteString(ret)
if isRedirect { if isRedirect {
log.Trace("FTP connection[%s] will be redirect[pasv_address: %s]", conn.RemoteAddr(), pasvAddress) log.Trace("FTP connection[%s] will be redirect[pasv_address: %s]", conn.RemoteAddr(), pasvAddress)
} }
@@ -210,16 +237,18 @@ loop:
method = DOWNLOAD method = DOWNLOAD
//send data to client //send data to client
_, _ = conn.Write([]byte(fmt.Sprintf("150 Opening BINARY mode data connection for '%s' (%d bytes).\r\n", filename, len(_rule.Data)))) _, _ = connBuf.WriteString(fmt.Sprintf(OpeningBinaryMode, filename, len(_rule.Data)))
_ = connBuf.Flush()
s.dataChannel <- map[string]interface{}{clientPasvConnAddress: []byte(rule.CompileTpl(_rule.Data, vars))} s.dataChannel <- map[string]interface{}{clientPasvConnAddress: []byte(rule.CompileTpl(_rule.Data, vars))}
_, _ = conn.Write([]byte("226 Transfer complete.\r\n")) _, _ = connBuf.WriteString(TransferComplete)
} }
case "STOR": case "STOR":
filename = args filename = args
method = UPLOAD method = UPLOAD
_, _ = conn.Write([]byte(fmt.Sprintf("150 Opening BINARY mode data connection for '%s'.\r\n", filename))) _, _ = connBuf.WriteString(fmt.Sprintf(OpeningBinaryModeUpload, filename))
_ = connBuf.Flush()
//only could read data send to local pasv server. //only could read data send to local pasv server.
if !isRedirect { if !isRedirect {
dataChannel := make(chan []byte) dataChannel := make(chan []byte)
@@ -227,68 +256,36 @@ loop:
uploadData = <-dataChannel uploadData = <-dataChannel
log.Trace("FTP connection[%s] uploaded %d bytes", conn.RemoteAddr(), len(uploadData)) log.Trace("FTP connection[%s] uploaded %d bytes", conn.RemoteAddr(), len(uploadData))
} }
_, _ = conn.Write([]byte("226 Transfer complete.\r\n")) _, _ = connBuf.WriteString(TransferComplete)
case "QUIT": case "QUIT":
_, _ = conn.Write([]byte("221 Goodbye.\r\n")) _, _ = connBuf.WriteString(Goodbye)
status = FINISHED status = FINISHED
break loop break loop
case "CWD": case "CWD":
_, _ = conn.Write([]byte("250 Directory successfully changed.\r\n")) _, _ = connBuf.WriteString(DirectoryChanged)
path += strings.TrimRight(args, "\r\n") + "/" path += strings.TrimRight(args, "\r\n") + "/"
case "PWD": case "PWD":
_, _ = conn.Write([]byte(fmt.Sprintf("257 \"%s\" is the current directory\r\n", path))) _, _ = connBuf.WriteString(fmt.Sprintf(CurrentDirectory, path))
default: default:
_, _ = conn.Write([]byte("230 more data please!\r\n")) _, _ = conn.Write([]byte("230 more data please!\r\n"))
} }
_ = connBuf.Flush()
} }
buf = &bytes.Buffer{} buf = &bytes.Buffer{}
} }
if _rule != nil { if _rule != nil {
area := qqwry.Area(ip) createRecord(_rule, flag, flagGroup, user, password, method, path, filename, ip, uploadData, status)
var r *Record
var err error
// create new record
ftpFile := &file.FTPFile{}
if len(uploadData) != 0 {
ftpFile = &file.FTPFile{
Name: filename,
Content: uploadData,
}
}
r, err = NewRecord(_rule, flag, user, password, method, path, ip, area, ftpFile, status)
if err != nil {
log.Warn("FTP record[rule_id:%d] created failed :%s", _rule.ID, err)
return
}
log.Info("FTP record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
if flagGroup != "" {
var count int64
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("FTP record[id%d] has been put to client message queue", r.ID)
}
}
r.PushToClient()
log.Trace("FTP record[id%d] has been put to client message queue", r.ID)
}
//send notice
if _rule.Notice {
go func() {
r.Notice()
log.Trace("FTP record[id%d] notice has been sent", r.ID)
}()
}
} }
} }
func (s *Server) handlePasvConnection(conn net.Conn, data map[string]interface{}) { func (s *Server) handlePasvConnection(conn net.Conn, data map[string]interface{}) {
defer func() {
if err := recover(); err != nil {
recycler.Recycle(err)
}
}()
remoteAddress := conn.RemoteAddr().String() remoteAddress := conn.RemoteAddr().String()
switch v := data[remoteAddress].(type) { switch v := data[remoteAddress].(type) {
case types.Nil: case types.Nil:
@@ -312,42 +309,97 @@ func (s *Server) handlePasvConnection(conn net.Conn, data map[string]interface{}
_ = conn.Close() _ = conn.Close()
} }
func (s *Server) Run() {
if err := s.updateRules(); err != nil {
log.Fatal(err.Error())
}
// run pasv server // run pasv server
go func() { func (s *Server) runPasvServer() (net.Listener, error) {
pasvAddress := fmt.Sprintf("%s:%d", strings.Split(s.Addr, ":")[0], s.PasvPort) pasvAddress := fmt.Sprintf("%s:%d", strings.Split(s.Addr, ":")[0], s.PasvPort)
log.Info("Start to listen FTP PASV port at %v", pasvAddress) log.Info("Start to listen FTP PASV port at %v, PasvIP is %v", pasvAddress, s.PasvIP)
listener, err := net.Listen("tcp", pasvAddress) listener, err := net.Listen("tcp", pasvAddress)
if err != nil { if err != nil {
log.Fatal("FTP failed to listen on pasv port : %v", err) return nil, errors.Wrap(err, "FTP failed to listen on pasv port")
} }
go func() {
for data := range s.dataChannel { for data := range s.dataChannel {
tcpConn, err := listener.Accept() tcpConn, err := listener.Accept()
if err != nil { if err != nil {
if !strings.Contains(err.Error(), net.ErrClosed.Error()) {
log.Warn("FTP accept connection error: %v", err) log.Warn("FTP accept connection error: %v", err)
} else {
break
}
continue continue
} }
s.handlePasvConnection(tcpConn, data) s.handlePasvConnection(tcpConn, data)
} }
}() }()
return listener, nil
}
func (s *Server) Stop() {
log.Info("FTP Server is stopping...")
s.Enable = false
s.livingLock.Unlock()
}
func (s *Server) Restart() {
s.Stop()
time.Sleep(time.Second * 2)
go s.Run()
}
func (s *Server) Run() {
s.Enable = true
s.livingLock.Lock()
defer func() {
if s.Enable {
log.Error("FTP Server exited unexpectedly")
}
s.Enable = false
s.livingLock.Unlock()
}()
if err := s.updateRules(); err != nil {
log.Error(err.Error())
return
}
pasvListener, err := s.runPasvServer()
if err != nil {
log.Error(err.Error())
}
defer func() {
if pasvListener != nil {
_ = pasvListener.Close()
}
}()
// run ftp server // run ftp server
log.Info("Starting FTP Server at %v", s.Addr) log.Info("Starting FTP Server at %v", s.Addr)
listener, err := net.Listen("tcp", s.Addr) listener, err := net.Listen("tcp", s.Addr)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Error(errors.Wrap(err, "FTP failed to start").Error())
return
} }
for {
go func() {
s.livingLock.Lock()
if !s.Enable {
_ = listener.Close()
}
}()
for s.Enable {
tcpConn, err := listener.Accept() tcpConn, err := listener.Accept()
if err != nil { if err != nil {
if !strings.Contains(err.Error(), net.ErrClosed.Error()) {
log.Warn("FTP accept connection error: %v", err) log.Warn("FTP accept connection error: %v", err)
} else {
break
}
continue continue
} }
go s.handleConnection(tcpConn) go s.handleConnection(tcpConn)
} }
} }
+60 -4
View File
@@ -8,7 +8,9 @@ import (
"github.com/li4n0/revsuit/internal/database" "github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/file" "github.com/li4n0/revsuit/internal/file"
"github.com/li4n0/revsuit/internal/notice" "github.com/li4n0/revsuit/internal/notice"
"github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/record" "github.com/li4n0/revsuit/internal/record"
log "unknwon.dev/clog/v2"
) )
var _ record.Record = (*Record)(nil) var _ record.Record = (*Record)(nil)
@@ -57,12 +59,21 @@ func ListRecords(c *gin.Context) {
res []Record res []Record
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&ftpRecord); err != nil { if err := c.ShouldBind(&ftpRecord); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -98,7 +109,7 @@ func ListRecords(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -108,10 +119,10 @@ func ListRecords(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Preload("File").Order("id " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Preload("File").Order("id " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -123,3 +134,48 @@ func ListRecords(c *gin.Context) {
"result": gin.H{"count": count, "data": res}, "result": gin.H{"count": count, "data": res},
}) })
} }
func createRecord(_rule *Rule, flag, flagGroup, user, password, method, path, filename, ip string, uploadData []byte, status Status) {
// create new record
area := qqwry.Area(ip)
var ftpFile *file.FTPFile
var r *Record
var err error
if len(uploadData) != 0 {
ftpFile = &file.FTPFile{
Name: filename,
Content: uploadData,
}
}
r, err = NewRecord(_rule, flag, user, password, method, path, ip, area, ftpFile, status)
if err != nil {
log.Warn("FTP record[rule_id:%d] created failed :%s", _rule.ID, err)
return
}
log.Info("FTP record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
if flagGroup != "" {
var count int64
database.DB.Where("rule_name=? and (user like ? or password like ?)", _rule.Name, "%"+flagGroup+"%", "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("FTP record[id:%d, flagGroup:%s] has been put to client message queue", r.ID, flagGroup)
}
} else {
r.PushToClient()
log.Trace("FTP record[id:%d, flag:%s] has been put to client message queue", r.ID, flag)
}
}
//send notice
if _rule.Notice {
go func() {
r.Notice()
log.Trace("FTP record[id:%d] notice has been sent", r.ID)
}()
}
}
+25 -16
View File
@@ -10,10 +10,10 @@ import (
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
// FTP rule struct // Rule FTP rule struct
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule `yaml:",inline"`
PasvAddress string `gorm:"pasv_address" json:"pasv_address" form:"pasv_address"` PasvAddress string `gorm:"pasv_address" json:"pasv_address" form:"pasv_address" yaml:"pasv_address"`
Data []byte `json:"data" form:"data"` Data []byte `json:"data" form:"data"`
} }
@@ -75,12 +75,21 @@ func ListRules(c *gin.Context) {
res []Rule res []Rule
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&ftpRule); err != nil { if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -96,7 +105,7 @@ func ListRules(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -106,10 +115,10 @@ func ListRules(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -122,7 +131,7 @@ func ListRules(c *gin.Context) {
}) })
} }
// Create or update ftp rule from user submit // UpsertRules creates or updates ftp rule from user submit
func UpsertRules(c *gin.Context) { func UpsertRules(c *gin.Context) {
var ( var (
ftpRule Rule ftpRule Rule
@@ -132,7 +141,7 @@ func UpsertRules(c *gin.Context) {
if err := c.ShouldBind(&ftpRule); err != nil { if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -145,16 +154,16 @@ func UpsertRules(c *gin.Context) {
if err := ftpRule.CreateOrUpdate(); err != nil { if err := ftpRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
} }
if update { if update {
log.Trace("FTP rule[id%d] has been updated", ftpRule.ID) log.Trace("FTP rule[id:%d] has been updated", ftpRule.ID)
} else { } else {
log.Trace("FTP rule[id%d] has been created", ftpRule.ID) log.Trace("FTP rule[id:%d] has been created", ftpRule.ID)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -164,14 +173,14 @@ func UpsertRules(c *gin.Context) {
}) })
} }
// Delete ftp rule from user submit // DeleteRules deletes ftp rule from user submit
func DeleteRules(c *gin.Context) { func DeleteRules(c *gin.Context) {
var ftpRule Rule var ftpRule Rule
if err := c.ShouldBind(&ftpRule); err != nil { if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -180,13 +189,13 @@ func DeleteRules(c *gin.Context) {
if err := ftpRule.Delete(); err != nil { if err := ftpRule.Delete(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
log.Trace("FTP rule[id%d] has been deleted", ftpRule.ID) log.Trace("FTP rule[id:%d] has been deleted", ftpRule.ID)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "succeed", "status": "succeed",
+38 -9
View File
@@ -3,7 +3,6 @@ package mysql
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"os"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@@ -13,6 +12,7 @@ import (
"github.com/li4n0/revsuit/internal/file" "github.com/li4n0/revsuit/internal/file"
"github.com/li4n0/revsuit/internal/qqwry" "github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/pkg/mysql/vmysql" "github.com/li4n0/revsuit/pkg/mysql/vmysql"
"github.com/pkg/errors"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
"vitess.io/vitess/go/sqltypes" "vitess.io/vitess/go/sqltypes"
) )
@@ -27,6 +27,7 @@ type Server struct {
Config Config
rules []*Rule rules []*Rule
rulesLock sync.RWMutex rulesLock sync.RWMutex
livingLock sync.Mutex
listener *vmysql.Listener listener *vmysql.Listener
Handler vmysql.Handler Handler vmysql.Handler
@@ -36,7 +37,7 @@ type Server struct {
func GetServer() *Server { func GetServer() *Server {
once.Do(func() { once.Do(func() {
server = &Server{rulesLock: sync.RWMutex{}} server = &Server{rulesLock: sync.RWMutex{}, livingLock: sync.Mutex{}}
}) })
return server return server
} }
@@ -51,7 +52,7 @@ func (s *Server) updateRules() error {
db := database.DB.Model(new(Rule)) db := database.DB.Model(new(Rule))
defer s.rulesLock.Unlock() defer s.rulesLock.Unlock()
s.rulesLock.Lock() s.rulesLock.Lock()
return db.Order("rank desc").Find(&s.rules).Error return errors.Wrap(db.Order("rank desc").Find(&s.rules).Error, "MySQL update rules error")
} }
// NewConnection is part of the mysql.Handler interface. // NewConnection is part of the mysql.Handler interface.
@@ -146,14 +147,14 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
if _rule.PushToClient { if _rule.PushToClient {
if flagGroup != "" { if flagGroup != "" {
var count int64 var count int64
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count) database.DB.Where("rule_name=? and (user like ? or schema like ?)", _rule.Name, "%"+flagGroup+"%", "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 { if count <= 1 {
r.PushToClient() r.PushToClient()
log.Trace("MySQL record[id%d] has been put to client message queue", r.ID) log.Trace("MySQL record[id:%d, flagGroup:%s] has been put to client message queue", r.ID, flagGroup)
} }
} else { } else {
r.PushToClient() r.PushToClient()
log.Trace("MySQL record[id%d] has been put to client message queue", r.ID) log.Trace("MySQL record[id:%d, flag:%s] has been put to client message queue", r.ID, flag)
} }
} }
@@ -266,9 +267,31 @@ func (s *Server) WarningCount(c *vmysql.Conn) uint16 {
return 0 return 0
} }
func (s *Server) Stop() {
log.Info("MySQL Server is stopping...")
s.Enable = false
s.livingLock.Unlock()
}
func (s *Server) Restart() {
s.Enable = false
time.Sleep(time.Second * 2)
go s.Run()
}
func (s *Server) Run() { func (s *Server) Run() {
s.Enable = true
s.livingLock.Lock()
defer func() {
if s.Enable {
log.Error("MySQL Server exited unexpectedly")
}
s.Enable = false
s.livingLock.Unlock()
}()
if err := s.updateRules(); err != nil { if err := s.updateRules(); err != nil {
log.Fatal(err.Error()) log.Error(err.Error())
return
} }
s.Handler = s s.Handler = s
@@ -279,9 +302,15 @@ func (s *Server) Run() {
log.Info("Starting MySQL Server at %s", s.Addr) log.Info("Starting MySQL Server at %s", s.Addr)
s.listener, err = vmysql.NewListener("tcp", s.Addr, authServer, s, s.VersionString, 0, 0) s.listener, err = vmysql.NewListener("tcp", s.Addr, authServer, s, s.VersionString, 0, 0)
if err != nil { if err != nil {
log.Warn("New MySQL Server failed: %s", err) log.Error("New MySQL Server failed: %s", err)
os.Exit(-1)
} }
go func() {
if !s.Enable {
s.listener.Close()
}
}()
s.listener.Accept() s.listener.Accept()
} }
+13 -4
View File
@@ -55,12 +55,21 @@ func ListRecords(c *gin.Context) {
res []Record res []Record
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&mysqlRecord); err != nil { if err := c.ShouldBind(&mysqlRecord); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
} }
@@ -93,7 +102,7 @@ func ListRecords(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -103,10 +112,10 @@ func ListRecords(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Preload("Files").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Preload("Files").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
+27 -18
View File
@@ -11,9 +11,9 @@ import (
) )
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule `yaml:",inline"`
Files string `form:"files" json:"files"` Files string `form:"files" json:"files"`
ExploitJdbcClient bool `gorm:"exploit_jdbc_client" form:"exploit_jdbc_client" json:"exploit_jdbc_client"` ExploitJdbcClient bool `gorm:"exploit_jdbc_client" form:"exploit_jdbc_client" json:"exploit_jdbc_client" yaml:"exploit_jdbc_client"`
Payloads database.MapField `json:"payloads" form:"payloads"` Payloads database.MapField `json:"payloads" form:"payloads"`
} }
@@ -21,7 +21,7 @@ func (Rule) TableName() string {
return "mysql_rules" return "mysql_rules"
} }
// Create or update the mysql rule in database and ruleSet // CreateOrUpdate creates or updates the mysql rule in database and ruleSet
func (r *Rule) CreateOrUpdate() (err error) { func (r *Rule) CreateOrUpdate() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Clauses(clause.OnConflict{ err = db.Clauses(clause.OnConflict{
@@ -45,7 +45,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
return err return err
} }
// Delete the mysql rule in database and ruleSet // Delete deletes the mysql rule in database and ruleSet
func (r *Rule) Delete() (err error) { func (r *Rule) Delete() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Delete(r).Error err = db.Delete(r).Error
@@ -56,19 +56,28 @@ func (r *Rule) Delete() (err error) {
return err return err
} }
// List all mysql rules those satisfy the filter // ListRules lists all mysql rules those satisfy the filter
func ListRules(c *gin.Context) { func ListRules(c *gin.Context) {
var ( var (
mysqlRule Rule mysqlRule Rule
res []Rule res []Rule
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&mysqlRule); err != nil { if err := c.ShouldBind(&mysqlRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -84,7 +93,7 @@ func ListRules(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -94,10 +103,10 @@ func ListRules(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -110,7 +119,7 @@ func ListRules(c *gin.Context) {
}) })
} }
// Create or update mysql rule from user submit // UpsertRules create or update mysql rule from user submit
func UpsertRules(c *gin.Context) { func UpsertRules(c *gin.Context) {
var ( var (
mysqlRule Rule mysqlRule Rule
@@ -120,7 +129,7 @@ func UpsertRules(c *gin.Context) {
if err := c.ShouldBind(&mysqlRule); err != nil { if err := c.ShouldBind(&mysqlRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -133,16 +142,16 @@ func UpsertRules(c *gin.Context) {
if err := mysqlRule.CreateOrUpdate(); err != nil { if err := mysqlRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
if update { if update {
log.Trace("MySQL rule[id%d] has been updated", mysqlRule.ID) log.Trace("MySQL rule[id:%d] has been updated", mysqlRule.ID)
} else { } else {
log.Trace("MySQL rule[id%d] has been created", mysqlRule.ID) log.Trace("MySQL rule[id:%d] has been created", mysqlRule.ID)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -152,14 +161,14 @@ func UpsertRules(c *gin.Context) {
}) })
} }
// Delete mysql rule from user submit // DeleteRules Delete mysql rule from user submit
func DeleteRules(c *gin.Context) { func DeleteRules(c *gin.Context) {
var mysqlRule Rule var mysqlRule Rule
if err := c.ShouldBind(&mysqlRule); err != nil { if err := c.ShouldBind(&mysqlRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -168,13 +177,13 @@ func DeleteRules(c *gin.Context) {
if err := mysqlRule.Delete(); err != nil { if err := mysqlRule.Delete(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
log.Trace("MySQL rule[id%d] has been deleted", mysqlRule.ID) log.Trace("MySQL rule[id:%d] has been deleted", mysqlRule.ID)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "succeed", "status": "succeed",
+12 -12
View File
@@ -259,7 +259,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
// Catch panics, and close the connection in any case. // Catch panics, and close the connection in any case.
defer func() { defer func() {
if x := recover(); x != nil { if x := recover(); x != nil {
log.Warn("mysql_server caught panic:\n%v\n%s", x, tb.Stack(4)) log.Trace("mysql_server caught panic:\n%v\n%s", x, tb.Stack(4))
} }
// We call flush here in case there's a premature return after // We call flush here in case there's a premature return after
// startWriterBuffering is called // startWriterBuffering is called
@@ -275,7 +275,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
salt, err := c.writeHandshakeV10(l.ServerVersion, l.authServer, l.TLSConfig != nil) salt, err := c.writeHandshakeV10(l.ServerVersion, l.authServer, l.TLSConfig != nil)
if err != nil { if err != nil {
if err != io.EOF { if err != io.EOF {
log.Warn("Cannot send HandshakeV10 packet to %s: %v", c, err) log.Trace("Cannot send HandshakeV10 packet to %s: %v", c, err)
} }
return return
} }
@@ -286,13 +286,13 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
if err != nil { if err != nil {
// Don't log EOF errors. They cause too much spam, same as main read loop. // Don't log EOF errors. They cause too much spam, same as main read loop.
if err != io.EOF { if err != io.EOF {
log.Warn("Cannot read client handshake response from %s: %v", c, err) log.Trace("Cannot read client handshake response from %s: %v", c, err)
} }
return return
} }
user, authMethod, authResponse, err := l.parseClientHandshakePacket(c, true, response) user, authMethod, authResponse, err := l.parseClientHandshakePacket(c, true, response)
if err != nil { if err != nil {
log.Warn("Cannot parse client handshake response from %s: %v", c, err) log.Trace("Cannot parse client handshake response from %s: %v", c, err)
return return
} }
@@ -305,14 +305,14 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
// SSL was enabled. We need to re-read the auth packet. // SSL was enabled. We need to re-read the auth packet.
response, err = c.readEphemeralPacket() response, err = c.readEphemeralPacket()
if err != nil { if err != nil {
log.Warn("Cannot read post-SSL client handshake response from %s: %v", c, err) log.Trace("Cannot read post-SSL client handshake response from %s: %v", c, err)
return return
} }
// Returns copies of the data, so we can recycle the buffer. // Returns copies of the data, so we can recycle the buffer.
user, authMethod, authResponse, err = l.parseClientHandshakePacket(c, false, response) user, authMethod, authResponse, err = l.parseClientHandshakePacket(c, false, response)
if err != nil { if err != nil {
log.Warn("Cannot parse post-SSL client handshake response from %s: %v", c, err) log.Trace("Cannot parse post-SSL client handshake response from %s: %v", c, err)
return return
} }
c.RecycleReadPacket() c.RecycleReadPacket()
@@ -366,13 +366,13 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
data := make([]byte, 21) //nolint:ineffassign,staticcheck // SA4006 This line is required because the binary protocol requires padding with 0 data := make([]byte, 21) //nolint:ineffassign,staticcheck // SA4006 This line is required because the binary protocol requires padding with 0
data = append(salt, byte(0x00)) data = append(salt, byte(0x00))
if err := c.writeAuthSwitchRequest(MysqlNativePassword, data); err != nil { if err := c.writeAuthSwitchRequest(MysqlNativePassword, data); err != nil {
log.Warn("Error writing auth switch packet for %s: %v", c, err) log.Trace("Error writing auth switch packet for %s: %v", c, err)
return return
} }
response, err := c.readEphemeralPacket() response, err := c.readEphemeralPacket()
if err != nil { if err != nil {
log.Warn("Error reading auth switch response for %s: %v", c, err) log.Trace("Error reading auth switch response for %s: %v", c, err)
return return
} }
c.RecycleReadPacket() c.RecycleReadPacket()
@@ -402,7 +402,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
data = authServerDialogSwitchData() data = authServerDialogSwitchData()
} }
if err := c.writeAuthSwitchRequest(authServerMethod, data); err != nil { if err := c.writeAuthSwitchRequest(authServerMethod, data); err != nil {
log.Warn("Error writing auth switch packet for %s: %v", c, err) log.Trace("Error writing auth switch packet for %s: %v", c, err)
return return
} }
@@ -424,7 +424,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
// Negotiation worked, send OK packet. // Negotiation worked, send OK packet.
if err := c.writeOKPacket(0, 0, c.StatusFlags, 0); err != nil { if err := c.writeOKPacket(0, 0, c.StatusFlags, 0); err != nil {
log.Warn("Cannot write OK packet to %s: %v", c, err) log.Trace("Cannot write OK packet to %s: %v", c, err)
return return
} }
@@ -435,7 +435,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32, acceptTime time.Ti
connectTime := time.Since(acceptTime) connectTime := time.Since(acceptTime)
if l.SlowConnectWarnThreshold != 0 && connectTime > l.SlowConnectWarnThreshold { if l.SlowConnectWarnThreshold != 0 && connectTime > l.SlowConnectWarnThreshold {
connSlow.Add(1) connSlow.Add(1)
log.Warn("Slow connection from %s: %v", c, connectTime) log.Trace("Slow connection from %s: %v", c, connectTime)
} }
for { for {
@@ -692,7 +692,7 @@ func (l *Listener) parseClientHandshakePacket(c *Conn, firstTime bool, data []by
// Decode connection attributes send by the client // Decode connection attributes send by the client
if clientFlags&CapabilityClientConnAttr != 0 { if clientFlags&CapabilityClientConnAttr != 0 {
if connAttrs, _, err := parseConnAttrs(data, pos); err != nil { if connAttrs, _, err := parseConnAttrs(data, pos); err != nil {
log.Warn("Decode connection attributes send by the client: %v", err) log.Trace("Decode connection attributes send by the client: %v", err)
} else { } else {
c.ConnAttrs = connAttrs c.ConnAttrs = connAttrs
} }
+38 -9
View File
@@ -1,6 +1,7 @@
package rhttp package rhttp
import ( import (
"context"
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@@ -8,6 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/internal/database" "github.com/li4n0/revsuit/internal/database"
@@ -23,6 +25,8 @@ type Server struct {
ApiGroup *gin.RouterGroup ApiGroup *gin.RouterGroup
rules []*Rule rules []*Rule
rulesLock sync.RWMutex rulesLock sync.RWMutex
httpServer http.Server
} }
const ( const (
@@ -86,14 +90,38 @@ func (s *Server) updateRules() error {
return db.Order("rank desc").Find(&s.rules).Error return db.Order("rank desc").Find(&s.rules).Error
} }
func (s *Server) startHttpServer() {
log.Info("Starting HTTP Server at %s, token:%s", s.Addr, s.Token)
s.httpServer = http.Server{
Addr: s.Addr,
Handler: s.Router,
}
err := s.httpServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err.Error())
}
}
func (s *Server) stopHttpServer() {
log.Info("HTTP Server is stopping...")
err := s.httpServer.Shutdown(context.TODO())
if err != nil {
log.Fatal(err.Error())
}
}
func (s *Server) Restart() {
//only need to stop http server, because it will start in a loop
s.stopHttpServer()
}
func (s *Server) Run() { func (s *Server) Run() {
if err := s.updateRules(); err != nil { if err := s.updateRules(); err != nil {
log.Warn(err.Error()) log.Warn(err.Error())
} }
log.Info("Starting HTTP Server at %s, token:%s", s.Addr, s.Token) for {
err := s.Router.Run(s.Addr) s.startHttpServer()
if err != nil { time.Sleep(2 * time.Second)
log.Fatal(err.Error())
} }
} }
@@ -167,21 +195,22 @@ func (s *Server) Receive(c *gin.Context) {
if _rule.PushToClient { if _rule.PushToClient {
if flagGroup != "" { if flagGroup != "" {
var count int64 var count int64
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count) database.DB.Where("rule_name=? and raw_request like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 { if count <= 1 {
r.PushToClient() r.PushToClient()
log.Trace("HTTP record[id%d] has been put to client message queue", r.ID) log.Trace("HTTP record[id:%d, flagGroup:%s] has been put to client message queue", r.ID, flagGroup)
}
} }
} else {
r.PushToClient() r.PushToClient()
log.Trace("HTTP record[id%d] has been put to client message queue", r.ID) log.Trace("HTTP record[id:%d, flag:%s] has been put to client message queue", r.ID, r.Flag)
}
} }
//send notice //send notice
if _rule.Notice { if _rule.Notice {
go func() { go func() {
r.Notice() r.Notice()
log.Trace("HTTP record[id%d] notice has been sent", r.ID) log.Trace("HTTP record[id:%d] notice has been sent", r.ID)
}() }()
} }
+14 -4
View File
@@ -50,12 +50,22 @@ func ListRecords(c *gin.Context) {
res []Record res []Record
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
pageSize = n
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&httpRecord); err != nil { if err := c.ShouldBind(&httpRecord); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -82,7 +92,7 @@ func ListRecords(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -92,10 +102,10 @@ func ListRecords(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
+31 -22
View File
@@ -10,19 +10,19 @@ import (
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
// Http rule struct // Rule Http rule struct
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule `yaml:",inline"`
ResponseStatusCode string `gorm:"index;default:200;not null" form:"response_status_code" json:"response_status_code"` ResponseStatusCode string `gorm:"index;default:200;not null" form:"response_status_code" json:"response_status_code" yaml:"response_status_code"`
ResponseHeaders database.MapField `form:"response_headers" json:"response_headers"` ResponseHeaders database.MapField `form:"response_headers" json:"response_headers" yaml:"response_headers"`
ResponseBody string `gorm:"default:Hello RevSuit!" form:"response_body" json:"response_body"` ResponseBody string `gorm:"default:Hello RevSuit!" form:"response_body" json:"response_body" yaml:"response_body"`
} }
func (Rule) TableName() string { func (Rule) TableName() string {
return "http_rules" return "http_rules"
} }
// New http rule struct // NewRule new http rule struct
func NewRule(name, flagFormat, responseBody string, pushToClient, notice bool, responseStatus string, responseHeaders database.MapField) *Rule { func NewRule(name, flagFormat, responseBody string, pushToClient, notice bool, responseStatus string, responseHeaders database.MapField) *Rule {
return &Rule{ return &Rule{
BaseRule: rule.BaseRule{ BaseRule: rule.BaseRule{
@@ -37,7 +37,7 @@ func NewRule(name, flagFormat, responseBody string, pushToClient, notice bool, r
} }
} }
// Create or update the http rule in database and ruleSet // CreateOrUpdate creates or updates the http rule in database and ruleSet
func (r *Rule) CreateOrUpdate() (err error) { func (r *Rule) CreateOrUpdate() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Clauses(clause.OnConflict{ err = db.Clauses(clause.OnConflict{
@@ -62,7 +62,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
return err return err
} }
// Delete the http rule in database and ruleSet // Delete deletes the http rule in database and ruleSet
func (r *Rule) Delete() (err error) { func (r *Rule) Delete() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Delete(r).Error err = db.Delete(r).Error
@@ -74,19 +74,28 @@ func (r *Rule) Delete() (err error) {
return err return err
} }
// List all http rules those satisfy the filter // ListRules lists all http rules those satisfy the filter
func ListRules(c *gin.Context) { func ListRules(c *gin.Context) {
var ( var (
httpRule Rule httpRule Rule
res []Rule res []Rule
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&httpRule); err != nil { if err := c.ShouldBind(&httpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -102,7 +111,7 @@ func ListRules(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -112,10 +121,10 @@ func ListRules(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -128,7 +137,7 @@ func ListRules(c *gin.Context) {
}) })
} }
// Create or update http rule from user submit // UpsertRules create or update http rule from user submit
func UpsertRules(c *gin.Context) { func UpsertRules(c *gin.Context) {
var ( var (
httpRule Rule httpRule Rule
@@ -138,7 +147,7 @@ func UpsertRules(c *gin.Context) {
if err := c.ShouldBind(&httpRule); err != nil { if err := c.ShouldBind(&httpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -151,16 +160,16 @@ func UpsertRules(c *gin.Context) {
if err := httpRule.CreateOrUpdate(); err != nil { if err := httpRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
} }
if update { if update {
log.Trace("HTTP rule[id%d] has been updated", httpRule.ID) log.Trace("HTTP rule[id:%d] has been updated", httpRule.ID)
} else { } else {
log.Trace("HTTP rule[id%d] has been created", httpRule.ID) log.Trace("HTTP rule[id:%d] has been created", httpRule.ID)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -170,14 +179,14 @@ func UpsertRules(c *gin.Context) {
}) })
} }
// Delete http rule from user submit // DeleteRules deletes http rule from user submit
func DeleteRules(c *gin.Context) { func DeleteRules(c *gin.Context) {
var httpRule Rule var httpRule Rule
if err := c.ShouldBind(&httpRule); err != nil { if err := c.ShouldBind(&httpRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -186,13 +195,13 @@ func DeleteRules(c *gin.Context) {
if err := httpRule.Delete(); err != nil { if err := httpRule.Delete(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
log.Trace("HTTP rule[id%d] has been deleted", httpRule.ID) log.Trace("HTTP rule[id:%d] has been deleted", httpRule.ID)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "succeed", "status": "succeed",
+13 -4
View File
@@ -46,12 +46,21 @@ func ListRecords(c *gin.Context) {
res []Record res []Record
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&rmiRecord); err != nil { if err := c.ShouldBind(&rmiRecord); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -75,7 +84,7 @@ func ListRecords(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -85,10 +94,10 @@ func ListRecords(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
+48 -11
View File
@@ -12,6 +12,7 @@ import (
"github.com/li4n0/revsuit/internal/database" "github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/qqwry" "github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/recycler" "github.com/li4n0/revsuit/internal/recycler"
"github.com/pkg/errors"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
@@ -19,6 +20,7 @@ type Server struct {
Config Config
rules []*Rule rules []*Rule
rulesLock sync.RWMutex rulesLock sync.RWMutex
livingLock sync.Mutex
} }
var ( var (
@@ -28,7 +30,7 @@ var (
func GetServer() *Server { func GetServer() *Server {
once.Do(func() { once.Do(func() {
server = &Server{rulesLock: sync.RWMutex{}} server = &Server{rulesLock: sync.RWMutex{}, livingLock: sync.Mutex{}}
}) })
return server return server
} }
@@ -43,7 +45,7 @@ func (s *Server) updateRules() error {
db := database.DB.Model(new(Rule)) db := database.DB.Model(new(Rule))
defer s.rulesLock.Unlock() defer s.rulesLock.Unlock()
s.rulesLock.Lock() s.rulesLock.Lock()
return db.Order("rank desc").Find(&s.rules).Error return errors.Wrap(db.Order("rank desc").Find(&s.rules).Error, "RMI update rules error")
} }
func (s *Server) handleConnection(conn net.Conn) { func (s *Server) handleConnection(conn net.Conn) {
@@ -119,29 +121,53 @@ func (s *Server) handleConnection(conn net.Conn) {
if _rule.PushToClient { if _rule.PushToClient {
if flagGroup != "" { if flagGroup != "" {
var count int64 var count int64
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count) database.DB.Where("rule_name=? and path like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 { if count <= 1 {
r.PushToClient() r.PushToClient()
log.Trace("RMI record[id%d] has been put to client message queue", r.ID) log.Trace("RMI record[id:%d, flagGroup:%s] has been put to client message queue", r.ID, flagGroup)
}
} }
} else {
r.PushToClient() r.PushToClient()
log.Trace("RMI record[id%d] has been put to client message queue", r.ID) log.Trace("RMI record[id:%d, flag:%s] has been put to client message queue", r.ID, flag)
}
} }
//send notice //send notice
if _rule.Notice { if _rule.Notice {
go func() { go func() {
r.Notice() r.Notice()
log.Trace("RMI record[id%d] notice has been sent", r.ID) log.Trace("RMI record[id:%d] notice has been sent", r.ID)
}() }()
} }
} }
} }
func (s *Server) Stop() {
log.Info("RMI Server is stopping...")
s.Enable = false
s.livingLock.Unlock()
}
func (s *Server) Restart() {
s.Enable = false
time.Sleep(time.Second * 2)
go s.Run()
}
func (s *Server) Run() { func (s *Server) Run() {
s.Enable = true
s.livingLock.Lock()
defer func() {
if s.Enable {
log.Error("RMI Server exited unexpectedly")
}
s.Enable = false
s.livingLock.Unlock()
}()
if err := s.updateRules(); err != nil { if err := s.updateRules(); err != nil {
log.Fatal(err.Error()) log.Error(err.Error())
return
} }
// run server // run server
@@ -149,16 +175,27 @@ func (s *Server) Run() {
listener, err := net.Listen("tcp", s.Addr) listener, err := net.Listen("tcp", s.Addr)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Error(err.Error())
return
} }
for { go func() {
s.livingLock.Lock()
if !s.Enable {
_ = listener.Close()
}
}()
for s.Enable {
tcpConn, err := listener.Accept() tcpConn, err := listener.Accept()
if err != nil { if err != nil {
if !strings.Contains(err.Error(), net.ErrClosed.Error()) {
log.Warn("RMI accept connection error: %v", err) log.Warn("RMI accept connection error: %v", err)
} else {
break
}
continue continue
} }
go s.handleConnection(tcpConn) go s.handleConnection(tcpConn)
} }
} }
+28 -19
View File
@@ -10,16 +10,16 @@ import (
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
// RMI rule struct // Rule RMI rule struct
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule `yaml:",inline"`
} }
func (Rule) TableName() string { func (Rule) TableName() string {
return "rmi_rules" return "rmi_rules"
} }
// New rmi rule struct // NewRule new rmi rule struct
func NewRule(name, flagFormat string, pushToClient, notice bool) *Rule { func NewRule(name, flagFormat string, pushToClient, notice bool) *Rule {
return &Rule{ return &Rule{
BaseRule: rule.BaseRule{ BaseRule: rule.BaseRule{
@@ -31,7 +31,7 @@ func NewRule(name, flagFormat string, pushToClient, notice bool) *Rule {
} }
} }
// Create or update the rmi rule in database and ruleSet // CreateOrUpdate creates or updates the rmi rule in database and ruleSet
func (r *Rule) CreateOrUpdate() (err error) { func (r *Rule) CreateOrUpdate() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Clauses(clause.OnConflict{ err = db.Clauses(clause.OnConflict{
@@ -53,7 +53,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
return err return err
} }
// Delete the rmi rule in database and ruleSet // Delete deletes the rmi rule in database and ruleSet
func (r *Rule) Delete() (err error) { func (r *Rule) Delete() (err error) {
db := database.DB.Model(r) db := database.DB.Model(r)
err = db.Delete(r).Error err = db.Delete(r).Error
@@ -65,19 +65,28 @@ func (r *Rule) Delete() (err error) {
return err return err
} }
// List all rmi rules those satisfy the filter // ListRules lists all rmi rules those satisfy the filter
func ListRules(c *gin.Context) { func ListRules(c *gin.Context) {
var ( var (
rmiRule Rule rmiRule Rule
res []Rule res []Rule
count int64 count int64
order = c.Query("order") order = c.Query("order")
pageSize int
) )
if c.Query("pageSize") == "" {
pageSize = 10
} else if n, err := strconv.Atoi(c.Query("pageSize")); err == nil {
if n <= 0 || n > 100 {
pageSize = 10
}
}
if err := c.ShouldBind(&rmiRule); err != nil { if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -93,7 +102,7 @@ func ListRules(c *gin.Context) {
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
@@ -103,10 +112,10 @@ func ListRules(c *gin.Context) {
order = "desc" order = "desc"
} }
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil { if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * pageSize).Limit(pageSize).Find(&res).Error; err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -119,7 +128,7 @@ func ListRules(c *gin.Context) {
}) })
} }
// Create or update rmi rule from user submit // UpsertRules creates or updates rmi rule from user submit
func UpsertRules(c *gin.Context) { func UpsertRules(c *gin.Context) {
var ( var (
rmiRule Rule rmiRule Rule
@@ -129,7 +138,7 @@ func UpsertRules(c *gin.Context) {
if err := c.ShouldBind(&rmiRule); err != nil { if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -142,16 +151,16 @@ func UpsertRules(c *gin.Context) {
if err := rmiRule.CreateOrUpdate(); err != nil { if err := rmiRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"result": nil, "result": nil,
}) })
return return
} }
if update { if update {
log.Trace("RMI rule[id%d] has been updated", rmiRule.ID) log.Trace("RMI rule[id:%d] has been updated", rmiRule.ID)
} else { } else {
log.Trace("RMI rule[id%d] has been created", rmiRule.ID) log.Trace("RMI rule[id:%d] has been created", rmiRule.ID)
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
@@ -161,14 +170,14 @@ func UpsertRules(c *gin.Context) {
}) })
} }
// Delete rmi rule from user submit // DeleteRules deletes rmi rule from user submit
func DeleteRules(c *gin.Context) { func DeleteRules(c *gin.Context) {
var rmiRule Rule var rmiRule Rule
if err := c.ShouldBind(&rmiRule); err != nil { if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
@@ -177,13 +186,13 @@ func DeleteRules(c *gin.Context) {
if err := rmiRule.Delete(); err != nil { if err := rmiRule.Delete(); err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"status": "failed", "status": "failed",
"error": err, "error": err.Error(),
"data": nil, "data": nil,
}) })
return return
} }
log.Trace("RMI rule[id%d] has been deleted", rmiRule.ID) log.Trace("RMI rule[id:%d] has been deleted", rmiRule.ID)
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": "succeed", "status": "succeed",
+1 -1
View File
@@ -19,7 +19,7 @@ type Config struct {
Addr string Addr string
Token string Token string
Database string Database string
LogLevel string LogLevel string `yaml:"log_level"`
Notice noticeConfig Notice noticeConfig
rhttp.Config rhttp.Config
DNS dns.Config DNS dns.Config
+10 -3
View File
@@ -27,9 +27,8 @@ func ping(c *gin.Context) {
} }
func events(c *gin.Context) { func events(c *gin.Context) {
log.Info("Receive connection from %v", c.Request.RemoteAddr) log.Info("Receive client connection from %v", c.Request.RemoteAddr)
c.Stream(func(w io.Writer) bool { c.Stream(func(w io.Writer) bool {
c.SSEvent("message", "connect succeed")
select { select {
case <-c.Writer.CloseNotify(): case <-c.Writer.CloseNotify():
return false return false
@@ -38,7 +37,7 @@ func events(c *gin.Context) {
} }
return true return true
}) })
log.Info(c.Request.RemoteAddr, "disconnect") log.Info("Client %s disconnect", c.Request.RemoteAddr)
} }
func recovery(c *gin.Context) { func recovery(c *gin.Context) {
@@ -85,3 +84,11 @@ func recovery(c *gin.Context) {
}() }()
c.Next() c.Next()
} }
func version(c *gin.Context) {
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": VERSION,
})
}
+18
View File
@@ -42,6 +42,7 @@ func (revsuit *Revsuit) registerPlatformRouter() {
api.GET("/auth", auth) api.GET("/auth", auth)
api.GET("/events", events) api.GET("/events", events)
api.GET("/ping", ping) api.GET("/ping", ping)
api.GET("/version", version)
} }
func (revsuit *Revsuit) registerHttpRouter() { func (revsuit *Revsuit) registerHttpRouter() {
@@ -54,6 +55,23 @@ func (revsuit *Revsuit) registerHttpRouter() {
} }
revsuit.http.Router.StaticFS("/revsuit/admin", http.FS(fe)) revsuit.http.Router.StaticFS("/revsuit/admin", http.FS(fe))
// init settings router group
settingsGroup := revsuit.http.ApiGroup.Group("setting")
settingsGroup.GET("/exportRules", exportRules)
settingsGroup.POST("/importRules", importRules)
settingsGroup.GET("/getHttpConfig", revsuit.getHttpConfig)
settingsGroup.POST("/updateHttpConfig", revsuit.updateHttpConfig)
settingsGroup.GET("/getDnsConfig", revsuit.getDnsConfig)
settingsGroup.POST("/updateDnsConfig", revsuit.updateDnsConfig)
settingsGroup.GET("/getRmiConfig", revsuit.getRmiConfig)
settingsGroup.POST("/updateRmiConfig", revsuit.updateRmiConfig)
settingsGroup.GET("/getMySQLConfig", revsuit.getMySQLConfig)
settingsGroup.POST("/updateMySQLConfig", revsuit.updateMySQLConfig)
settingsGroup.GET("/getFtpConfig", revsuit.getFtpConfig)
settingsGroup.POST("/updateFtpConfig", revsuit.updateFtpConfig)
settingsGroup.GET("/getNoticeConfig", revsuit.getNoticeConfig)
settingsGroup.POST("/updateNoticeConfig", revsuit.updateNoticeConfig)
// init record router group // init record router group
recordGroup := revsuit.http.ApiGroup.Group("/record") recordGroup := revsuit.http.ApiGroup.Group("/record")
+25 -15
View File
@@ -14,7 +14,10 @@ import (
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
const VERSION = "Beta0.1"
type Revsuit struct { type Revsuit struct {
config *Config
logLevel log.Level logLevel log.Level
http *http.Server http *http.Server
@@ -25,6 +28,11 @@ type Revsuit struct {
} }
func initDatabase(dsn string) { func initDatabase(dsn string) {
_ = log.NewConsole(100,
log.ConsoleConfig{
Level: log.LevelInfo,
})
err := database.InitDB("sqlite", dsn) err := database.InitDB("sqlite", dsn)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
@@ -104,6 +112,10 @@ func initLog(level string) (logLevel log.Level) {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
database.DB.Logger.LogMode(logger.Error) database.DB.Logger.LogMode(logger.Error)
logLevel = log.LevelFatal logLevel = log.LevelFatal
default:
gin.SetMode(gin.DebugMode)
database.DB.Logger.LogMode(logger.Info)
logLevel = log.LevelInfo
} }
_ = log.NewConsole(100, _ = log.NewConsole(100,
log.ConsoleConfig{ log.ConsoleConfig{
@@ -138,29 +150,27 @@ func initNotice(nc noticeConfig) {
func New(c *Config) *Revsuit { func New(c *Config) *Revsuit {
logLevel := initLog(c.LogLevel)
initDatabase(c.Database) initDatabase(c.Database)
logLevel := initLog(c.LogLevel)
initNotice(c.Notice) initNotice(c.Notice)
s := &Revsuit{ s := &Revsuit{
config: c,
logLevel: logLevel, logLevel: logLevel,
http: http.GetServer(), http: http.GetServer(),
} }
if c.DNS.Enable {
s.dns = dns.GetServer() s.dns = dns.GetServer()
} s.dns.Config = c.DNS
if c.MySQL.Enable {
s.mysql = mysql.GetServer() s.mysql = mysql.GetServer()
s.mysql.Config = c.MySQL s.mysql.Config = c.MySQL
}
if c.RMI.Enable {
s.rmi = rmi.GetServer() s.rmi = rmi.GetServer()
s.rmi.Config = c.RMI s.rmi.Config = c.RMI
}
if c.FTP.Enable {
s.ftp = ftp.GetServer() s.ftp = ftp.GetServer()
s.ftp.Config = c.FTP s.ftp.Config = c.FTP
}
if c.Addr != "" { if c.Addr != "" {
s.http.SetAddr(c.Addr) s.http.SetAddr(c.Addr)
@@ -178,16 +188,16 @@ func (revsuit *Revsuit) Run() {
defer log.Stop() defer log.Stop()
revsuit.registerRouter() revsuit.registerRouter()
if revsuit.dns != nil { if revsuit.dns != nil && revsuit.dns.Enable {
go revsuit.dns.Run() go revsuit.dns.Run()
} }
if revsuit.mysql != nil { if revsuit.rmi != nil && revsuit.rmi.Enable {
go revsuit.mysql.Run()
}
if revsuit.rmi != nil {
go revsuit.rmi.Run() go revsuit.rmi.Run()
} }
if revsuit.ftp != nil { if revsuit.mysql != nil && revsuit.mysql.Enable {
go revsuit.mysql.Run()
}
if revsuit.ftp != nil && revsuit.ftp.Enable {
go revsuit.ftp.Run() go revsuit.ftp.Run()
} }
+367
View File
@@ -0,0 +1,367 @@
package server
import (
"fmt"
"io"
"time"
"github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
log "unknwon.dev/clog/v2"
)
type Rules struct {
Http []rhttp.Rule
Dns []dns.Rule
Mysql []mysql.Rule
Rmi []rmi.Rule
Ftp []ftp.Rule
}
func exportRules(c *gin.Context) {
var (
db = database.DB
rules Rules
)
db.Model(&rhttp.Rule{}).Find(&rules.Http)
db.Model(&dns.Rule{}).Find(&rules.Dns)
db.Model(&mysql.Rule{}).Find(&rules.Mysql)
db.Model(&rmi.Rule{}).Find(&rules.Rmi)
db.Model(&ftp.Rule{}).Find(&rules.Ftp)
out, err := yaml.Marshal(rules)
if err != nil {
log.Warn("export rules error: %s", err)
}
c.Header("Content-Disposition", fmt.Sprintf("attachment;filename=revsuit_rules_%s.yaml", time.Now().Format("20060102150405")))
c.String(200, string(out))
}
func importRules(c *gin.Context) {
var (
db = database.DB
rules Rules
count int
errs []string
)
f, err := c.FormFile("rules")
if err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
log.Trace("%v", err)
return
}
file, _ := f.Open()
content, err := io.ReadAll(file)
if err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
log.Trace("%v", err)
return
}
err = yaml.Unmarshal(content, &rules)
if err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
return
}
for _, rule := range rules.Http {
err := db.Model(&rhttp.Rule{}).Create(&rule).Error
if err != nil {
errs = append(errs, errors.Wrap(err, fmt.Sprintf("http rule[%s]", rule.Name)).Error())
continue
}
count += 1
}
for _, rule := range rules.Dns {
err := db.Model(&dns.Rule{}).Create(&rule).Error
if err != nil {
errs = append(errs, errors.Wrap(err, fmt.Sprintf("dns rule[%s]", rule.Name)).Error())
continue
}
count += 1
}
for _, rule := range rules.Mysql {
err := db.Model(&mysql.Rule{}).Create(&rule).Error
if err != nil {
errs = append(errs, errors.Wrap(err, fmt.Sprintf("mysql rule[%s]", rule.Name)).Error())
continue
}
count += 1
}
for _, rule := range rules.Rmi {
err := db.Model(&rmi.Rule{}).Create(&rule).Error
if err != nil {
errs = append(errs, errors.Wrap(err, fmt.Sprintf("rmi rule[%s]", rule.Name)).Error())
continue
}
count += 1
}
for _, rule := range rules.Ftp {
err := db.Model(&ftp.Rule{}).Create(&rule).Error
if err != nil {
errs = append(errs, errors.Wrap(err, fmt.Sprintf("ftp rule[%s]", rule.Name)).Error())
continue
}
count += 1
}
c.JSON(200, gin.H{
"status": "succeed",
"error": errs,
"result": fmt.Sprintf("%d rules were imported successfully, %d failed.", count, len(errs)),
})
}
func (revsuit *Revsuit) getHttpConfig(c *gin.Context) {
var res = make(map[string]string)
res["Addr"] = revsuit.config.Addr
res["Token"] = revsuit.config.Token
res["Database"] = revsuit.config.Database
res["LogLevel"] = revsuit.config.LogLevel
res["IpHeader"] = revsuit.config.IpHeader
c.JSON(200, res)
}
func (revsuit *Revsuit) updateHttpConfig(c *gin.Context) {
var form = make(map[string]string)
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if form["LogLevel"] != revsuit.config.LogLevel {
revsuit.logLevel = initLog(form["LogLevel"])
revsuit.config.LogLevel = form["LogLevel"]
if revsuit.logLevel == log.LevelTrace {
revsuit.http.Router.Use(gin.Logger())
}
log.Info("Update platform config [log_level] to %s", form["LogLevel"])
}
if form["Token"] != revsuit.config.Token {
revsuit.config.Token = form["Token"]
revsuit.http.SetToken(form["Token"])
log.Info("Update platform config [token] to %s", form["Token"])
}
if form["Database"] != revsuit.config.Database {
revsuit.config.Database = form["Database"]
initDatabase(form["Database"])
log.Info("Update platform config [database] to %s", form["Database"])
}
if form["IpHeader"] != revsuit.config.IpHeader {
revsuit.config.IpHeader = form["IpHeader"]
revsuit.http.SetIpHeader(form["IpHeader"])
log.Info("Update http config [ip_header] to %s", form["IpHeader"])
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": "update succeed",
})
}
func (revsuit *Revsuit) getFtpConfig(c *gin.Context) {
c.JSON(200, revsuit.ftp.Config)
}
func (revsuit *Revsuit) updateFtpConfig(c *gin.Context) {
var form = ftp.Config{}
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if form.Addr != revsuit.ftp.Addr {
revsuit.ftp.Addr = form.Addr
log.Info("Update ftp config [addr] to %s", form.Addr)
}
if form.PasvIP != revsuit.ftp.PasvIP {
revsuit.ftp.PasvIP = form.PasvIP
log.Info("Update ftp config [pasv_ip] to %s", form.PasvIP)
}
if form.PasvPort != revsuit.ftp.PasvPort {
revsuit.ftp.PasvPort = form.PasvPort
log.Info("Update ftp config [pasv_port] to %d", form.PasvPort)
}
if form.Enable != revsuit.ftp.Enable {
log.Info("Update ftp config [enable] to %v", form.Enable)
if form.Enable {
go revsuit.ftp.Run()
} else {
revsuit.ftp.Stop()
}
return
}
if revsuit.ftp.Enable {
revsuit.ftp.Restart()
}
}
func (revsuit *Revsuit) getDnsConfig(c *gin.Context) {
c.JSON(200, revsuit.dns.Config)
}
func (revsuit *Revsuit) updateDnsConfig(c *gin.Context) {
var form = dns.Config{}
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if form.Enable != revsuit.dns.Enable {
log.Info("Update dns config [enable] to %v", form.Enable)
if form.Enable {
go revsuit.dns.Run()
} else {
revsuit.dns.Stop()
}
return
}
}
func (revsuit *Revsuit) getMySQLConfig(c *gin.Context) {
c.JSON(200, revsuit.mysql.Config)
}
func (revsuit *Revsuit) updateMySQLConfig(c *gin.Context) {
var form = mysql.Config{}
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if form.Addr != revsuit.mysql.Addr {
revsuit.mysql.Addr = form.Addr
log.Info("Update mysql config [addr] to %s", form.Addr)
}
if form.VersionString != revsuit.mysql.VersionString {
revsuit.mysql.VersionString = form.VersionString
log.Info("Update mysql config [version_string] to %s", form.VersionString)
}
if form.Enable != revsuit.mysql.Enable {
log.Info("Update mysql config [enable] to %v", form.Enable)
if form.Enable {
go revsuit.mysql.Run()
} else {
revsuit.mysql.Stop()
}
return
}
if revsuit.mysql.Enable {
revsuit.mysql.Restart()
}
}
func (revsuit *Revsuit) getRmiConfig(c *gin.Context) {
c.JSON(200, revsuit.rmi.Config)
}
func (revsuit *Revsuit) updateRmiConfig(c *gin.Context) {
var form = rmi.Config{}
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if form.Addr != revsuit.rmi.Addr {
revsuit.rmi.Addr = form.Addr
log.Info("Update rmi config [addr] to %s", form.Addr)
}
if form.Enable != revsuit.rmi.Enable {
log.Info("Update rmi config [enable] to %v", form.Enable)
if form.Enable {
go revsuit.rmi.Run()
} else {
revsuit.rmi.Stop()
}
return
}
if revsuit.rmi.Enable {
revsuit.rmi.Restart()
}
}
func (revsuit *Revsuit) getNoticeConfig(c *gin.Context) {
c.JSON(200, revsuit.config.Notice)
}
func (revsuit *Revsuit) updateNoticeConfig(c *gin.Context) {
var form = noticeConfig{}
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
log.Info("Update notice config")
revsuit.config.Notice = form
initNotice(form)
}