feat(ftp): support receive ftp connection (#6)

Co-authored-by: E99p1ant <[email protected]>
This commit is contained in:
Li4n0
2021-04-25 13:39:27 +08:00
committed by GitHub
co-authored by E99p1ant
parent 5e24f9530d
commit 26755351f7
36 changed files with 1446 additions and 141 deletions
+2
View File
@@ -3,6 +3,8 @@
# local files
qqwry.dat
revsuit.db
revsuit.db-shm
revsuit.db-wal
# local config
config.yaml
+7 -2
View File
@@ -1,8 +1,8 @@
version: 4.0
addr: :10000
token: token
token:
database: revsuit.db
log_level: debug
log_level: info
http:
ip_header:
@@ -15,6 +15,11 @@ mysql:
enable: true
addr: :3306
version_string: 10.4.13-MariaDB-log
ftp:
enable: true
addr: :21
pasv_ip: 127.0.0.1 # your public network ip
pasv_port: 2020
notice:
dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token}
+6
View File
@@ -23,6 +23,9 @@
<a-menu-item key="/logs/mysql">
<router-link to="/logs/mysql">MySQL Logs</router-link>
</a-menu-item>
<a-menu-item key="/logs/ftp">
<router-link to="/logs/ftp">FTP Logs</router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="rules">
<span slot="title"><a-icon type="radar-chart"/><span>Rules</span></span>
@@ -38,6 +41,9 @@
<a-menu-item key="/rules/mysql">
<router-link to="/rules/mysql">MySQL Rules</router-link>
</a-menu-item>
<a-menu-item key="/rules/ftp">
<router-link to="/rules/ftp">FTP Rules</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
+11
View File
@@ -43,3 +43,14 @@ export function getRmiRecord(params) {
}
})
}
export function getFtpRecord(params) {
return request({
url: '/record/ftp',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+33
View File
@@ -132,3 +132,36 @@ export function deleteRmiRule(data) {
}
})
}
export function getFtpRule(params) {
return request({
url: '/rule/ftp',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function upsertFtpRule(data) {
return request({
url: '/rule/ftp',
data: data,
method: 'post',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function deleteFtpRule(data) {
return request({
url: '/rule/ftp',
data: data,
method: 'delete',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+1 -1
View File
@@ -7,7 +7,7 @@
@cancel="cancel"
@ok="auth"
>
<a-input v-model.lazy="token" placeholder="Your token"/>
<a-input v-model.lazy="token" @pressEnter="auth" placeholder="Your token"/>
</a-modal>
</div>
</template>
+6 -3
View File
@@ -15,10 +15,13 @@
<a-form-model-item :rules="rules.flagFormat" prop="flag_format">
<span slot="label">
Flag Format&nbsp;
<a-tooltip title="1. Only when the request contains content that satisfies the flag format, the request will be captured.
2. Please use regular expression syntax.
<a-tooltip title="Basic usage:
1. Only when the request contains content that satisfies the flag format, the request will be captured.
2. Use regular expression syntax.
3. The character '*' means to capture all requests.
4. Advanced usage: When the format uses grouping, the platform will only notify the user or push to the client when the first group appears for the first time.">
Advanced usage:
1. When the regex uses grouping without group name, the platform will only notify the user or push to the client when the first group appears for the first time.
2. When the regex uses grouping with group name, you can get these submatches through template variables and use them in other fields of the rule.">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
+18 -8
View File
@@ -13,42 +13,52 @@ const routes = [
{
path: '/logs/http',
name: 'HttpLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Http')
component: () => import( '../views/logs/Http')
},
{
path: '/logs/dns',
name: 'DnsLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Dns')
component: () => import( '../views/logs/Dns')
},
{
path: '/logs/mysql',
name: 'MysqlLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql')
component: () => import( '../views/logs/Mysql')
},
{
path: '/logs/rmi',
name: 'RmiLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Rmi')
component: () => import( '../views/logs/Rmi')
},
{
path: '/logs/ftp',
name: 'FtpLogs',
component: () => import( '../views/logs/Ftp')
},
{
path: '/rules/http',
name: 'HttpRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Http')
component: () => import( '../views/rules/Http')
},
{
path: '/rules/dns',
name: 'DnsRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Dns')
component: () => import( '../views/rules/Dns')
},
{
path: '/rules/mysql',
name: 'MysqlRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql')
component: () => import( '../views/rules/Mysql')
},
{
path: '/rules/rmi',
name: 'RmiRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Rmi')
component: () => import( '../views/rules/Rmi')
},
{
path: '/rules/ftp',
name: 'FtpRules',
component: () => import( '../views/rules/Ftp')
}
]
+220
View File
@@ -0,0 +1,220 @@
<template>
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
>
<div slot="selectDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px">
<a-checkbox
:checked="filters[column.dataIndex] === 'CRASHED'"
@change="(e)=>{e.target.checked?filters[column.dataIndex] = 'CRASHED':filters[column.dataIndex] = '';fetch()}">
CRASHED
</a-checkbox>
<br/>
<a-checkbox
:checked="filters[column.dataIndex] === 'FINISHED'"
@change="(e)=>{e.target.checked?filters[column.dataIndex] = 'FINISHED':filters[column.dataIndex] = '';fetch()}">
FINISHED
</a-checkbox>
</div>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
>
Search
</a-button>
<a-button size="small" style="width: 90px" @click="() =>{
clearFilters();
delete filters[column.dataIndex];
fetch()
}">
Reset
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="time" slot-scope="time">
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
</span>
<span slot="status" slot-scope="status">
<a-tag
:color="colors[status]"
>
{{ status }}
</a-tag>
</span>
</a-table>
</template>
<style>
.gray-table-row {
background-color: #f5f5f5;
}
</style>
<script>
import {getFtpRecord} from '@/api/record'
import {store} from '@/main'
const colors = {
"CRASHED": "#f50",
"FINISHED": "#87d068"
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'REQUEST TIME',
dataIndex: 'request_time',
key: 'request_time',
scopedSlots: {customRender: 'time'},
},
{
title: 'RULE',
dataIndex: 'rule_name',
key: 'rule_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG',
dataIndex: 'flag',
key: 'flag',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'USER',
dataIndex: 'user',
key: 'user',
},
{
title: 'PASSWORD',
dataIndex: 'password',
key: 'password',
},
{
title: 'PATH',
dataIndex: 'path',
key: 'path',
ellipsis: true,
},
{
title: 'STATUS',
dataIndex: 'status',
key: 'status',
scopedSlots: {
customRender: 'status',
filterDropdown: 'selectDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'REMOTE IP',
key: 'remote_ip',
dataIndex: 'remote_ip',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'IP AREA',
key: 'ip_area',
dataIndex: 'ip_area'
}
];
export default {
name: 'DnsLogs',
data() {
return {
data: [],
pagination: {current: 1},
filters: {},
order: "desc",
loading: false,
columns,
colors: colors
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getFtpRecord(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
console.error(e)
}
})
}
},
mounted() {
this.fetch({page: "1"});
},
}
</script>
+6 -6
View File
@@ -77,12 +77,12 @@ import {getHttpRecord} from '@/api/record'
import {store} from '@/main'
const colors = {
"GET": "green",
"POST": "red",
"HEAD": "pink",
"PUT": "geekblue",
"OPTIONS": "cyan",
"DELETE": "purple",
"GET": "#52c41a",
"POST": "#f5222d",
"PUT": "#eb2f96",
"HEAD": "#02a7ff",
"OPTIONS": "#13c2c2",
"DELETE": "#722ed1",
}
const columns = [
+25 -7
View File
@@ -9,7 +9,23 @@
>
<div v-if="record.files.length" slot="expandedRowRender" slot-scope="record" style="margin: 0">
<b v-if="record.files.length" style="color: gray">FILES:</b><br>
<a v-for="file in record.files" :key="file.name+record.id" :href="'/revsuit/api/file/mysql/'+file.id" target="_blank">{{ file.name }} </a>
<a v-for="file in record.files" :key="file.name+record.id" :href="'/revsuit/api/file/mysql/'+file.id"
target="_blank">{{ file.name }} </a>
</div>
<div slot="selectDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px">
<a-checkbox
:checked="filters[column.dataIndex] === 'true'"
@change="(e)=>{e.target.checked?filters[column.dataIndex] = 'true':filters[column.dataIndex] = '';fetch()}">
True
</a-checkbox>
<br/>
<a-checkbox
:checked="filters[column.dataIndex] === 'false'"
@change="(e)=>{e.target.checked?filters[column.dataIndex] = 'false':filters[column.dataIndex] = '';fetch()}">
False
</a-checkbox>
</div>
<div
slot="filterDropdown"
@@ -58,12 +74,12 @@
</span>
<span slot="loadData" slot-scope="loadData">
<a-tag v-if="loadData"
color="green"
>True</a-tag><a-tag v-else color="red">False</a-tag>
color="#eb2f96"
>True</a-tag><a-tag v-else color="#f5222d">False</a-tag>
</span>
<span slot="fileNum" slot-scope="files">
<a-tag v-if="files.length>=3"
color="purple"
color="#722ed1"
>{{ files.length }}</a-tag>
<a-tag v-else :color="colors[files.length]">
{{ files.length }}
@@ -82,9 +98,9 @@ import {getMysqlRecord} from '@/api/record'
import {store} from '@/main'
const colors = [
"geekblue",
"blue",
"pink",
"#13c2c2",
"#52c41a",
"#02a7ff",
]
const columns = [
@@ -133,6 +149,8 @@ const columns = [
dataIndex: 'load_local_data',
key: 'load_local_data',
scopedSlots: {
filterDropdown: 'selectDropdown',
filterIcon: 'filterIcon',
customRender: "loadData",
}
},
+18 -10
View File
@@ -6,7 +6,7 @@
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' Dns rule'"
:title="formAction+ ' DNS rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@@ -47,7 +47,14 @@
</a-row>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item label="Value" :rules="rules.value">
<a-form-model-item :rules="rules.value">
<span slot="label">
Value
<a-tooltip
title="Support template such as ${varname}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input
v-model="form.value"
style="width: 100%"
@@ -139,19 +146,19 @@
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span>
<span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="-->
<!-- color: #67C23A;-->
<!-- background-color: transparent;-->
<!-- border-color: #67C23A;-->
<!-- text-shadow: none;-->
<!-- margin-right: 10px;-->
<!--" size="small" ghost>View</a-button>-->
<a-button @click="viewRule(record)" style="
color: #67C23A;
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin:0 10px 3px 0;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
@@ -219,6 +226,7 @@ const columns = [
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
ellipsis: true,
},
{
title: 'RANK',
+356
View File
@@ -0,0 +1,356 @@
<template xmlns:a-col="http://www.w3.org/1999/html">
<div>
<a-button id="add-rule" type="primary" @click="addRule">
<a-icon type="plus"/>
New Rule
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' FTP rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@close="closeDrawer"
>
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
<BasicRule :form="form" :readOnly="formReadOnly"/>
<a-row :gutter="24">
<a-col :span="24">
<a-form-model-item>
<span slot="label">
Pasv Address
<a-tooltip
title="Support template such as ${user}/${password}/${varname}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-input
v-model="form.pasv_address"
style="width: 100%"
placeholder="use default value in the config"
:readOnly="formReadOnly"
/>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
Cancel
</a-button>
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
Submit
</a-button>
</div>
</a-drawer>
<!-- rule table -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
>
Search
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="rank" slot-scope="rank">
<a-tag
:color="'#'+(0x2db7f5+rank*80).toString(16)"
>
{{ rank }}
</a-tag>
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
<span slot="valueRender" slot-scope="values">
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span>
<span slot="action" slot-scope="text,record,index">
<a-button @click="viewRule(record)" style="
color: #67C23A;
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin:0 10px 3px 0;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteRule(record,index)"
>
<a-button type="danger" size="small" ghost>Delete</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</template>
<style scoped>
#add-rule {
margin-bottom: 10px;
}
</style>
<script>
import {getFtpRule, upsertFtpRule, deleteFtpRule} from '@/api/rule'
import {store} from '@/main'
import BasicRule from "@/components/BasicRule";
const VIEW = "View"
const EDIT = "Edit"
const CREATE = "Create"
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
ellipsis: true,
},
{
title: 'RANK',
dataIndex: 'rank',
key: 'rank',
scopedSlots: {
customRender: 'rank',
},
},
{
title: 'PASV ADDRESS',
dataIndex: 'pasv_address',
key: 'pasv_address',
ellipsis: true,
},
{
title: 'PUSH TO CLIENT',
dataIndex: 'push_to_client',
key: 'push_to_client',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'NOTICE',
dataIndex: 'notice',
key: 'notice',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'Action',
key: 'action',
scopedSlots: {customRender: 'action'},
},
];
export default {
name: 'FtpRules',
data() {
return {
data: [],
formVisible: false,
pagination: {current: 1},
filters: {},
loading: false,
columns,
form: {},
formReadOnly: false,
formAction: "", // View ,Create or Edit
}
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getFtpRule(params).then(res => {
let result = res.data.result
this.data = result.data
const pagination = {...this.pagination};
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = result.count;
this.pagination = pagination;
this.loading = false
}).catch(e => {
if (e.response.status === 403) {
store.authed = false
return []
} else {
this.$notification.error({
message: 'Unknown error: ' + e.response.status,
style: {
width: '100px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
}
})
},
clickSwitch(record, prop) {
record[prop] = !record[prop]
upsertFtpRule(record).then().catch(e => {
this.$notification.error({
message: 'Edit failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
addRule() {
this.form = {}
this.showForm(CREATE)
},
viewRule(record) {
this.form = record
this.showForm(VIEW)
},
editRule(record) {
this.form = JSON.parse(JSON.stringify(record))
this.showForm(EDIT)
},
deleteRule(record, index) {
deleteFtpRule(record).then(() => {
this.data.splice(index, 1)
}).catch(e => {
this.$notification.error({
message: 'Error',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
},
showForm(action) {
this.formAction = action
this.formReadOnly = action === VIEW;
this.formVisible = true;
},
closeDrawer() {
this.formVisible = false;
},
handleSubmit() {
this.$refs.form.validate(valid => {
if (valid) {
upsertFtpRule(this.form).then(() => {
this.closeDrawer()
this.fetch({page: this.pagination.current});
this.$notification.info({
message: 'Success',
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 2.5
});
}).catch(e => {
this.$notification.error({
message: this.formAction + ' failed',
description:
e.response.data.error,
style: {
width: '600px',
marginLeft: `${335 - 600}px`,
},
duration: 4
});
})
}
})
},
handleCancel() {
this.form = {}
this.closeDrawer()
}
},
mounted() {
this.fetch({page: "1"});
},
components: {
BasicRule,
}
}
</script>
+7 -5
View File
@@ -21,7 +21,7 @@
<span slot="label">
Response Status Code
<a-tooltip
title="Number between 100-600, or template such as ${query.var_name}/${body.var_name}/${header.var_name}">
title="Number between 100-600, or template such as ${query.varname}/${body.varname}/${header.varname}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
@@ -39,7 +39,7 @@
<span slot="label">
Response Headers
<a-tooltip
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
title="Support template such as ${query.varname}/${body.varname}/${header.varname}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
@@ -77,13 +77,14 @@
<span slot="label">
Response Body
<a-tooltip
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
title="Support template such as ${query.varname}/${body.varname}/${header.varname}">
<a-icon type="question-circle-o"/>
</a-tooltip>
</span>
<a-textarea v-model="form.response_body"
placeholder="Hello RevSuit!"
:readOnly="formReadOnly"
rows="10"
/>
</a-form-model-item>
</a-col>
@@ -164,14 +165,14 @@
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
@@ -221,6 +222,7 @@ const columns = [
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
ellipsis: true,
},
{
title: 'RANK',
+3 -2
View File
@@ -146,14 +146,14 @@
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
@@ -203,6 +203,7 @@ const columns = [
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
ellipsis: true,
},
{
title: 'RANK',
+10 -16
View File
@@ -6,7 +6,7 @@
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' Dns rule'"
:title="formAction+ ' RMI rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@@ -79,13 +79,6 @@
{{ rank }}
</a-tag>
</span>
<span slot="type" slot-scope="type">
<a-tag
:color="colors[type]"
>
{{ resolveTypes[type] }}
</a-tag>
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
@@ -94,19 +87,19 @@
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span>
<span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="-->
<!-- color: #67C23A;-->
<!-- background-color: transparent;-->
<!-- border-color: #67C23A;-->
<!-- text-shadow: none;-->
<!-- margin-right: 10px;-->
<!--" size="small" ghost>View</a-button>-->
<a-button @click="viewRule(record)" style="
color: #67C23A;
background-color: transparent;
border-color: #67C23A;
text-shadow: none;
margin:0 10px 3px 0;
" size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
@@ -156,6 +149,7 @@ const columns = [
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
ellipsis: true,
},
{
title: 'RANK',
+8
View File
@@ -1,11 +1,19 @@
package database
import (
"strings"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func NewSqlite3(dsn string) (*gorm.DB, error) {
if strings.Contains(dsn, "?") {
dsn += "&_synchronous=1&_journal_mode=WAL"
} else {
dsn += "?_synchronous=1&_journal_mode=WAL"
}
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
+4 -1
View File
@@ -1,7 +1,10 @@
package qqwry
// Area return IpArea according to ip
// Area returns IpArea according to ip
func Area(ip string) string {
defer func() {
_ = recover()
}()
if GetQQWry() == nil {
return ""
}
+12
View File
@@ -0,0 +1,12 @@
package rule
import "strings"
func CompileTpl(tpl string, vars map[string]string) (compiled string) {
compiled = tpl
for n, v := range vars {
compiled = strings.ReplaceAll(compiled, "${"+n+"}", v)
}
return compiled
}
+15 -3
View File
@@ -2,6 +2,7 @@ package rule
import (
"regexp"
"strings"
"time"
log "unknwon.dev/clog/v2"
@@ -25,7 +26,9 @@ type BaseRule struct {
Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"`
}
func (br BaseRule) Match(s string) (flag, flagGroup string) {
func (br BaseRule) Match(s string) (flag, flagGroup string, vars map[string]string) {
vars = make(map[string]string)
if br.flagCatcher == nil {
if br.FlagFormat == "*" {
flag = "*"
@@ -41,13 +44,22 @@ func (br BaseRule) Match(s string) (flag, flagGroup string) {
}
matched := br.flagCatcher.FindStringSubmatch(s)
groupNames := br.flagCatcher.SubexpNames()
if len(matched) == 0 {
return
}
flag = matched[0]
if len(matched) > 1 {
if len(matched) > 1 && len(groupNames) == 0 {
flagGroup = matched[1]
}
return flag, flagGroup
for j, name := range groupNames {
if j != 0 && name != "" {
vars[name] = strings.TrimSpace(matched[j])
}
}
return flag, flagGroup, vars
}
+12 -11
View File
@@ -8,6 +8,7 @@ import (
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/newdns"
"github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/rule"
"github.com/patrickmn/go-cache"
log "unknwon.dev/clog/v2"
)
@@ -72,7 +73,7 @@ func (s *Server) Run() {
ip := strings.Split(remoteAddr, ":")[0]
for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(domain)
flag, flagGroup, vars := _rule.Match(domain)
if flag == "" {
continue
}
@@ -82,7 +83,7 @@ func (s *Server) Run() {
log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return nil, nil
}
log.Info("DNS record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
log.Info("DNS 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 {
@@ -91,11 +92,11 @@ func (s *Server) Run() {
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
log.Trace("DNS record[id%d] has been put to client message queue", r.ID)
}
} else {
r.PushToClient()
log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
log.Trace("DNS record[id%d] has been put to client message queue", r.ID)
}
}
@@ -103,12 +104,12 @@ func (s *Server) Run() {
if _rule.Notice {
go func() {
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)
}()
}
if _rule.Value != "" {
value := rule.CompileTpl(_rule.Value, vars)
_type := _rule.Type
if _rule.Type == newdns.REBINDING {
_type = newdns.A
@@ -121,16 +122,16 @@ func (s *Server) Run() {
Records: func() []newdns.Record {
switch _rule.Type {
case newdns.TXT:
return []newdns.Record{{Data: []string{_rule.Value}}}
return []newdns.Record{{Data: []string{value}}}
case newdns.CNAME, newdns.NS:
return []newdns.Record{{Address: _rule.Value + "."}}
return []newdns.Record{{Address: value + "."}}
case newdns.REBINDING:
// Get rebinding ip list
values, ok := rebindingCache.Get(ip)
if !ok {
rebindingCache.Set(ip, strings.Split(_rule.Value, ","), cache.DefaultExpiration)
values = strings.Split(_rule.Value, ",")
rebindingCache.Set(ip, strings.Split(value, ","), cache.DefaultExpiration)
values = strings.Split(value, ",")
}
//Choose and delete first ip
@@ -144,7 +145,7 @@ func (s *Server) Run() {
log.Trace("DNS rebinding client(ip:%v) to %v", ip, value)
return []newdns.Record{{Address: value}}
default:
return []newdns.Record{{Address: _rule.Value}}
return []newdns.Record{{Address: value}}
}
}(),
TTL: _rule.TTL * time.Second,
+3 -3
View File
@@ -157,9 +157,9 @@ func UpsertRules(c *gin.Context) {
}
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 {
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{
@@ -191,7 +191,7 @@ func DeleteRules(c *gin.Context) {
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{
"status": "succeed",
+8
View File
@@ -0,0 +1,8 @@
package ftp
type Config struct {
Enable bool
Addr string
PasvIP string `yaml:"pasv_ip"`
PasvPort int `yaml:"pasv_port"`
}
+230
View File
@@ -0,0 +1,230 @@
package ftp
import (
"bytes"
"fmt"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/rule"
log "unknwon.dev/clog/v2"
)
type Server struct {
Config
rules []*Rule
rulesLock sync.RWMutex
}
type Status string
const (
CRASHED Status = "CRASHED"
FINISHED Status = "FINISHED"
)
var (
server *Server
once sync.Once
)
func GetServer() *Server {
once.Do(func() {
server = &Server{rulesLock: sync.RWMutex{}}
})
return server
}
func (s *Server) getRules() []*Rule {
defer s.rulesLock.RUnlock()
s.rulesLock.RLock()
return s.rules
}
func (s *Server) updateRules() error {
db := database.DB.Model(new(Rule))
s.rulesLock.Lock()
db.Order("rank desc").Find(&s.rules)
s.rulesLock.Unlock()
return nil
}
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(time.Second * 30)); err != nil {
log.Error("FTP set connection deadline error:%v", err.Error())
}
if _, err := conn.Write([]byte("220 (vsFTPd 3.0.2)\r\n")); err != nil {
log.Error("FTP write connection error:%v", err.Error())
}
ip := strings.Split(conn.RemoteAddr().String(), ":")[0]
buf := &bytes.Buffer{}
var user, password, path, flag, flagGroup string
status := CRASHED
var matchedRule *Rule
var vars map[string]string
loop:
for {
data := make([]byte, 2048)
n, err := conn.Read(data)
if err != nil {
break
}
buf.Write(data[:n])
if buf.Len() > 4 {
cmd := string(buf.Bytes()[:4])
switch cmd {
case "USER":
user = strings.TrimRight(string(buf.Bytes()[5:]), "\r\n")
_, _ = conn.Write([]byte("331 password please - version check\r\n"))
case "PASS":
password = strings.TrimRight(string(buf.Bytes()[5:]), "\r\n")
_, _ = conn.Write([]byte("230 User logged in\r\n"))
for _, _rule := range s.getRules() {
for _, s := range []string{user, password} {
flag, flagGroup, vars = _rule.Match(s)
if flag != "" {
vars["user"] = user
vars["password"] = password
break
}
}
if flag == "" {
continue
}
matchedRule = _rule
}
case "QUIT":
_, _ = conn.Write([]byte("221 Goodbye.\r\n"))
case "RETR":
path += "/" + strings.TrimRight(string(buf.Bytes()[5:]), "\r\n")
_, _ = conn.Write([]byte("451 Nope\r\n"))
_, _ = conn.Write([]byte("221 Goodbye.\r\n"))
status = FINISHED
break loop
case "EPSV", "EPRT", "PORT":
// 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)))
case "PASV":
// return rule's pasv_address or default pasv address
ret := fmt.Sprintf("227 Entering Passive Mode (%s,%v,%d)\r\n", strings.ReplaceAll(s.PasvIP, ".", ","), float64(s.PasvPort/256), s.PasvPort%256)
if matchedRule != nil && matchedRule.PasvAddress != "" {
pasvAddress := rule.CompileTpl(matchedRule.PasvAddress, vars)
pasvIP, pasvPort, err := net.SplitHostPort(pasvAddress)
if err != nil {
log.Warn("FTP failed to split rule[id%d] pasv_address(%s) :%s", matchedRule.ID, pasvAddress, err.Error())
break
}
port, err := strconv.Atoi(pasvPort)
if err != nil {
log.Warn("FTP failed to convert rule[id%d] pasv_port(%s) :%s", matchedRule.ID, pasvPort, err.Error())
break
}
ret = fmt.Sprintf("227 Entering Passive Mode (%s,%v,%d)\r\n", strings.ReplaceAll(pasvIP, ".", ","), float64(port/256), port%256)
}
_, _ = conn.Write([]byte(ret))
default:
cmd = string(buf.Bytes()[:3])
if cmd == "CWD" {
_, _ = conn.Write([]byte("250 Directory successfully changed.\r\n"))
path += "/" + strings.TrimRight(string(buf.Bytes()[4:]), "\r\n")
} else if cmd == "PWD" {
_, _ = conn.Write([]byte("257 \"/\" is the current directory\r\n"))
} else {
_, _ = conn.Write([]byte("230 more data please!\r\n"))
}
}
}
buf = &bytes.Buffer{}
}
if matchedRule != nil {
_rule := matchedRule
area := qqwry.Area(ip)
// create new record
r, err := NewRecord(_rule, flag, user, password, path, ip, area, status)
if err != nil {
log.Error("FTP record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
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) Run() {
if err := s.updateRules(); err != nil {
log.Fatal(err.Error())
}
// run server
log.Info("Starting FTP Server at %v", s.Addr)
listener, err := net.Listen("tcp", s.Addr)
if err != nil {
log.Fatal(err.Error())
}
go func() {
pasvAddress := fmt.Sprintf("%s:%d", strings.Split(s.Addr, ":")[0], s.PasvPort)
log.Info("Start to listen FTP PASV port at %v", pasvAddress)
listener, err := net.Listen("tcp", pasvAddress)
if err != nil {
log.Fatal("FTP failed to listen on pasv port : %v", err)
}
for {
tcpConn, err := listener.Accept()
if err != nil {
log.Error("FTP accept connection error: %v", err)
continue
}
_ = tcpConn.Close()
}
}()
for {
tcpConn, err := listener.Accept()
if err != nil {
log.Error("FTP accept connection error: %v", err)
continue
}
go s.handleConnection(tcpConn)
}
}
+112
View File
@@ -0,0 +1,112 @@
package ftp
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/notice"
"github.com/li4n0/revsuit/internal/record"
)
var _ record.Record = (*Record)(nil)
type Record struct {
User string `form:"user" json:"user"`
Password string `form:"password" json:"password"`
Path string `form:"path" json:"path"`
Status Status `form:"status" json:"status"`
record.BaseRecord
Rule Rule `gorm:"foreignKey:RuleName;references:Name;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" form:"-" json:"-" notice:"-"`
}
func (Record) TableName() string {
return "ftp_records"
}
func (r Record) Notice() {
notice.Notice(r)
}
func NewRecord(rule *Rule, flag, user, password, path, ip, area string, status Status) (r *Record, err error) {
r = &Record{
BaseRecord: record.BaseRecord{
Flag: flag,
RemoteIP: ip,
IpArea: area,
RequestTime: time.Now(),
},
Path: path,
User: user,
Password: password,
Status: status,
Rule: *rule,
}
err = database.DB.Create(r).Error
return r, err
}
func ListRecords(c *gin.Context) {
var (
ftpRecord Record
res []Record
count int64
order = c.Query("order")
)
if err := c.ShouldBind(&ftpRecord); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err,
"result": nil,
})
return
}
db := database.DB.Model(&ftpRecord)
if ftpRecord.Flag != "" {
db.Where("flag = ?", ftpRecord.Flag)
}
if ftpRecord.Path != "" {
db.Where("path like ?", "%"+ftpRecord.Path+"%")
}
if ftpRecord.Status != "" {
db.Where("status = ?", ftpRecord.Status)
}
if ftpRecord.RemoteIP != "" {
db.Where("remote_ip = ?", ftpRecord.RemoteIP)
}
if ftpRecord.RuleName != "" {
db.Where("rule_name = ?", ftpRecord.RuleName)
}
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
return
}
if order != "asc" {
order = "desc"
}
if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": gin.H{"count": count, "data": res},
})
}
+194
View File
@@ -0,0 +1,194 @@
package ftp
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/rule"
"gorm.io/gorm/clause"
log "unknwon.dev/clog/v2"
)
// FTP rule struct
type Rule struct {
rule.BaseRule
PasvAddress string `gorm:"pasv_address" json:"pasv_address" form:"pasv_address"`
}
func (Rule) TableName() string {
return "ftp_rules"
}
// NewRule creates a new ftp rule struct
func NewRule(name, flagFormat, pasvAddress string, pushToClient, notice bool) *Rule {
return &Rule{
BaseRule: rule.BaseRule{
Name: name,
FlagFormat: flagFormat,
PushToClient: pushToClient,
Notice: notice,
},
PasvAddress: pasvAddress,
}
}
// CreateOrUpdate creates or updates the ftp rule in database and ruleSet
func (r *Rule) CreateOrUpdate() (err error) {
db := database.DB.Model(r)
err = db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns(
[]string{
"name",
"flag_format",
"rank",
"pasv_address",
"push_to_client",
"notice",
}),
}).Create(r).Error
if err != nil {
return
}
return GetServer().updateRules()
}
// Delete deletes the ftp rule in database and ruleSet
func (r *Rule) Delete() (err error) {
db := database.DB.Model(r)
err = db.Delete(r).Error
if err != nil {
return
}
return GetServer().updateRules()
}
// ListRules lists all ftp rules those satisfy the filter
func ListRules(c *gin.Context) {
var (
ftpRule Rule
res []Rule
count int64
order = c.Query("order")
)
if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err,
"result": nil,
})
return
}
db := database.DB.Model(&ftpRule)
if ftpRule.Name != "" {
db.Where("name = ?", ftpRule.Name)
}
db.Count(&count)
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
return
}
if order != "asc" {
order = "desc"
}
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err,
"data": nil,
})
return
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": gin.H{"count": count, "data": res},
})
}
// Create or update ftp rule from user submit
func UpsertRules(c *gin.Context) {
var (
ftpRule Rule
update bool
)
if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if ftpRule.ID != 0 {
update = true
}
if err := ftpRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
return
}
if update {
log.Trace("FTP rule[id%d] has been updated", ftpRule.ID)
} else {
log.Trace("FTP rule[id%d] has been created", ftpRule.ID)
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": nil,
})
}
// Delete ftp rule from user submit
func DeleteRules(c *gin.Context) {
var ftpRule Rule
if err := c.ShouldBind(&ftpRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if err := ftpRule.Delete(); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
log.Trace("FTP rule[id%d] has been deleted", ftpRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"data": nil,
})
}
+19 -11
View File
@@ -65,8 +65,9 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
)
for _, _rule := range s.getRules() {
flag, _ := _rule.Match(user + schema)
if flag == "" {
userFlag, _, _ := _rule.Match(user)
schemaFlag, _, _ := _rule.Match(schema)
if userFlag == "" && schemaFlag == "" {
continue
}
s.connRulePool.Store(c.ConnectionID, _rule)
@@ -81,8 +82,6 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
if strings.Contains(c.ConnAttrs["_client_name"], "MySQL Connector") {
c.IsJdbcClient = true
c.SupportLoadDataLocal = true
// 测试发现只有 pymysql 和原生命令行会对这个 flag 真正进行修改
// 而且 Connector/J 默认值为 False, 所以这里做特殊兼容
}
}
}
@@ -90,21 +89,30 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
// ConnectionClosed is part of the mysql.Handler interface.
func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Trace("MySQL Client leaved, ID [%d]", c.ConnectionID)
var (
user = c.User
clientName string
clientOS string
schema = c.SchemaName
supportLoadLocalData = c.SupportLoadDataLocal
cr, ok = s.connRulePool.Load(c.ConnectionID)
clientName, clientOS, flag, flagGroup string
)
if !ok {
return
}
_rule := cr.(*Rule)
flag, flagGroup := _rule.Match(user)
for _, s := range []string{user, schema} {
flag, flagGroup, _ = _rule.Match(s)
if flag != "" {
break
}
}
if flag == "" {
log.Error("MySQL Connection rule(%d) not match flag", c.ConnectionID)
}
if c.ConnAttrs != nil {
clientName = c.ConnAttrs["_client_name"] + " " + c.ConnAttrs["_client_version"]
clientOS = c.ConnAttrs["_os"] + " " + c.ConnAttrs["_platform"]
@@ -125,7 +133,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Error("MySQL record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return
}
log.Info("MySQL record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
log.Info("MySQL 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 {
@@ -134,11 +142,11 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
log.Trace("MySQL record[id%d] has been put to client message queue", r.ID)
}
} else {
r.PushToClient()
log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
log.Trace("MySQL record[id%d] has been put to client message queue", r.ID)
}
}
@@ -146,7 +154,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
if _rule.Notice {
go func() {
r.Notice()
log.Trace("MySQL record(id:%d) notice has been sent", r.ID)
log.Trace("MySQL record[id%d] notice has been sent", r.ID)
}()
}
+7
View File
@@ -81,6 +81,13 @@ func ListRecords(c *gin.Context) {
if mysqlRecord.ClientName != "" {
db.Where("client_name like ?", "%"+mysqlRecord.ClientName)
}
if c.Query("load_local_data") != "" {
if c.Query("load_local_data") == "true" {
db.Where("load_local_data = ?", true)
} else {
db.Where("load_local_data = ?", false)
}
}
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
+3 -3
View File
@@ -140,9 +140,9 @@ func UpsertRules(c *gin.Context) {
}
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 {
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{
@@ -174,7 +174,7 @@ func DeleteRules(c *gin.Context) {
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{
"status": "succeed",
+23 -18
View File
@@ -106,26 +106,31 @@ func getRawRequest(r *http.Request) ([]byte, error) {
return httputil.DumpRequestOut(r, true)
}
func compileTpl(c *gin.Context, tpl string) (compiled string) {
func compileTpl(c *gin.Context, tpl string, vars map[string]string) (compiled string) {
compiled = tpl
if queryVarMatcher.FindString(tpl) != "" {
compiled = queryVarMatcher.ReplaceAllString(compiled, c.Query(queryVarMatcher.FindStringSubmatch(tpl)[1]))
for _, submatch := range queryVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = strings.ReplaceAll(compiled, submatch[0], c.Query(submatch[1]))
}
if bodyVarMatcher.FindString(tpl) != "" {
compiled = bodyVarMatcher.ReplaceAllString(compiled, c.PostForm(bodyVarMatcher.FindStringSubmatch(tpl)[1]))
for _, submatch := range bodyVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = strings.ReplaceAll(compiled, submatch[0], c.PostForm(submatch[1]))
}
if headerVarMatcher.FindString(tpl) != "" {
compiled = headerVarMatcher.ReplaceAllString(compiled, c.GetHeader(headerVarMatcher.FindStringSubmatch(tpl)[1]))
for _, submatch := range headerVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = strings.ReplaceAll(compiled, submatch[0], c.GetHeader(submatch[1]))
}
for n, v := range vars {
compiled = strings.ReplaceAll(compiled, "${"+n+"}", v)
}
return compiled
}
func (s *Server) Receive(c *gin.Context) {
u := c.Request.URL.String()
for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(u)
flag, flagGroup, vars := _rule.Match(u)
if flag == "" {
continue
}
@@ -148,16 +153,16 @@ func (s *Server) Receive(c *gin.Context) {
// create new record
r, err := NewRecord(_rule, flag, c.Request.Method, u, ip, area, string(raw))
if err != nil {
log.Error("HTTP record(rule_id:%d) created failed :%s", _rule.ID, err.Error())
code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode))
log.Error("HTTP record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode, vars))
if err != nil || code < 100 || code > 600 {
code = 400
}
c.String(code, compileTpl(c, _rule.ResponseBody))
c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return
}
log.Info("HTTP record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
log.Info("HTTP 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 {
@@ -166,31 +171,31 @@ func (s *Server) Receive(c *gin.Context) {
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("HTTP record(id:%d) has been put to client message queue", r.ID)
log.Trace("HTTP record[id%d] has been put to client message queue", r.ID)
}
}
r.PushToClient()
log.Trace("HTTP record(id:%d) has been put to client message queue", r.ID)
log.Trace("HTTP record[id%d] has been put to client message queue", r.ID)
}
//send notice
if _rule.Notice {
go func() {
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)
}()
}
for header, value := range _rule.ResponseHeaders {
c.Header(compileTpl(c, header), compileTpl(c, value))
c.Header(compileTpl(c, header, vars), compileTpl(c, value, vars))
}
code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode))
code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode, vars))
if err != nil || code < 100 || code > 600 {
code = 400
}
c.String(code, compileTpl(c, _rule.ResponseBody))
c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return
}
+3 -3
View File
@@ -158,9 +158,9 @@ func UpsertRules(c *gin.Context) {
}
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 {
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{
@@ -192,7 +192,7 @@ func DeleteRules(c *gin.Context) {
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{
"status": "succeed",
+8 -7
View File
@@ -49,11 +49,12 @@ func (s *Server) updateRules() error {
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
if err := conn.SetDeadline(time.Now().Add(time.Second * 30)); err != nil {
log.Error("RMI set connection deadline error:%v", err.Error())
}
ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
@@ -94,7 +95,7 @@ func (s *Server) handleConnection(conn net.Conn) {
path := strings.TrimRight(string(frags[len(frags)-1][2:]), "\x00")
for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(path)
flag, flagGroup, _ := _rule.Match(path)
if flag == "" {
continue
}
@@ -104,10 +105,10 @@ func (s *Server) handleConnection(conn net.Conn) {
// create new record
r, err := NewRecord(_rule, flag, path, ip, area)
if err != nil {
log.Error("RMI record(rule_id:%d) created failed :%s", _rule.ID, err.Error())
log.Error("RMI record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
return
}
log.Info("RMI record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
log.Info("RMI 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 {
@@ -116,18 +117,18 @@ func (s *Server) handleConnection(conn net.Conn) {
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
log.Trace("RMI record(id:%d) has been put to client message queue", r.ID)
log.Trace("RMI record[id%d] has been put to client message queue", r.ID)
}
}
r.PushToClient()
log.Trace("RMI record(id:%d) has been put to client message queue", r.ID)
log.Trace("RMI record[id%d] has been put to client message queue", r.ID)
}
//send notice
if _rule.Notice {
go func() {
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)
}()
}
}
+4 -4
View File
@@ -10,7 +10,7 @@ import (
log "unknwon.dev/clog/v2"
)
// Http rule struct
// RMI rule struct
type Rule struct {
rule.BaseRule
}
@@ -149,9 +149,9 @@ func UpsertRules(c *gin.Context) {
}
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 {
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{
@@ -183,7 +183,7 @@ func DeleteRules(c *gin.Context) {
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{
"status": "succeed",
+2
View File
@@ -2,6 +2,7 @@ package server
import (
"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"
@@ -24,4 +25,5 @@ type Config struct {
DNS dns.Config
MySQL mysql.Config
RMI rmi.Config
FTP ftp.Config
}
+14
View File
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/frontend"
"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"
@@ -15,6 +16,11 @@ import (
func (revsuit *Revsuit) registerRouter() {
revsuit.http.Router = gin.Default()
if revsuit.logLevel != log.LevelTrace {
revsuit.http.Router = gin.New()
revsuit.http.Router.Use(gin.Recovery())
}
revsuit.registerPlatformRouter()
revsuit.registerHttpRouter()
}
@@ -61,6 +67,9 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup := recordGroup.Group("/rmi")
rmiGroup.GET("", rmi.ListRecords)
ftpGroup := recordGroup.Group("/ftp")
ftpGroup.GET("", ftp.ListRecords)
// init rule router group
ruleGroup := revsuit.http.ApiGroup.Group("/rule")
@@ -84,6 +93,11 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup.POST("", rmi.UpsertRules)
rmiGroup.DELETE("", rmi.DeleteRules)
ftpGroup = ruleGroup.Group("/ftp")
ftpGroup.GET("", ftp.ListRules)
ftpGroup.POST("", ftp.UpsertRules)
ftpGroup.DELETE("", ftp.DeleteRules)
// init file router group
fileGroup := revsuit.http.ApiGroup.Group("/file")
fileGroup.GET("/mysql/:id", mysql.GetFile)
+23 -4
View File
@@ -5,6 +5,7 @@ import (
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/notice"
"github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql"
http "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
@@ -13,10 +14,13 @@ import (
)
type Revsuit struct {
logLevel log.Level
http *http.Server
dns *dns.Server
mysql *mysql.Server
rmi *rmi.Server
ftp *ftp.Server
}
func initDatabase(dsn string) {
@@ -61,11 +65,18 @@ func initDatabase(dsn string) {
if err != nil {
log.Fatal(err.Error())
}
err = database.DB.AutoMigrate(&ftp.Record{})
if err != nil {
log.Fatal(err.Error())
}
err = database.DB.AutoMigrate(&ftp.Rule{})
if err != nil {
log.Fatal(err.Error())
}
}
func initLog(level string) {
var logLevel log.Level
func initLog(level string) (logLevel log.Level) {
switch level {
case "debug":
@@ -93,7 +104,7 @@ func initLog(level string) {
log.ConsoleConfig{
Level: logLevel,
})
return logLevel
}
func initNotice(nc noticeConfig) {
@@ -123,10 +134,11 @@ func initNotice(nc noticeConfig) {
func New(c *Config) *Revsuit {
initDatabase(c.Database)
initLog(c.LogLevel)
logLevel := initLog(c.LogLevel)
initNotice(c.Notice)
s := &Revsuit{
logLevel: logLevel,
http: http.GetServer(),
}
if c.DNS.Enable {
@@ -140,6 +152,10 @@ func New(c *Config) *Revsuit {
s.rmi = rmi.GetServer()
s.rmi.Config = c.RMI
}
if c.FTP.Enable {
s.ftp = ftp.GetServer()
s.ftp.Config = c.FTP
}
if c.Addr != "" {
s.http.SetAddr(c.Addr)
@@ -166,6 +182,9 @@ func (revsuit *Revsuit) Run() {
if revsuit.rmi != nil {
go revsuit.rmi.Run()
}
if revsuit.ftp != nil {
go revsuit.ftp.Run()
}
revsuit.http.Run()
}