feat(rmi): support receive rmi connection (#5)

This commit is contained in:
Li4n0
2021-04-23 17:58:46 +08:00
committed by GitHub
parent 72c6150517
commit 5e24f9530d
25 changed files with 1101 additions and 20 deletions
+3
View File
@@ -8,6 +8,9 @@ http:
ip_header:
dns:
enable: true
rmi:
enable: true
addr: :1099
mysql:
enable: true
addr: :3306
+6
View File
@@ -17,6 +17,9 @@
<a-menu-item key="/logs/dns">
<router-link to="/logs/dns">DNS Logs</router-link>
</a-menu-item>
<a-menu-item key="/logs/rmi">
<router-link to="/logs/rmi">RMI Logs</router-link>
</a-menu-item>
<a-menu-item key="/logs/mysql">
<router-link to="/logs/mysql">MySQL Logs</router-link>
</a-menu-item>
@@ -29,6 +32,9 @@
<a-menu-item key="/rules/dns">
<router-link to="/rules/dns">DNS Rules</router-link>
</a-menu-item>
<a-menu-item key="/rules/rmi">
<router-link to="/rules/rmi">RMI Rules</router-link>
</a-menu-item>
<a-menu-item key="/rules/mysql">
<router-link to="/rules/mysql">MySQL Rules</router-link>
</a-menu-item>
+11
View File
@@ -31,4 +31,15 @@ export function getMysqlRecord(params) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getRmiRecord(params) {
return request({
url: '/record/rmi',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+33
View File
@@ -98,4 +98,37 @@ export function deleteMysqlRule(data) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function getRmiRule(params) {
return request({
url: '/rule/rmi',
params: params,
method: 'get',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function upsertRmiRule(data) {
return request({
url: '/rule/rmi',
data: data,
method: 'post',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
export function deleteRmiRule(data) {
return request({
url: '/rule/rmi',
data: data,
method: 'delete',
validateStatus: function (status) {
return status >= 200 && status < 300 // 默认的
}
})
}
+10
View File
@@ -25,6 +25,11 @@ const routes = [
name: 'MysqlLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql')
},
{
path: '/logs/rmi',
name: 'RmiLogs',
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Rmi')
},
{
path: '/rules/http',
name: 'HttpRules',
@@ -39,6 +44,11 @@ const routes = [
path: '/rules/mysql',
name: 'MysqlRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql')
},
{
path: '/rules/rmi',
name: 'RmiRules',
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Rmi')
}
]
+1
View File
@@ -131,6 +131,7 @@ const columns = [
title: 'PATH',
dataIndex: 'path',
key: 'path',
ellipsis: true,
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
+171
View File
@@ -0,0 +1,171 @@
<template>
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {
filters[column.dataIndex] = selectedKeys[0];
fetch()
}"
>
Search
</a-button>
<a-button size="small" style="width: 90px" @click="() =>{
clearFilters();
delete filters[column.dataIndex];
fetch()
}">
Reset
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="time" slot-scope="time">
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
</span>
</a-table>
</template>
<style>
.gray-table-row {
background-color: #f5f5f5;
}
</style>
<script>
import {getRmiRecord} from '@/api/record'
import {store} from '@/main'
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'REQUEST TIME',
dataIndex: 'request_time',
key: 'request_time',
scopedSlots: {customRender: 'time'},
},
{
title: 'RULE',
dataIndex: 'rule_name',
key: 'rule_name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG',
dataIndex: 'flag',
key: 'flag',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'PATH',
dataIndex: 'path',
key: 'path',
ellipsis: true,
},
{
title: 'REMOTE IP',
key: 'remote_ip',
dataIndex: 'remote_ip',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'IP AREA',
key: 'ip_area',
dataIndex: 'ip_area'
}
];
export default {
name: 'DnsLogs',
data() {
return {
data: [],
pagination: {current: 1},
filters: {},
order: "desc",
loading: false,
columns,
};
},
methods: {
handleTableChange(pagination, filters, sorter) {
const pager = {...this.pagination};
pager.current = pagination.current;
this.pagination = pager;
this.order = sorter.order === "ascend" ? "asc" : "desc"
this.fetch();
},
fetch: function () {
this.loading = true;
let params = {
...this.filters,
page: this.pagination.current,
order: this.order
}
getRmiRecord(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>
+1 -8
View File
@@ -137,11 +137,6 @@
</a-tag>
</span>
<span slot="files" slot-scope="files">
<!-- eslint-disable-next-line-->
{{ files.length > 30 ? files.substr(0, 30) + "..." : files }}
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
@@ -221,9 +216,7 @@ const columns = [
title: 'FILES',
dataIndex: 'files',
key: 'files',
scopedSlots: {
customRender: "files"
}
ellipsis: true
},
{
title: 'PUSH TO CLIENT',
+337
View File
@@ -0,0 +1,337 @@
<template xmlns:a-col="http://www.w3.org/1999/html">
<div>
<a-button id="add-rule" type="primary" @click="addRule">
<a-icon type="plus"/>
New Rule
</a-button>
<!-- rule form-->
<a-drawer
:title="formAction+ ' Dns rule'"
:width="460"
:visible="formVisible"
:body-style="{ paddingBottom: '80px' }"
@close="closeDrawer"
>
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
<BasicRule :form="form" :readOnly="formReadOnly"/>
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
Cancel
</a-button>
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
Submit
</a-button>
</div>
</a-drawer>
<!-- rule table -->
<a-table
:columns="columns"
:data-source="data"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<div
slot="filterDropdown"
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
style="padding: 8px"
>
<a-input
:placeholder="`Search ${column.dataIndex}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block;"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
/>
<a-button
type="primary"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
>
Search
</a-button>
</div>
<a-icon
slot="filterIcon"
slot-scope="filtered"
type="search"
:style="{ color: filtered ? '#108ee9' : undefined }"
/>
<span slot="rank" slot-scope="rank">
<a-tag
:color="'#'+(0x2db7f5+rank*80).toString(16)"
>
{{ rank }}
</a-tag>
</span>
<span slot="type" slot-scope="type">
<a-tag
:color="colors[type]"
>
{{ resolveTypes[type] }}
</a-tag>
</span>
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
</span>
<span slot="valueRender" slot-scope="values">
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
</span>
<span slot="action" slot-scope="text,record,index">
<!-- <a-button @click="viewRule(record)" style="-->
<!-- color: #67C23A;-->
<!-- background-color: transparent;-->
<!-- border-color: #67C23A;-->
<!-- text-shadow: none;-->
<!-- margin-right: 10px;-->
<!--" size="small" ghost>View</a-button>-->
<a-button @click="editRule(record,index)" style="
color: #909399;
background-color: transparent;
border-color: #909399;
text-shadow: none;
margin-right: 10px;
" size="small" ghost>Edit</a-button>
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="deleteRule(record,index)"
>
<a-button type="danger" size="small" ghost>Delete</a-button>
</a-popconfirm>
</span>
</a-table>
</div>
</template>
<style scoped>
#add-rule {
margin-bottom: 10px;
}
</style>
<script>
import {getRmiRule, upsertRmiRule, deleteRmiRule} from '@/api/rule'
import {store} from '@/main'
import BasicRule from "@/components/BasicRule";
const VIEW = "View"
const EDIT = "Edit"
const CREATE = "Create"
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
sortDirections: ['descend', 'ascend'],
},
{
title: 'NAME',
dataIndex: 'name',
key: 'name',
scopedSlots: {
filterDropdown: 'filterDropdown',
filterIcon: 'filterIcon',
},
},
{
title: 'FLAG FORMAT',
dataIndex: 'flag_format',
key: 'flag_format',
},
{
title: 'RANK',
dataIndex: 'rank',
key: 'rank',
scopedSlots: {
customRender: 'rank',
},
},
{
title: 'PUSH TO CLIENT',
dataIndex: 'push_to_client',
key: 'push_to_client',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'NOTICE',
dataIndex: 'notice',
key: 'notice',
scopedSlots: {
customRender: 'switchRender',
}
},
{
title: 'Action',
key: 'action',
scopedSlots: {customRender: 'action'},
},
];
export default {
name: 'RmiRules',
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
}
getRmiRule(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]
upsertRmiRule(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) {
deleteRmiRule(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) {
upsertRmiRule(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>
+1 -1
View File
@@ -82,7 +82,7 @@ func (s *Server) Run() {
log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return nil, nil
}
log.Trace("DNS record(id:%d) has been created", r.ID)
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 {
+6 -1
View File
@@ -42,7 +42,7 @@ func newRecord(rule *Rule, flag, domain, remoteIp, ipArea string) (r *Record, er
return r, err
}
func List(c *gin.Context) {
func ListRecords(c *gin.Context) {
var (
dnsRecord Record
res []Record
@@ -80,6 +80,11 @@ func List(c *gin.Context) {
})
return
}
if order != "desc" && 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",
+4
View File
@@ -107,6 +107,10 @@ func ListRules(c *gin.Context) {
return
}
if order != "desc" && 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",
+3 -3
View File
@@ -125,7 +125,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Error("MySQL record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return
}
log.Trace("MySQL record(id:%d) has been created", r.ID)
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 {
@@ -267,10 +267,10 @@ func (s *Server) Run() {
var authServer = &vmysql.AuthServerNone{}
var err error
log.Info("Starting Mysql Server at %s", s.Addr)
log.Info("Starting MySQL Server at %s", s.Addr)
s.listener, err = vmysql.NewListener("tcp", s.Addr, authServer, s, s.VersionString, 0, 0)
if err != nil {
log.Error("New Mysql Server failed: %s", err)
log.Error("New MySQL Server failed: %s", err)
os.Exit(-1)
}
+7 -1
View File
@@ -49,13 +49,14 @@ func newRecord(rule *Rule, flag, username, clientName, clientOS, remoteIp, ipAre
return r, err
}
func List(c *gin.Context) {
func ListRecords(c *gin.Context) {
var (
mysqlRecord Record
res []Record
count int64
order = c.Query("order")
)
if err := c.ShouldBind(&mysqlRecord); err != nil {
c.JSON(400, gin.H{
"status": "failed",
@@ -90,6 +91,11 @@ func List(c *gin.Context) {
})
return
}
if order != "desc" && order != "asc" {
order = "desc"
}
if err := db.Preload("Files").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
c.JSON(400, gin.H{
"status": "failed",
+5
View File
@@ -89,6 +89,11 @@ func ListRules(c *gin.Context) {
})
return
}
if order != "desc" && 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",
+1 -1
View File
@@ -157,7 +157,7 @@ func (s *Server) Receive(c *gin.Context) {
c.String(code, compileTpl(c, _rule.ResponseBody))
return
}
log.Trace("HTTP record(id:%d) has been created", r.ID)
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 {
+4
View File
@@ -89,6 +89,10 @@ func ListRecords(c *gin.Context) {
return
}
if order != "desc" && 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",
+4
View File
@@ -108,6 +108,10 @@ func ListRules(c *gin.Context) {
return
}
if order != "desc" && 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",
+6
View File
@@ -0,0 +1,6 @@
package rmi
type Config struct {
Enable bool
Addr string
}
+103
View File
@@ -0,0 +1,103 @@
package rmi
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 {
Path string `form:"path" json:"path"`
record.BaseRecord
Rule Rule `gorm:"foreignKey:RuleName;references:Name;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" form:"-" json:"-" notice:"-"`
}
func (Record) TableName() string {
return "rmi_records"
}
func (r Record) Notice() {
notice.Notice(r)
}
func NewRecord(rule *Rule, flag, path, ip, area string) (r *Record, err error) {
r = &Record{
BaseRecord: record.BaseRecord{
Flag: flag,
RemoteIP: ip,
IpArea: area,
RequestTime: time.Now(),
},
Path: path,
Rule: *rule,
}
err = database.DB.Create(r).Error
return r, err
}
func ListRecords(c *gin.Context) {
var (
rmiRecord Record
res []Record
count int64
order = c.Query("order")
)
if err := c.ShouldBind(&rmiRecord); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err,
"result": nil,
})
return
}
db := database.DB.Model(&rmiRecord)
if rmiRecord.Flag != "" {
db.Where("flag = ?", rmiRecord.Flag)
}
if rmiRecord.Path != "" {
db.Where("path like ?", "%"+rmiRecord.Path+"%")
}
if rmiRecord.RemoteIP != "" {
db.Where("remote_ip = ?", rmiRecord.RemoteIP)
}
if rmiRecord.RuleName != "" {
db.Where("rule_name = ?", rmiRecord.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 != "desc" && 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},
})
}
+158
View File
@@ -0,0 +1,158 @@
package rmi
import (
"bytes"
"encoding/binary"
"net"
"strconv"
"strings"
"sync"
"time"
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/qqwry"
log "unknwon.dev/clog/v2"
)
type Server struct {
Config
rules []*Rule
rulesLock sync.RWMutex
}
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()
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())
}
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
log.Error("RMI read connection error:%v", err.Error())
}
if !bytes.Contains(buf, []byte{0x4a, 0x52, 0x4d, 0x49}) {
return
}
send := []byte{0x4e}
bs := make([]byte, 8)
binary.BigEndian.PutUint16(bs, uint16(len(ip)))
send = append(send, bs...)
send = append(send, []byte(ip)...)
send = append(send, []byte{0x00, 0x00}...)
uintPort, _ := strconv.Atoi(port)
bs = make([]byte, 8)
binary.BigEndian.PutUint16(bs, uint16(uintPort))
send = append(send, bs...)
_, err = conn.Write(send)
if err != nil {
log.Error("RMI write connection error: %v", err.Error())
}
data := make([]byte, 512)
for length := 0; length < 50; {
n, err := conn.Read(data)
if err != nil {
log.Error("RMI read connection error: %v", err.Error())
}
length += n
}
frags := bytes.Split(data, []byte{0xdf, 0x74})
path := strings.TrimRight(string(frags[len(frags)-1][2:]), "\x00")
for _, _rule := range s.getRules() {
flag, flagGroup := _rule.Match(path)
if flag == "" {
continue
}
area := qqwry.Area(ip)
// 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())
return
}
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 {
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("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)
}
//send notice
if _rule.Notice {
go func() {
r.Notice()
log.Trace("RMI 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 RMI Server at %v", s.Addr)
listener, err := net.Listen("tcp", s.Addr)
if err != nil {
log.Fatal(err.Error())
}
for {
tcpConn, err := listener.Accept()
if err != nil {
log.Error("RMI accept connection error: %v", err.Error())
continue
}
go s.handleConnection(tcpConn)
}
}
+193
View File
@@ -0,0 +1,193 @@
package rmi
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"
)
// Http rule struct
type Rule struct {
rule.BaseRule
}
func (Rule) TableName() string {
return "rmi_rules"
}
// New rmi rule struct
func NewRule(name, flagFormat string, pushToClient, notice bool) *Rule {
return &Rule{
BaseRule: rule.BaseRule{
Name: name,
FlagFormat: flagFormat,
PushToClient: pushToClient,
Notice: notice,
},
}
}
// Create or update the rmi 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",
"push_to_client",
"notice",
}),
}).Create(r).Error
if err != nil {
return
}
err = GetServer().updateRules()
return err
}
// Delete the rmi 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
}
err = GetServer().updateRules()
return err
}
// List all rmi rules those satisfy the filter
func ListRules(c *gin.Context) {
var (
rmiRule Rule
res []Rule
count int64
order = c.Query("order")
)
if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err,
"result": nil,
})
return
}
db := database.DB.Model(&rmiRule)
if rmiRule.Name != "" {
db.Where("name = ?", rmiRule.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 != "desc" && 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.Error(),
"data": nil,
})
return
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": gin.H{"count": count, "data": res},
})
}
// Create or update rmi rule from user submit
func UpsertRules(c *gin.Context) {
var (
rmiRule Rule
update bool
)
if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if rmiRule.ID != 0 {
update = true
}
if err := rmiRule.CreateOrUpdate(); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"result": nil,
})
return
}
if update {
log.Trace("RMI rule(id:%d) has been updated", rmiRule.ID)
} else {
log.Trace("RMI rule(id:%d) has been created", rmiRule.ID)
}
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"result": nil,
})
}
// Delete rmi rule from user submit
func DeleteRules(c *gin.Context) {
var rmiRule Rule
if err := c.ShouldBind(&rmiRule); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
if err := rmiRule.Delete(); err != nil {
c.JSON(400, gin.H{
"status": "failed",
"error": err.Error(),
"data": nil,
})
return
}
log.Trace("RMI rule(id:%d) has been deleted", rmiRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
"error": nil,
"data": nil,
})
}
+3 -1
View File
@@ -4,6 +4,7 @@ import (
"github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
)
type noticeConfig struct {
@@ -21,5 +22,6 @@ type Config struct {
Notice noticeConfig
rhttp.Config
DNS dns.Config
Mysql mysql.Config
MySQL mysql.Config
RMI rmi.Config
}
+11 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
log "unknwon.dev/clog/v2"
)
@@ -52,10 +53,13 @@ func (revsuit *Revsuit) registerHttpRouter() {
httpGroup.GET("", rhttp.ListRecords)
dnsGroup := recordGroup.Group("/dns")
dnsGroup.GET("", dns.List)
dnsGroup.GET("", dns.ListRecords)
mysqlGroup := recordGroup.Group("/mysql")
mysqlGroup.GET("", mysql.List)
mysqlGroup.GET("", mysql.ListRecords)
rmiGroup := recordGroup.Group("/rmi")
rmiGroup.GET("", rmi.ListRecords)
// init rule router group
ruleGroup := revsuit.http.ApiGroup.Group("/rule")
@@ -75,6 +79,11 @@ func (revsuit *Revsuit) registerHttpRouter() {
mysqlGroup.POST("", mysql.UpsertRules)
mysqlGroup.DELETE("", mysql.DeleteRules)
rmiGroup = ruleGroup.Group("/rmi")
rmiGroup.GET("", rmi.ListRules)
rmiGroup.POST("", rmi.UpsertRules)
rmiGroup.DELETE("", rmi.DeleteRules)
// init file router group
fileGroup := revsuit.http.ApiGroup.Group("/file")
fileGroup.GET("/mysql/:id", mysql.GetFile)
+19 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/li4n0/revsuit/pkg/dns"
"github.com/li4n0/revsuit/pkg/mysql"
http "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
"gorm.io/gorm/logger"
log "unknwon.dev/clog/v2"
)
@@ -15,6 +16,7 @@ type Revsuit struct {
http *http.Server
dns *dns.Server
mysql *mysql.Server
rmi *rmi.Server
}
func initDatabase(dsn string) {
@@ -51,6 +53,14 @@ func initDatabase(dsn string) {
if err != nil {
log.Fatal(err.Error())
}
err = database.DB.AutoMigrate(&rmi.Record{})
if err != nil {
log.Fatal(err.Error())
}
err = database.DB.AutoMigrate(&rmi.Rule{})
if err != nil {
log.Fatal(err.Error())
}
}
@@ -122,9 +132,13 @@ func New(c *Config) *Revsuit {
if c.DNS.Enable {
s.dns = dns.GetServer()
}
if c.Mysql.Enable {
if c.MySQL.Enable {
s.mysql = mysql.GetServer()
s.mysql.Config = c.Mysql
s.mysql.Config = c.MySQL
}
if c.RMI.Enable {
s.rmi = rmi.GetServer()
s.rmi.Config = c.RMI
}
if c.Addr != "" {
@@ -149,6 +163,9 @@ func (revsuit *Revsuit) Run() {
if revsuit.mysql != nil {
go revsuit.mysql.Run()
}
if revsuit.rmi != nil {
go revsuit.rmi.Run()
}
revsuit.http.Run()
}