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 # local files
qqwry.dat qqwry.dat
revsuit.db revsuit.db
revsuit.db-shm
revsuit.db-wal
# local config # local config
config.yaml config.yaml
+7 -2
View File
@@ -1,8 +1,8 @@
version: 4.0 version: 4.0
addr: :10000 addr: :10000
token: token token:
database: revsuit.db database: revsuit.db
log_level: debug log_level: info
http: http:
ip_header: ip_header:
@@ -15,6 +15,11 @@ mysql:
enable: true enable: true
addr: :3306 addr: :3306
version_string: 10.4.13-MariaDB-log 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: notice:
dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token} dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token}
+12 -6
View File
@@ -3,12 +3,12 @@
<a-layout-sider v-model="collapsed" :trigger="null" collapsible> <a-layout-sider v-model="collapsed" :trigger="null" collapsible>
<div class="logo"><b>R</b><span v-if="!collapsed"><b>ev</b>Suit</span></div> <div class="logo"><b>R</b><span v-if="!collapsed"><b>ev</b>Suit</span></div>
<a-menu theme="dark" mode="inline" :selectedKeys="[this.$route.path]" :open-keys.sync='openKeys'> <a-menu theme="dark" mode="inline" :selectedKeys="[this.$route.path]" :open-keys.sync='openKeys'>
<!-- <a-menu-item key="/">--> <!-- <a-menu-item key="/">-->
<!-- <router-link to="/">--> <!-- <router-link to="/">-->
<!-- <a-icon type="dashboard"/>--> <!-- <a-icon type="dashboard"/>-->
<!-- <span>Dashboard</span>--> <!-- <span>Dashboard</span>-->
<!-- </router-link>--> <!-- </router-link>-->
<!-- </a-menu-item>--> <!-- </a-menu-item>-->
<a-sub-menu key="logs"> <a-sub-menu key="logs">
<span slot="title"><a-icon type="bar-chart"/><span>Logs</span></span> <span slot="title"><a-icon type="bar-chart"/><span>Logs</span></span>
<a-menu-item key="/logs/http"> <a-menu-item key="/logs/http">
@@ -23,6 +23,9 @@
<a-menu-item key="/logs/mysql"> <a-menu-item key="/logs/mysql">
<router-link to="/logs/mysql">MySQL Logs</router-link> <router-link to="/logs/mysql">MySQL Logs</router-link>
</a-menu-item> </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>
<a-sub-menu key="rules"> <a-sub-menu key="rules">
<span slot="title"><a-icon type="radar-chart"/><span>Rules</span></span> <span slot="title"><a-icon type="radar-chart"/><span>Rules</span></span>
@@ -38,6 +41,9 @@
<a-menu-item key="/rules/mysql"> <a-menu-item key="/rules/mysql">
<router-link to="/rules/mysql">MySQL Rules</router-link> <router-link to="/rules/mysql">MySQL Rules</router-link>
</a-menu-item> </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-sub-menu>
</a-menu> </a-menu>
</a-layout-sider> </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" @cancel="cancel"
@ok="auth" @ok="auth"
> >
<a-input v-model.lazy="token" placeholder="Your token"/> <a-input v-model.lazy="token" @pressEnter="auth" placeholder="Your token"/>
</a-modal> </a-modal>
</div> </div>
</template> </template>
+6 -3
View File
@@ -15,10 +15,13 @@
<a-form-model-item :rules="rules.flagFormat" prop="flag_format"> <a-form-model-item :rules="rules.flagFormat" prop="flag_format">
<span slot="label"> <span slot="label">
Flag Format&nbsp; Flag Format&nbsp;
<a-tooltip title="1. Only when the request contains content that satisfies the flag format, the request will be captured. <a-tooltip title="Basic usage:
2. Please use regular expression syntax. 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. 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-icon type="question-circle-o"/>
</a-tooltip> </a-tooltip>
</span> </span>
+18 -8
View File
@@ -13,42 +13,52 @@ const routes = [
{ {
path: '/logs/http', path: '/logs/http',
name: 'HttpLogs', name: 'HttpLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Http') component: () => import( '../views/logs/Http')
}, },
{ {
path: '/logs/dns', path: '/logs/dns',
name: 'DnsLogs', name: 'DnsLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Dns') component: () => import( '../views/logs/Dns')
}, },
{ {
path: '/logs/mysql', path: '/logs/mysql',
name: 'MysqlLogs', name: 'MysqlLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql') component: () => import( '../views/logs/Mysql')
}, },
{ {
path: '/logs/rmi', path: '/logs/rmi',
name: 'RmiLogs', 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', path: '/rules/http',
name: 'HttpRules', name: 'HttpRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Http') component: () => import( '../views/rules/Http')
}, },
{ {
path: '/rules/dns', path: '/rules/dns',
name: 'DnsRules', name: 'DnsRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Dns') component: () => import( '../views/rules/Dns')
}, },
{ {
path: '/rules/mysql', path: '/rules/mysql',
name: 'MysqlRules', name: 'MysqlRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql') component: () => import( '../views/rules/Mysql')
}, },
{ {
path: '/rules/rmi', path: '/rules/rmi',
name: 'RmiRules', 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' import {store} from '@/main'
const colors = { const colors = {
"GET": "green", "GET": "#52c41a",
"POST": "red", "POST": "#f5222d",
"HEAD": "pink", "PUT": "#eb2f96",
"PUT": "geekblue", "HEAD": "#02a7ff",
"OPTIONS": "cyan", "OPTIONS": "#13c2c2",
"DELETE": "purple", "DELETE": "#722ed1",
} }
const columns = [ const columns = [
+25 -7
View File
@@ -9,7 +9,23 @@
> >
<div v-if="record.files.length" slot="expandedRowRender" slot-scope="record" style="margin: 0"> <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> <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>
<div <div
slot="filterDropdown" slot="filterDropdown"
@@ -58,12 +74,12 @@
</span> </span>
<span slot="loadData" slot-scope="loadData"> <span slot="loadData" slot-scope="loadData">
<a-tag v-if="loadData" <a-tag v-if="loadData"
color="green" color="#eb2f96"
>True</a-tag><a-tag v-else color="red">False</a-tag> >True</a-tag><a-tag v-else color="#f5222d">False</a-tag>
</span> </span>
<span slot="fileNum" slot-scope="files"> <span slot="fileNum" slot-scope="files">
<a-tag v-if="files.length>=3" <a-tag v-if="files.length>=3"
color="purple" color="#722ed1"
>{{ files.length }}</a-tag> >{{ files.length }}</a-tag>
<a-tag v-else :color="colors[files.length]"> <a-tag v-else :color="colors[files.length]">
{{ files.length }} {{ files.length }}
@@ -82,9 +98,9 @@ import {getMysqlRecord} from '@/api/record'
import {store} from '@/main' import {store} from '@/main'
const colors = [ const colors = [
"geekblue", "#13c2c2",
"blue", "#52c41a",
"pink", "#02a7ff",
] ]
const columns = [ const columns = [
@@ -133,6 +149,8 @@ const columns = [
dataIndex: 'load_local_data', dataIndex: 'load_local_data',
key: 'load_local_data', key: 'load_local_data',
scopedSlots: { scopedSlots: {
filterDropdown: 'selectDropdown',
filterIcon: 'filterIcon',
customRender: "loadData", customRender: "loadData",
} }
}, },
+18 -10
View File
@@ -6,7 +6,7 @@
</a-button> </a-button>
<!-- rule form--> <!-- rule form-->
<a-drawer <a-drawer
:title="formAction+ ' Dns rule'" :title="formAction+ ' DNS rule'"
:width="460" :width="460"
:visible="formVisible" :visible="formVisible"
:body-style="{ paddingBottom: '80px' }" :body-style="{ paddingBottom: '80px' }"
@@ -47,7 +47,14 @@
</a-row> </a-row>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="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 <a-input
v-model="form.value" v-model="form.value"
style="width: 100%" style="width: 100%"
@@ -139,19 +146,19 @@
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span> <span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span> </span>
<span slot="action" slot-scope="text,record,index"> <span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="--> <a-button @click="viewRule(record)" style="
<!-- color: #67C23A;--> color: #67C23A;
<!-- background-color: transparent;--> background-color: transparent;
<!-- border-color: #67C23A;--> border-color: #67C23A;
<!-- text-shadow: none;--> text-shadow: none;
<!-- margin-right: 10px;--> margin:0 10px 3px 0;
<!--" size="small" ghost>View</a-button>--> " size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style=" <a-button @click="editRule(record,index)" style="
color: #909399; color: #909399;
background-color: transparent; background-color: transparent;
border-color: #909399; border-color: #909399;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button> " size="small" ghost>Edit</a-button>
<a-popconfirm <a-popconfirm
title="Are you sure delete this task?" title="Are you sure delete this task?"
@@ -219,6 +226,7 @@ const columns = [
title: 'FLAG FORMAT', title: 'FLAG FORMAT',
dataIndex: 'flag_format', dataIndex: 'flag_format',
key: 'flag_format', key: 'flag_format',
ellipsis: true,
}, },
{ {
title: 'RANK', 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"> <span slot="label">
Response Status Code Response Status Code
<a-tooltip <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-icon type="question-circle-o"/>
</a-tooltip> </a-tooltip>
</span> </span>
@@ -39,7 +39,7 @@
<span slot="label"> <span slot="label">
Response Headers Response Headers
<a-tooltip <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-icon type="question-circle-o"/>
</a-tooltip> </a-tooltip>
</span> </span>
@@ -77,13 +77,14 @@
<span slot="label"> <span slot="label">
Response Body Response Body
<a-tooltip <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-icon type="question-circle-o"/>
</a-tooltip> </a-tooltip>
</span> </span>
<a-textarea v-model="form.response_body" <a-textarea v-model="form.response_body"
placeholder="Hello RevSuit!" placeholder="Hello RevSuit!"
:readOnly="formReadOnly" :readOnly="formReadOnly"
rows="10"
/> />
</a-form-model-item> </a-form-model-item>
</a-col> </a-col>
@@ -164,14 +165,14 @@
background-color: transparent; background-color: transparent;
border-color: #67C23A; border-color: #67C23A;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>View</a-button> " size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style=" <a-button @click="editRule(record,index)" style="
color: #909399; color: #909399;
background-color: transparent; background-color: transparent;
border-color: #909399; border-color: #909399;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button> " size="small" ghost>Edit</a-button>
<a-popconfirm <a-popconfirm
title="Are you sure delete this task?" title="Are you sure delete this task?"
@@ -221,6 +222,7 @@ const columns = [
title: 'FLAG FORMAT', title: 'FLAG FORMAT',
dataIndex: 'flag_format', dataIndex: 'flag_format',
key: 'flag_format', key: 'flag_format',
ellipsis: true,
}, },
{ {
title: 'RANK', title: 'RANK',
+3 -2
View File
@@ -146,14 +146,14 @@
background-color: transparent; background-color: transparent;
border-color: #67C23A; border-color: #67C23A;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>View</a-button> " size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style=" <a-button @click="editRule(record,index)" style="
color: #909399; color: #909399;
background-color: transparent; background-color: transparent;
border-color: #909399; border-color: #909399;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button> " size="small" ghost>Edit</a-button>
<a-popconfirm <a-popconfirm
title="Are you sure delete this task?" title="Are you sure delete this task?"
@@ -203,6 +203,7 @@ const columns = [
title: 'FLAG FORMAT', title: 'FLAG FORMAT',
dataIndex: 'flag_format', dataIndex: 'flag_format',
key: 'flag_format', key: 'flag_format',
ellipsis: true,
}, },
{ {
title: 'RANK', title: 'RANK',
+10 -16
View File
@@ -6,7 +6,7 @@
</a-button> </a-button>
<!-- rule form--> <!-- rule form-->
<a-drawer <a-drawer
:title="formAction+ ' Dns rule'" :title="formAction+ ' RMI rule'"
:width="460" :width="460"
:visible="formVisible" :visible="formVisible"
:body-style="{ paddingBottom: '80px' }" :body-style="{ paddingBottom: '80px' }"
@@ -79,13 +79,6 @@
{{ rank }} {{ rank }}
</a-tag> </a-tag>
</span> </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"> <span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch> <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 v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span> </span>
<span slot="action" slot-scope="text,record,index"> <span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="--> <a-button @click="viewRule(record)" style="
<!-- color: #67C23A;--> color: #67C23A;
<!-- background-color: transparent;--> background-color: transparent;
<!-- border-color: #67C23A;--> border-color: #67C23A;
<!-- text-shadow: none;--> text-shadow: none;
<!-- margin-right: 10px;--> margin:0 10px 3px 0;
<!--" size="small" ghost>View</a-button>--> " size="small" ghost>View</a-button>
<a-button @click="editRule(record,index)" style=" <a-button @click="editRule(record,index)" style="
color: #909399; color: #909399;
background-color: transparent; background-color: transparent;
border-color: #909399; border-color: #909399;
text-shadow: none; text-shadow: none;
margin-right: 10px; margin:0 10px 3px 0;
" size="small" ghost>Edit</a-button> " size="small" ghost>Edit</a-button>
<a-popconfirm <a-popconfirm
title="Are you sure delete this task?" title="Are you sure delete this task?"
@@ -156,6 +149,7 @@ const columns = [
title: 'FLAG FORMAT', title: 'FLAG FORMAT',
dataIndex: 'flag_format', dataIndex: 'flag_format',
key: 'flag_format', key: 'flag_format',
ellipsis: true,
}, },
{ {
title: 'RANK', title: 'RANK',
+8
View File
@@ -1,11 +1,19 @@
package database package database
import ( import (
"strings"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
func NewSqlite3(dsn string) (*gorm.DB, error) { 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{}) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
if err != nil { if err != nil {
return nil, err return nil, err
+4 -1
View File
@@ -1,7 +1,10 @@
package qqwry package qqwry
// Area return IpArea according to ip // Area returns IpArea according to ip
func Area(ip string) string { func Area(ip string) string {
defer func() {
_ = recover()
}()
if GetQQWry() == nil { if GetQQWry() == nil {
return "" 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 ( import (
"regexp" "regexp"
"strings"
"time" "time"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
@@ -25,7 +26,9 @@ type BaseRule struct {
Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"` 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.flagCatcher == nil {
if br.FlagFormat == "*" { if br.FlagFormat == "*" {
flag = "*" flag = "*"
@@ -41,13 +44,22 @@ func (br BaseRule) Match(s string) (flag, flagGroup string) {
} }
matched := br.flagCatcher.FindStringSubmatch(s) matched := br.flagCatcher.FindStringSubmatch(s)
groupNames := br.flagCatcher.SubexpNames()
if len(matched) == 0 { if len(matched) == 0 {
return return
} }
flag = matched[0] flag = matched[0]
if len(matched) > 1 { if len(matched) > 1 && len(groupNames) == 0 {
flagGroup = matched[1] 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/database"
"github.com/li4n0/revsuit/internal/newdns" "github.com/li4n0/revsuit/internal/newdns"
"github.com/li4n0/revsuit/internal/qqwry" "github.com/li4n0/revsuit/internal/qqwry"
"github.com/li4n0/revsuit/internal/rule"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
@@ -72,7 +73,7 @@ func (s *Server) Run() {
ip := strings.Split(remoteAddr, ":")[0] ip := strings.Split(remoteAddr, ":")[0]
for _, _rule := range s.getRules() { for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(domain) flag, flagGroup, vars := _rule.Match(domain)
if flag == "" { if flag == "" {
continue continue
} }
@@ -82,7 +83,7 @@ func (s *Server) Run() {
log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error()) log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return nil, nil 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. //only send to client when this connection recorded first time.
if _rule.PushToClient { 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) 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] has been put to client message queue", r.ID)
} }
} 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] has been put to client message queue", r.ID)
} }
} }
@@ -103,12 +104,12 @@ 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)
}() }()
} }
if _rule.Value != "" { if _rule.Value != "" {
value := rule.CompileTpl(_rule.Value, vars)
_type := _rule.Type _type := _rule.Type
if _rule.Type == newdns.REBINDING { if _rule.Type == newdns.REBINDING {
_type = newdns.A _type = newdns.A
@@ -121,16 +122,16 @@ func (s *Server) Run() {
Records: func() []newdns.Record { Records: func() []newdns.Record {
switch _rule.Type { switch _rule.Type {
case newdns.TXT: case newdns.TXT:
return []newdns.Record{{Data: []string{_rule.Value}}} return []newdns.Record{{Data: []string{value}}}
case newdns.CNAME, newdns.NS: case newdns.CNAME, newdns.NS:
return []newdns.Record{{Address: _rule.Value + "."}} return []newdns.Record{{Address: value + "."}}
case newdns.REBINDING: case newdns.REBINDING:
// Get rebinding ip list // Get rebinding ip list
values, ok := rebindingCache.Get(ip) values, ok := rebindingCache.Get(ip)
if !ok { if !ok {
rebindingCache.Set(ip, strings.Split(_rule.Value, ","), cache.DefaultExpiration) rebindingCache.Set(ip, strings.Split(value, ","), cache.DefaultExpiration)
values = strings.Split(_rule.Value, ",") values = strings.Split(value, ",")
} }
//Choose and delete first ip //Choose and delete first ip
@@ -144,7 +145,7 @@ func (s *Server) Run() {
log.Trace("DNS rebinding client(ip:%v) to %v", ip, value) log.Trace("DNS rebinding client(ip:%v) to %v", ip, value)
return []newdns.Record{{Address: value}} return []newdns.Record{{Address: value}}
default: default:
return []newdns.Record{{Address: _rule.Value}} return []newdns.Record{{Address: value}}
} }
}(), }(),
TTL: _rule.TTL * time.Second, TTL: _rule.TTL * time.Second,
+3 -3
View File
@@ -157,9 +157,9 @@ func UpsertRules(c *gin.Context) {
} }
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{
@@ -191,7 +191,7 @@ func DeleteRules(c *gin.Context) {
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",
+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() { for _, _rule := range s.getRules() {
flag, _ := _rule.Match(user + schema) userFlag, _, _ := _rule.Match(user)
if flag == "" { schemaFlag, _, _ := _rule.Match(schema)
if userFlag == "" && schemaFlag == "" {
continue continue
} }
s.connRulePool.Store(c.ConnectionID, _rule) 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") { if strings.Contains(c.ConnAttrs["_client_name"], "MySQL Connector") {
c.IsJdbcClient = true c.IsJdbcClient = true
c.SupportLoadDataLocal = 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. // ConnectionClosed is part of the mysql.Handler interface.
func (s *Server) ConnectionClosed(c *vmysql.Conn) { func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Trace("MySQL Client leaved, ID [%d]", c.ConnectionID) log.Trace("MySQL Client leaved, ID [%d]", c.ConnectionID)
var ( var (
user = c.User user = c.User
clientName string schema = c.SchemaName
clientOS string
supportLoadLocalData = c.SupportLoadDataLocal supportLoadLocalData = c.SupportLoadDataLocal
cr, ok = s.connRulePool.Load(c.ConnectionID) cr, ok = s.connRulePool.Load(c.ConnectionID)
clientName, clientOS, flag, flagGroup string
) )
if !ok { if !ok {
return return
} }
_rule := cr.(*Rule) _rule := cr.(*Rule)
flag, flagGroup := _rule.Match(user) for _, s := range []string{user, schema} {
flag, flagGroup, _ = _rule.Match(s)
if flag != "" {
break
}
}
if flag == "" { if flag == "" {
log.Error("MySQL Connection rule(%d) not match flag", c.ConnectionID) log.Error("MySQL Connection rule(%d) not match flag", c.ConnectionID)
} }
if c.ConnAttrs != nil { if c.ConnAttrs != nil {
clientName = c.ConnAttrs["_client_name"] + " " + c.ConnAttrs["_client_version"] clientName = c.ConnAttrs["_client_name"] + " " + c.ConnAttrs["_client_version"]
clientOS = c.ConnAttrs["_os"] + " " + c.ConnAttrs["_platform"] 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()) log.Error("MySQL record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return 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. //only send to client when this connection recorded first time.
if _rule.PushToClient { 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) 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("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 { } 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] has been put to client message queue", r.ID)
} }
} }
@@ -146,7 +154,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
if _rule.Notice { if _rule.Notice {
go func() { go func() {
r.Notice() 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 != "" { if mysqlRecord.ClientName != "" {
db.Where("client_name like ?", "%"+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")) page, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
+3 -3
View File
@@ -140,9 +140,9 @@ func UpsertRules(c *gin.Context) {
} }
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{
@@ -174,7 +174,7 @@ func DeleteRules(c *gin.Context) {
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",
+23 -18
View File
@@ -106,26 +106,31 @@ func getRawRequest(r *http.Request) ([]byte, error) {
return httputil.DumpRequestOut(r, true) 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 compiled = tpl
if queryVarMatcher.FindString(tpl) != "" { for _, submatch := range queryVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = queryVarMatcher.ReplaceAllString(compiled, c.Query(queryVarMatcher.FindStringSubmatch(tpl)[1])) compiled = strings.ReplaceAll(compiled, submatch[0], c.Query(submatch[1]))
} }
if bodyVarMatcher.FindString(tpl) != "" { for _, submatch := range bodyVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = bodyVarMatcher.ReplaceAllString(compiled, c.PostForm(bodyVarMatcher.FindStringSubmatch(tpl)[1])) compiled = strings.ReplaceAll(compiled, submatch[0], c.PostForm(submatch[1]))
} }
if headerVarMatcher.FindString(tpl) != "" { for _, submatch := range headerVarMatcher.FindAllStringSubmatch(tpl, -1) {
compiled = headerVarMatcher.ReplaceAllString(compiled, c.GetHeader(headerVarMatcher.FindStringSubmatch(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 return compiled
} }
func (s *Server) Receive(c *gin.Context) { func (s *Server) Receive(c *gin.Context) {
u := c.Request.URL.String() u := c.Request.URL.String()
for _, _rule := range s.getRules() { for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(u) flag, flagGroup, vars := _rule.Match(u)
if flag == "" { if flag == "" {
continue continue
} }
@@ -148,16 +153,16 @@ func (s *Server) Receive(c *gin.Context) {
// create new record // create new record
r, err := NewRecord(_rule, flag, c.Request.Method, u, ip, area, string(raw)) r, err := NewRecord(_rule, flag, c.Request.Method, u, ip, area, string(raw))
if err != nil { if err != nil {
log.Error("HTTP record(rule_id:%d) created failed :%s", _rule.ID, err.Error()) log.Error("HTTP record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode)) code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode, vars))
if err != nil || code < 100 || code > 600 { if err != nil || code < 100 || code > 600 {
code = 400 code = 400
} }
c.String(code, compileTpl(c, _rule.ResponseBody)) c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return 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. //only send to client when this connection recorded first time.
if _rule.PushToClient { 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) database.DB.Where("rule_name=? and raw 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] has been put to client message queue", r.ID)
} }
} }
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] has been put to client message queue", r.ID)
} }
//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)
}() }()
} }
for header, value := range _rule.ResponseHeaders { 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 { if err != nil || code < 100 || code > 600 {
code = 400 code = 400
} }
c.String(code, compileTpl(c, _rule.ResponseBody)) c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return return
} }
+3 -3
View File
@@ -158,9 +158,9 @@ func UpsertRules(c *gin.Context) {
} }
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{
@@ -192,7 +192,7 @@ func DeleteRules(c *gin.Context) {
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",
+8 -7
View File
@@ -49,11 +49,12 @@ func (s *Server) updateRules() error {
func (s *Server) handleConnection(conn net.Conn) { func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
if err := conn.SetDeadline(time.Now().Add(time.Second * 30)); err != nil { if err := conn.SetDeadline(time.Now().Add(time.Second * 30)); err != nil {
log.Error("RMI set connection deadline error:%v", err.Error()) log.Error("RMI set connection deadline error:%v", err.Error())
} }
ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
buf := make([]byte, 1024) buf := make([]byte, 1024)
_, err := conn.Read(buf) _, err := conn.Read(buf)
if err != nil { if err != nil {
@@ -94,7 +95,7 @@ func (s *Server) handleConnection(conn net.Conn) {
path := strings.TrimRight(string(frags[len(frags)-1][2:]), "\x00") path := strings.TrimRight(string(frags[len(frags)-1][2:]), "\x00")
for _, _rule := range s.getRules() { for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(path) flag, flagGroup, _ := _rule.Match(path)
if flag == "" { if flag == "" {
continue continue
} }
@@ -104,10 +105,10 @@ func (s *Server) handleConnection(conn net.Conn) {
// create new record // create new record
r, err := NewRecord(_rule, flag, path, ip, area) r, err := NewRecord(_rule, flag, path, ip, area)
if err != nil { 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 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. //only send to client when this connection recorded first time.
if _rule.PushToClient { 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) database.DB.Where("rule_name=? and raw 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] has been put to client message queue", r.ID)
} }
} }
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] has been put to client message queue", r.ID)
} }
//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)
}() }()
} }
} }
+4 -4
View File
@@ -10,7 +10,7 @@ import (
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
) )
// Http rule struct // RMI rule struct
type Rule struct { type Rule struct {
rule.BaseRule rule.BaseRule
} }
@@ -149,9 +149,9 @@ func UpsertRules(c *gin.Context) {
} }
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{
@@ -183,7 +183,7 @@ func DeleteRules(c *gin.Context) {
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",
+2
View File
@@ -2,6 +2,7 @@ package server
import ( import (
"github.com/li4n0/revsuit/pkg/dns" "github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql" "github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp" "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi" "github.com/li4n0/revsuit/pkg/rmi"
@@ -24,4 +25,5 @@ type Config struct {
DNS dns.Config DNS dns.Config
MySQL mysql.Config MySQL mysql.Config
RMI rmi.Config RMI rmi.Config
FTP ftp.Config
} }
+14
View File
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/frontend" "github.com/li4n0/revsuit/frontend"
"github.com/li4n0/revsuit/pkg/dns" "github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql" "github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp" "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi" "github.com/li4n0/revsuit/pkg/rmi"
@@ -15,6 +16,11 @@ import (
func (revsuit *Revsuit) registerRouter() { func (revsuit *Revsuit) registerRouter() {
revsuit.http.Router = gin.Default() revsuit.http.Router = gin.Default()
if revsuit.logLevel != log.LevelTrace {
revsuit.http.Router = gin.New()
revsuit.http.Router.Use(gin.Recovery())
}
revsuit.registerPlatformRouter() revsuit.registerPlatformRouter()
revsuit.registerHttpRouter() revsuit.registerHttpRouter()
} }
@@ -61,6 +67,9 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup := recordGroup.Group("/rmi") rmiGroup := recordGroup.Group("/rmi")
rmiGroup.GET("", rmi.ListRecords) rmiGroup.GET("", rmi.ListRecords)
ftpGroup := recordGroup.Group("/ftp")
ftpGroup.GET("", ftp.ListRecords)
// init rule router group // init rule router group
ruleGroup := revsuit.http.ApiGroup.Group("/rule") ruleGroup := revsuit.http.ApiGroup.Group("/rule")
@@ -84,6 +93,11 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup.POST("", rmi.UpsertRules) rmiGroup.POST("", rmi.UpsertRules)
rmiGroup.DELETE("", rmi.DeleteRules) rmiGroup.DELETE("", rmi.DeleteRules)
ftpGroup = ruleGroup.Group("/ftp")
ftpGroup.GET("", ftp.ListRules)
ftpGroup.POST("", ftp.UpsertRules)
ftpGroup.DELETE("", ftp.DeleteRules)
// init file router group // init file router group
fileGroup := revsuit.http.ApiGroup.Group("/file") fileGroup := revsuit.http.ApiGroup.Group("/file")
fileGroup.GET("/mysql/:id", mysql.GetFile) 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/database"
"github.com/li4n0/revsuit/internal/notice" "github.com/li4n0/revsuit/internal/notice"
"github.com/li4n0/revsuit/pkg/dns" "github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql" "github.com/li4n0/revsuit/pkg/mysql"
http "github.com/li4n0/revsuit/pkg/rhttp" http "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi" "github.com/li4n0/revsuit/pkg/rmi"
@@ -13,10 +14,13 @@ import (
) )
type Revsuit struct { type Revsuit struct {
logLevel log.Level
http *http.Server http *http.Server
dns *dns.Server dns *dns.Server
mysql *mysql.Server mysql *mysql.Server
rmi *rmi.Server rmi *rmi.Server
ftp *ftp.Server
} }
func initDatabase(dsn string) { func initDatabase(dsn string) {
@@ -61,11 +65,18 @@ func initDatabase(dsn string) {
if err != nil { if err != nil {
log.Fatal(err.Error()) 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) { func initLog(level string) (logLevel log.Level) {
var logLevel log.Level
switch level { switch level {
case "debug": case "debug":
@@ -93,7 +104,7 @@ func initLog(level string) {
log.ConsoleConfig{ log.ConsoleConfig{
Level: logLevel, Level: logLevel,
}) })
return logLevel
} }
func initNotice(nc noticeConfig) { func initNotice(nc noticeConfig) {
@@ -123,10 +134,11 @@ func initNotice(nc noticeConfig) {
func New(c *Config) *Revsuit { func New(c *Config) *Revsuit {
initDatabase(c.Database) initDatabase(c.Database)
initLog(c.LogLevel) logLevel := initLog(c.LogLevel)
initNotice(c.Notice) initNotice(c.Notice)
s := &Revsuit{ s := &Revsuit{
logLevel: logLevel,
http: http.GetServer(), http: http.GetServer(),
} }
if c.DNS.Enable { if c.DNS.Enable {
@@ -140,6 +152,10 @@ func New(c *Config) *Revsuit {
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.Config = c.FTP
}
if c.Addr != "" { if c.Addr != "" {
s.http.SetAddr(c.Addr) s.http.SetAddr(c.Addr)
@@ -166,6 +182,9 @@ func (revsuit *Revsuit) Run() {
if revsuit.rmi != nil { if revsuit.rmi != nil {
go revsuit.rmi.Run() go revsuit.rmi.Run()
} }
if revsuit.ftp != nil {
go revsuit.ftp.Run()
}
revsuit.http.Run() revsuit.http.Run()
} }