From 26755351f7a08cb6f0cca282178e67b65ae52eb5 Mon Sep 17 00:00:00 2001
From: Li4n0 <34324462+Li4n0@users.noreply.github.com>
Date: Sun, 25 Apr 2021 13:39:27 +0800
Subject: [PATCH] feat(ftp): support receive ftp connection (#6)
Co-authored-by: E99p1ant <524306184@qq.com>
---
.gitignore | 2 +
config.tpl.yaml | 9 +-
frontend/src/App.vue | 18 +-
frontend/src/api/record.js | 11 +
frontend/src/api/rule.js | 33 +++
frontend/src/components/Auth.vue | 2 +-
frontend/src/components/BasicRule.vue | 9 +-
frontend/src/router/index.js | 26 +-
frontend/src/views/logs/Ftp.vue | 220 ++++++++++++++++
frontend/src/views/logs/Http.vue | 12 +-
frontend/src/views/logs/Mysql.vue | 32 ++-
frontend/src/views/rules/Dns.vue | 28 +-
frontend/src/views/rules/Ftp.vue | 356 ++++++++++++++++++++++++++
frontend/src/views/rules/Http.vue | 18 +-
frontend/src/views/rules/Mysql.vue | 5 +-
frontend/src/views/rules/Rmi.vue | 26 +-
internal/database/sqlite3.go | 8 +
internal/qqwry/query.go | 5 +-
internal/rule/compile.go | 12 +
internal/rule/rule.go | 18 +-
pkg/dns/dns.go | 23 +-
pkg/dns/rule.go | 6 +-
pkg/ftp/config.go | 8 +
pkg/ftp/ftp.go | 230 +++++++++++++++++
pkg/ftp/record.go | 112 ++++++++
pkg/ftp/rule.go | 194 ++++++++++++++
pkg/mysql/mysql.go | 36 ++-
pkg/mysql/record.go | 7 +
pkg/mysql/rule.go | 6 +-
pkg/rhttp/http.go | 41 +--
pkg/rhttp/rule.go | 6 +-
pkg/rmi/rmi.go | 15 +-
pkg/rmi/rule.go | 8 +-
pkg/server/config.go | 2 +
pkg/server/router.go | 14 +
pkg/server/server.go | 29 ++-
36 files changed, 1446 insertions(+), 141 deletions(-)
create mode 100644 frontend/src/views/logs/Ftp.vue
create mode 100644 frontend/src/views/rules/Ftp.vue
create mode 100644 internal/rule/compile.go
create mode 100644 pkg/ftp/config.go
create mode 100644 pkg/ftp/ftp.go
create mode 100644 pkg/ftp/record.go
create mode 100644 pkg/ftp/rule.go
diff --git a/.gitignore b/.gitignore
index 399f39f..46891e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
# local files
qqwry.dat
revsuit.db
+revsuit.db-shm
+revsuit.db-wal
# local config
config.yaml
diff --git a/config.tpl.yaml b/config.tpl.yaml
index ef4193d..c1e8756 100644
--- a/config.tpl.yaml
+++ b/config.tpl.yaml
@@ -1,8 +1,8 @@
version: 4.0
addr: :10000
-token: token
+token:
database: revsuit.db
-log_level: debug
+log_level: info
http:
ip_header:
@@ -15,6 +15,11 @@ mysql:
enable: true
addr: :3306
version_string: 10.4.13-MariaDB-log
+ftp:
+ enable: true
+ addr: :21
+ pasv_ip: 127.0.0.1 # your public network ip
+ pasv_port: 2020
notice:
dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index abbe6ca..133fc80 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -3,12 +3,12 @@
RevSuit
-
-
-
-
-
-
+
+
+
+
+
+
Logs
@@ -23,6 +23,9 @@
MySQL Logs
+
+ FTP Logs
+
Rules
@@ -38,6 +41,9 @@
MySQL Rules
+
+ FTP Rules
+
diff --git a/frontend/src/api/record.js b/frontend/src/api/record.js
index 00b53bd..c2d7ef8 100644
--- a/frontend/src/api/record.js
+++ b/frontend/src/api/record.js
@@ -42,4 +42,15 @@ export function getRmiRecord(params) {
return status >= 200 && status < 300 // 默认的
}
})
+}
+
+export function getFtpRecord(params) {
+ return request({
+ url: '/record/ftp',
+ params: params,
+ method: 'get',
+ validateStatus: function (status) {
+ return status >= 200 && status < 300 // 默认的
+ }
+ })
}
\ No newline at end of file
diff --git a/frontend/src/api/rule.js b/frontend/src/api/rule.js
index d6933ff..a4bb4aa 100644
--- a/frontend/src/api/rule.js
+++ b/frontend/src/api/rule.js
@@ -131,4 +131,37 @@ export function deleteRmiRule(data) {
return status >= 200 && status < 300 // 默认的
}
})
+}
+
+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 // 默认的
+ }
+ })
}
\ No newline at end of file
diff --git a/frontend/src/components/Auth.vue b/frontend/src/components/Auth.vue
index ce3d4a6..73e267f 100644
--- a/frontend/src/components/Auth.vue
+++ b/frontend/src/components/Auth.vue
@@ -7,7 +7,7 @@
@cancel="cancel"
@ok="auth"
>
-
+
diff --git a/frontend/src/components/BasicRule.vue b/frontend/src/components/BasicRule.vue
index c433c02..7180933 100644
--- a/frontend/src/components/BasicRule.vue
+++ b/frontend/src/components/BasicRule.vue
@@ -15,10 +15,13 @@
Flag Format
-
+ 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.">
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index 4474505..9fde7c2 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -13,42 +13,52 @@ const routes = [
{
path: '/logs/http',
name: 'HttpLogs',
- component: () => import(/* webpackChunkName: "about" */ '../views/logs/Http')
+ component: () => import( '../views/logs/Http')
},
{
path: '/logs/dns',
name: 'DnsLogs',
- component: () => import(/* webpackChunkName: "about" */ '../views/logs/Dns')
+ component: () => import( '../views/logs/Dns')
},
{
path: '/logs/mysql',
name: 'MysqlLogs',
- component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql')
+ component: () => import( '../views/logs/Mysql')
},
{
path: '/logs/rmi',
name: 'RmiLogs',
- component: () => import(/* webpackChunkName: "about" */ '../views/logs/Rmi')
+ component: () => import( '../views/logs/Rmi')
+ },
+ {
+ path: '/logs/ftp',
+ name: 'FtpLogs',
+ component: () => import( '../views/logs/Ftp')
},
{
path: '/rules/http',
name: 'HttpRules',
- component: () => import(/* webpackChunkName: "about" */ '../views/rules/Http')
+ component: () => import( '../views/rules/Http')
},
{
path: '/rules/dns',
name: 'DnsRules',
- component: () => import(/* webpackChunkName: "about" */ '../views/rules/Dns')
+ component: () => import( '../views/rules/Dns')
},
{
path: '/rules/mysql',
name: 'MysqlRules',
- component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql')
+ component: () => import( '../views/rules/Mysql')
},
{
path: '/rules/rmi',
name: 'RmiRules',
- component: () => import(/* webpackChunkName: "about" */ '../views/rules/Rmi')
+ component: () => import( '../views/rules/Rmi')
+ },
+ {
+ path: '/rules/ftp',
+ name: 'FtpRules',
+ component: () => import( '../views/rules/Ftp')
}
]
diff --git a/frontend/src/views/logs/Ftp.vue b/frontend/src/views/logs/Ftp.vue
new file mode 100644
index 0000000..16bea1a
--- /dev/null
+++ b/frontend/src/views/logs/Ftp.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
{e.target.checked?filters[column.dataIndex] = 'CRASHED':filters[column.dataIndex] = '';fetch()}">
+ CRASHED
+
+
+
{e.target.checked?filters[column.dataIndex] = 'FINISHED':filters[column.dataIndex] = '';fetch()}">
+ FINISHED
+
+
+
+
setSelectedKeys(e.target.value ? [e.target.value] : [])"
+ @pressEnter="() => {
+ filters[column.dataIndex] = selectedKeys[0];
+ fetch()
+ }"
+ />
+ {
+ filters[column.dataIndex] = selectedKeys[0];
+ fetch()
+ }"
+ >
+ Search
+
+ {
+ clearFilters();
+ delete filters[column.dataIndex];
+ fetch()
+ }">
+ Reset
+
+
+
+
+
+ {{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
+
+
+
+ {{ status }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/logs/Http.vue b/frontend/src/views/logs/Http.vue
index 74328a9..17d2a87 100644
--- a/frontend/src/views/logs/Http.vue
+++ b/frontend/src/views/logs/Http.vue
@@ -77,12 +77,12 @@ import {getHttpRecord} from '@/api/record'
import {store} from '@/main'
const colors = {
- "GET": "green",
- "POST": "red",
- "HEAD": "pink",
- "PUT": "geekblue",
- "OPTIONS": "cyan",
- "DELETE": "purple",
+ "GET": "#52c41a",
+ "POST": "#f5222d",
+ "PUT": "#eb2f96",
+ "HEAD": "#02a7ff",
+ "OPTIONS": "#13c2c2",
+ "DELETE": "#722ed1",
}
const columns = [
diff --git a/frontend/src/views/logs/Mysql.vue b/frontend/src/views/logs/Mysql.vue
index 7a9bba3..16aa894 100644
--- a/frontend/src/views/logs/Mysql.vue
+++ b/frontend/src/views/logs/Mysql.vue
@@ -9,7 +9,23 @@
>
+
+
{e.target.checked?filters[column.dataIndex] = 'true':filters[column.dataIndex] = '';fetch()}">
+ True
+
+
+
{e.target.checked?filters[column.dataIndex] = 'false':filters[column.dataIndex] = '';fetch()}">
+ False
+
TrueFalse
+ color="#eb2f96"
+ >TrueFalse
{{ files.length }}
{{ files.length }}
@@ -82,9 +98,9 @@ import {getMysqlRecord} from '@/api/record'
import {store} from '@/main'
const colors = [
- "geekblue",
- "blue",
- "pink",
+ "#13c2c2",
+ "#52c41a",
+ "#02a7ff",
]
const columns = [
@@ -133,6 +149,8 @@ const columns = [
dataIndex: 'load_local_data',
key: 'load_local_data',
scopedSlots: {
+ filterDropdown: 'selectDropdown',
+ filterIcon: 'filterIcon',
customRender: "loadData",
}
},
diff --git a/frontend/src/views/rules/Dns.vue b/frontend/src/views/rules/Dns.vue
index 765a942..6821fca 100644
--- a/frontend/src/views/rules/Dns.vue
+++ b/frontend/src/views/rules/Dns.vue
@@ -6,7 +6,7 @@
-
+
+
+ Value
+
+
+
+
{{ value }}
-
-
-
-
-
-
-
+ View
Edit
+
+
+
+ New Rule
+
+
+
+
+
+
+
+
+
+ Pasv Address
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Submit
+
+
+
+
+
+
+
setSelectedKeys(e.target.value ? [e.target.value] : [])"
+ @pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
+ />
+ {filters[column.dataIndex] = selectedKeys[0];fetch()}"
+ >
+ Search
+
+
+
+
+
+ {{ rank }}
+
+
+
+
+
+
+
+ {{ value }}
+
+
+ View
+ Edit
+
+ Delete
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/rules/Http.vue b/frontend/src/views/rules/Http.vue
index 778259d..c20fa40 100644
--- a/frontend/src/views/rules/Http.vue
+++ b/frontend/src/views/rules/Http.vue
@@ -21,7 +21,7 @@
Response Status Code
+ title="Number between 100-600, or template such as ${query.varname}/${body.varname}/${header.varname}">
@@ -39,7 +39,7 @@
Response Headers
+ title="Support template such as ${query.varname}/${body.varname}/${header.varname}">
@@ -77,13 +77,14 @@
Response Body
+ title="Support template such as ${query.varname}/${body.varname}/${header.varname}">
@@ -161,17 +162,17 @@
View
Edit
View
Edit
-
-
- {{ resolveTypes[type] }}
-
-
@@ -94,19 +87,19 @@
{{ value }}
-
-
-
-
-
-
-
+ View
Edit
1 {
+ if len(matched) > 1 && len(groupNames) == 0 {
flagGroup = matched[1]
}
- return flag, flagGroup
+
+ for j, name := range groupNames {
+ if j != 0 && name != "" {
+ vars[name] = strings.TrimSpace(matched[j])
+ }
+ }
+
+ return flag, flagGroup, vars
}
diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go
index 54f0a11..ec2eeed 100644
--- a/pkg/dns/dns.go
+++ b/pkg/dns/dns.go
@@ -8,6 +8,7 @@ import (
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/newdns"
"github.com/li4n0/revsuit/internal/qqwry"
+ "github.com/li4n0/revsuit/internal/rule"
"github.com/patrickmn/go-cache"
log "unknwon.dev/clog/v2"
)
@@ -72,7 +73,7 @@ func (s *Server) Run() {
ip := strings.Split(remoteAddr, ":")[0]
for _, _rule := range s.getRules() {
- flag, flagGroup := _rule.Match(domain)
+ flag, flagGroup, vars := _rule.Match(domain)
if flag == "" {
continue
}
@@ -82,7 +83,7 @@ func (s *Server) Run() {
log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return nil, nil
}
- log.Info("DNS record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
+ log.Info("DNS record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
@@ -91,11 +92,11 @@ func (s *Server) Run() {
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
- log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("DNS record[id%d] has been put to client message queue", r.ID)
}
} else {
r.PushToClient()
- log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("DNS record[id%d] has been put to client message queue", r.ID)
}
}
@@ -103,12 +104,12 @@ func (s *Server) Run() {
if _rule.Notice {
go func() {
r.Notice()
- log.Trace("DNS record(id:%d) notice has been sent", r.ID)
+ log.Trace("DNS record[id%d] notice has been sent", r.ID)
}()
}
if _rule.Value != "" {
-
+ value := rule.CompileTpl(_rule.Value, vars)
_type := _rule.Type
if _rule.Type == newdns.REBINDING {
_type = newdns.A
@@ -121,16 +122,16 @@ func (s *Server) Run() {
Records: func() []newdns.Record {
switch _rule.Type {
case newdns.TXT:
- return []newdns.Record{{Data: []string{_rule.Value}}}
+ return []newdns.Record{{Data: []string{value}}}
case newdns.CNAME, newdns.NS:
- return []newdns.Record{{Address: _rule.Value + "."}}
+ return []newdns.Record{{Address: value + "."}}
case newdns.REBINDING:
// Get rebinding ip list
values, ok := rebindingCache.Get(ip)
if !ok {
- rebindingCache.Set(ip, strings.Split(_rule.Value, ","), cache.DefaultExpiration)
- values = strings.Split(_rule.Value, ",")
+ rebindingCache.Set(ip, strings.Split(value, ","), cache.DefaultExpiration)
+ values = strings.Split(value, ",")
}
//Choose and delete first ip
@@ -144,7 +145,7 @@ func (s *Server) Run() {
log.Trace("DNS rebinding client(ip:%v) to %v", ip, value)
return []newdns.Record{{Address: value}}
default:
- return []newdns.Record{{Address: _rule.Value}}
+ return []newdns.Record{{Address: value}}
}
}(),
TTL: _rule.TTL * time.Second,
diff --git a/pkg/dns/rule.go b/pkg/dns/rule.go
index 6cdc614..f4e274c 100644
--- a/pkg/dns/rule.go
+++ b/pkg/dns/rule.go
@@ -157,9 +157,9 @@ func UpsertRules(c *gin.Context) {
}
if update {
- log.Trace("DNS rule(id:%d) has been updated", dnsRule.ID)
+ log.Trace("DNS rule[id%d] has been updated", dnsRule.ID)
} else {
- log.Trace("DNS rule(id:%d) has been created", dnsRule.ID)
+ log.Trace("DNS rule[id%d] has been created", dnsRule.ID)
}
c.JSON(200, gin.H{
@@ -191,7 +191,7 @@ func DeleteRules(c *gin.Context) {
return
}
- log.Trace("DNS rule(id:%d) has been deleted", dnsRule.ID)
+ log.Trace("DNS rule[id%d] has been deleted", dnsRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
diff --git a/pkg/ftp/config.go b/pkg/ftp/config.go
new file mode 100644
index 0000000..f505ec7
--- /dev/null
+++ b/pkg/ftp/config.go
@@ -0,0 +1,8 @@
+package ftp
+
+type Config struct {
+ Enable bool
+ Addr string
+ PasvIP string `yaml:"pasv_ip"`
+ PasvPort int `yaml:"pasv_port"`
+}
diff --git a/pkg/ftp/ftp.go b/pkg/ftp/ftp.go
new file mode 100644
index 0000000..f07236a
--- /dev/null
+++ b/pkg/ftp/ftp.go
@@ -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)
+ }
+
+}
diff --git a/pkg/ftp/record.go b/pkg/ftp/record.go
new file mode 100644
index 0000000..c18a98b
--- /dev/null
+++ b/pkg/ftp/record.go
@@ -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},
+ })
+}
diff --git a/pkg/ftp/rule.go b/pkg/ftp/rule.go
new file mode 100644
index 0000000..7f2c483
--- /dev/null
+++ b/pkg/ftp/rule.go
@@ -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,
+ })
+}
diff --git a/pkg/mysql/mysql.go b/pkg/mysql/mysql.go
index 58f4108..95ac8b0 100644
--- a/pkg/mysql/mysql.go
+++ b/pkg/mysql/mysql.go
@@ -65,8 +65,9 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
)
for _, _rule := range s.getRules() {
- flag, _ := _rule.Match(user + schema)
- if flag == "" {
+ userFlag, _, _ := _rule.Match(user)
+ schemaFlag, _, _ := _rule.Match(schema)
+ if userFlag == "" && schemaFlag == "" {
continue
}
s.connRulePool.Store(c.ConnectionID, _rule)
@@ -81,8 +82,6 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
if strings.Contains(c.ConnAttrs["_client_name"], "MySQL Connector") {
c.IsJdbcClient = true
c.SupportLoadDataLocal = true
- // 测试发现只有 pymysql 和原生命令行会对这个 flag 真正进行修改
- // 而且 Connector/J 默认值为 False, 所以这里做特殊兼容
}
}
}
@@ -90,21 +89,30 @@ func (s *Server) NewConnection(c *vmysql.Conn) {
// ConnectionClosed is part of the mysql.Handler interface.
func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Trace("MySQL Client leaved, ID [%d]", c.ConnectionID)
+
var (
- user = c.User
- clientName string
- clientOS string
- supportLoadLocalData = c.SupportLoadDataLocal
- cr, ok = s.connRulePool.Load(c.ConnectionID)
+ user = c.User
+ schema = c.SchemaName
+ supportLoadLocalData = c.SupportLoadDataLocal
+ cr, ok = s.connRulePool.Load(c.ConnectionID)
+ clientName, clientOS, flag, flagGroup string
)
+
if !ok {
return
}
+
_rule := cr.(*Rule)
- flag, flagGroup := _rule.Match(user)
+ for _, s := range []string{user, schema} {
+ flag, flagGroup, _ = _rule.Match(s)
+ if flag != "" {
+ break
+ }
+ }
if flag == "" {
log.Error("MySQL Connection rule(%d) not match flag", c.ConnectionID)
}
+
if c.ConnAttrs != nil {
clientName = c.ConnAttrs["_client_name"] + " " + c.ConnAttrs["_client_version"]
clientOS = c.ConnAttrs["_os"] + " " + c.ConnAttrs["_platform"]
@@ -125,7 +133,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
log.Error("MySQL record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
return
}
- log.Info("MySQL record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
+ log.Info("MySQL record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
@@ -134,11 +142,11 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
- log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("MySQL record[id%d] has been put to client message queue", r.ID)
}
} else {
r.PushToClient()
- log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("MySQL record[id%d] has been put to client message queue", r.ID)
}
}
@@ -146,7 +154,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
if _rule.Notice {
go func() {
r.Notice()
- log.Trace("MySQL record(id:%d) notice has been sent", r.ID)
+ log.Trace("MySQL record[id%d] notice has been sent", r.ID)
}()
}
diff --git a/pkg/mysql/record.go b/pkg/mysql/record.go
index b70511b..a697a09 100644
--- a/pkg/mysql/record.go
+++ b/pkg/mysql/record.go
@@ -81,6 +81,13 @@ func ListRecords(c *gin.Context) {
if mysqlRecord.ClientName != "" {
db.Where("client_name like ?", "%"+mysqlRecord.ClientName)
}
+ if c.Query("load_local_data") != "" {
+ if c.Query("load_local_data") == "true" {
+ db.Where("load_local_data = ?", true)
+ } else {
+ db.Where("load_local_data = ?", false)
+ }
+ }
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
diff --git a/pkg/mysql/rule.go b/pkg/mysql/rule.go
index 95eac1d..614c0d0 100644
--- a/pkg/mysql/rule.go
+++ b/pkg/mysql/rule.go
@@ -140,9 +140,9 @@ func UpsertRules(c *gin.Context) {
}
if update {
- log.Trace("MySQL rule(id:%d) has been updated", mysqlRule.ID)
+ log.Trace("MySQL rule[id%d] has been updated", mysqlRule.ID)
} else {
- log.Trace("MySQL rule(id:%d) has been created", mysqlRule.ID)
+ log.Trace("MySQL rule[id%d] has been created", mysqlRule.ID)
}
c.JSON(200, gin.H{
@@ -174,7 +174,7 @@ func DeleteRules(c *gin.Context) {
return
}
- log.Trace("MySQL rule(id:%d) has been deleted", mysqlRule.ID)
+ log.Trace("MySQL rule[id%d] has been deleted", mysqlRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
diff --git a/pkg/rhttp/http.go b/pkg/rhttp/http.go
index a27b711..9981b0e 100644
--- a/pkg/rhttp/http.go
+++ b/pkg/rhttp/http.go
@@ -106,26 +106,31 @@ func getRawRequest(r *http.Request) ([]byte, error) {
return httputil.DumpRequestOut(r, true)
}
-func compileTpl(c *gin.Context, tpl string) (compiled string) {
+func compileTpl(c *gin.Context, tpl string, vars map[string]string) (compiled string) {
compiled = tpl
- if queryVarMatcher.FindString(tpl) != "" {
- compiled = queryVarMatcher.ReplaceAllString(compiled, c.Query(queryVarMatcher.FindStringSubmatch(tpl)[1]))
+ for _, submatch := range queryVarMatcher.FindAllStringSubmatch(tpl, -1) {
+ compiled = strings.ReplaceAll(compiled, submatch[0], c.Query(submatch[1]))
}
- if bodyVarMatcher.FindString(tpl) != "" {
- compiled = bodyVarMatcher.ReplaceAllString(compiled, c.PostForm(bodyVarMatcher.FindStringSubmatch(tpl)[1]))
+ for _, submatch := range bodyVarMatcher.FindAllStringSubmatch(tpl, -1) {
+ compiled = strings.ReplaceAll(compiled, submatch[0], c.PostForm(submatch[1]))
}
- if headerVarMatcher.FindString(tpl) != "" {
- compiled = headerVarMatcher.ReplaceAllString(compiled, c.GetHeader(headerVarMatcher.FindStringSubmatch(tpl)[1]))
+ for _, submatch := range headerVarMatcher.FindAllStringSubmatch(tpl, -1) {
+ compiled = strings.ReplaceAll(compiled, submatch[0], c.GetHeader(submatch[1]))
}
+
+ for n, v := range vars {
+ compiled = strings.ReplaceAll(compiled, "${"+n+"}", v)
+ }
+
return compiled
}
func (s *Server) Receive(c *gin.Context) {
u := c.Request.URL.String()
for _, _rule := range s.getRules() {
- flag, flagGroup := _rule.Match(u)
+ flag, flagGroup, vars := _rule.Match(u)
if flag == "" {
continue
}
@@ -148,16 +153,16 @@ func (s *Server) Receive(c *gin.Context) {
// create new record
r, err := NewRecord(_rule, flag, c.Request.Method, u, ip, area, string(raw))
if err != nil {
- log.Error("HTTP record(rule_id:%d) created failed :%s", _rule.ID, err.Error())
- code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode))
+ log.Error("HTTP record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
+ code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode, vars))
if err != nil || code < 100 || code > 600 {
code = 400
}
- c.String(code, compileTpl(c, _rule.ResponseBody))
+ c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return
}
- log.Info("HTTP record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
+ log.Info("HTTP record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
@@ -166,31 +171,31 @@ func (s *Server) Receive(c *gin.Context) {
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
- log.Trace("HTTP record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("HTTP record[id%d] has been put to client message queue", r.ID)
}
}
r.PushToClient()
- log.Trace("HTTP record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("HTTP record[id%d] has been put to client message queue", r.ID)
}
//send notice
if _rule.Notice {
go func() {
r.Notice()
- log.Trace("HTTP record(id:%d) notice has been sent", r.ID)
+ log.Trace("HTTP record[id%d] notice has been sent", r.ID)
}()
}
for header, value := range _rule.ResponseHeaders {
- c.Header(compileTpl(c, header), compileTpl(c, value))
+ c.Header(compileTpl(c, header, vars), compileTpl(c, value, vars))
}
- code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode))
+ code, err := strconv.Atoi(compileTpl(c, _rule.ResponseStatusCode, vars))
if err != nil || code < 100 || code > 600 {
code = 400
}
- c.String(code, compileTpl(c, _rule.ResponseBody))
+ c.String(code, compileTpl(c, _rule.ResponseBody, vars))
return
}
diff --git a/pkg/rhttp/rule.go b/pkg/rhttp/rule.go
index cc48ef4..61736c7 100644
--- a/pkg/rhttp/rule.go
+++ b/pkg/rhttp/rule.go
@@ -158,9 +158,9 @@ func UpsertRules(c *gin.Context) {
}
if update {
- log.Trace("HTTP rule(id:%d) has been updated", httpRule.ID)
+ log.Trace("HTTP rule[id%d] has been updated", httpRule.ID)
} else {
- log.Trace("HTTP rule(id:%d) has been created", httpRule.ID)
+ log.Trace("HTTP rule[id%d] has been created", httpRule.ID)
}
c.JSON(200, gin.H{
@@ -192,7 +192,7 @@ func DeleteRules(c *gin.Context) {
return
}
- log.Trace("HTTP rule(id:%d) has been deleted", httpRule.ID)
+ log.Trace("HTTP rule[id%d] has been deleted", httpRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
diff --git a/pkg/rmi/rmi.go b/pkg/rmi/rmi.go
index 3fbdb58..8cb0955 100644
--- a/pkg/rmi/rmi.go
+++ b/pkg/rmi/rmi.go
@@ -49,11 +49,12 @@ func (s *Server) updateRules() error {
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
- ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
if err := conn.SetDeadline(time.Now().Add(time.Second * 30)); err != nil {
log.Error("RMI set connection deadline error:%v", err.Error())
}
+ ip, port, _ := net.SplitHostPort(conn.RemoteAddr().String())
+
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
@@ -94,7 +95,7 @@ func (s *Server) handleConnection(conn net.Conn) {
path := strings.TrimRight(string(frags[len(frags)-1][2:]), "\x00")
for _, _rule := range s.getRules() {
- flag, flagGroup := _rule.Match(path)
+ flag, flagGroup, _ := _rule.Match(path)
if flag == "" {
continue
}
@@ -104,10 +105,10 @@ func (s *Server) handleConnection(conn net.Conn) {
// create new record
r, err := NewRecord(_rule, flag, path, ip, area)
if err != nil {
- log.Error("RMI record(rule_id:%d) created failed :%s", _rule.ID, err.Error())
+ log.Error("RMI record[rule_id:%d] created failed :%s", _rule.ID, err.Error())
return
}
- log.Info("RMI record(id:%d,rule:%s,remote_ip:%s) has been created", r.ID, _rule.Name, ip)
+ log.Info("RMI record[id:%d rule:%s remote_ip:%s] has been created", r.ID, _rule.Name, ip)
//only send to client when this connection recorded first time.
if _rule.PushToClient {
@@ -116,18 +117,18 @@ func (s *Server) handleConnection(conn net.Conn) {
database.DB.Where("rule_name=? and raw like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
if count <= 1 {
r.PushToClient()
- log.Trace("RMI record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("RMI record[id%d] has been put to client message queue", r.ID)
}
}
r.PushToClient()
- log.Trace("RMI record(id:%d) has been put to client message queue", r.ID)
+ log.Trace("RMI record[id%d] has been put to client message queue", r.ID)
}
//send notice
if _rule.Notice {
go func() {
r.Notice()
- log.Trace("RMI record(id:%d) notice has been sent", r.ID)
+ log.Trace("RMI record[id%d] notice has been sent", r.ID)
}()
}
}
diff --git a/pkg/rmi/rule.go b/pkg/rmi/rule.go
index e2bf0f9..6436042 100644
--- a/pkg/rmi/rule.go
+++ b/pkg/rmi/rule.go
@@ -10,7 +10,7 @@ import (
log "unknwon.dev/clog/v2"
)
-// Http rule struct
+// RMI rule struct
type Rule struct {
rule.BaseRule
}
@@ -149,9 +149,9 @@ func UpsertRules(c *gin.Context) {
}
if update {
- log.Trace("RMI rule(id:%d) has been updated", rmiRule.ID)
+ log.Trace("RMI rule[id%d] has been updated", rmiRule.ID)
} else {
- log.Trace("RMI rule(id:%d) has been created", rmiRule.ID)
+ log.Trace("RMI rule[id%d] has been created", rmiRule.ID)
}
c.JSON(200, gin.H{
@@ -183,7 +183,7 @@ func DeleteRules(c *gin.Context) {
return
}
- log.Trace("RMI rule(id:%d) has been deleted", rmiRule.ID)
+ log.Trace("RMI rule[id%d] has been deleted", rmiRule.ID)
c.JSON(200, gin.H{
"status": "succeed",
diff --git a/pkg/server/config.go b/pkg/server/config.go
index 7571a24..3ee0bae 100644
--- a/pkg/server/config.go
+++ b/pkg/server/config.go
@@ -2,6 +2,7 @@ package server
import (
"github.com/li4n0/revsuit/pkg/dns"
+ "github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
@@ -24,4 +25,5 @@ type Config struct {
DNS dns.Config
MySQL mysql.Config
RMI rmi.Config
+ FTP ftp.Config
}
diff --git a/pkg/server/router.go b/pkg/server/router.go
index 6247fcf..a2fbe66 100644
--- a/pkg/server/router.go
+++ b/pkg/server/router.go
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/li4n0/revsuit/frontend"
"github.com/li4n0/revsuit/pkg/dns"
+ "github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql"
"github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
@@ -15,6 +16,11 @@ import (
func (revsuit *Revsuit) registerRouter() {
revsuit.http.Router = gin.Default()
+ if revsuit.logLevel != log.LevelTrace {
+ revsuit.http.Router = gin.New()
+ revsuit.http.Router.Use(gin.Recovery())
+ }
+
revsuit.registerPlatformRouter()
revsuit.registerHttpRouter()
}
@@ -61,6 +67,9 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup := recordGroup.Group("/rmi")
rmiGroup.GET("", rmi.ListRecords)
+ ftpGroup := recordGroup.Group("/ftp")
+ ftpGroup.GET("", ftp.ListRecords)
+
// init rule router group
ruleGroup := revsuit.http.ApiGroup.Group("/rule")
@@ -84,6 +93,11 @@ func (revsuit *Revsuit) registerHttpRouter() {
rmiGroup.POST("", rmi.UpsertRules)
rmiGroup.DELETE("", rmi.DeleteRules)
+ ftpGroup = ruleGroup.Group("/ftp")
+ ftpGroup.GET("", ftp.ListRules)
+ ftpGroup.POST("", ftp.UpsertRules)
+ ftpGroup.DELETE("", ftp.DeleteRules)
+
// init file router group
fileGroup := revsuit.http.ApiGroup.Group("/file")
fileGroup.GET("/mysql/:id", mysql.GetFile)
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 025b683..1ac6c98 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -5,6 +5,7 @@ import (
"github.com/li4n0/revsuit/internal/database"
"github.com/li4n0/revsuit/internal/notice"
"github.com/li4n0/revsuit/pkg/dns"
+ "github.com/li4n0/revsuit/pkg/ftp"
"github.com/li4n0/revsuit/pkg/mysql"
http "github.com/li4n0/revsuit/pkg/rhttp"
"github.com/li4n0/revsuit/pkg/rmi"
@@ -13,10 +14,13 @@ import (
)
type Revsuit struct {
+ logLevel log.Level
+
http *http.Server
dns *dns.Server
mysql *mysql.Server
rmi *rmi.Server
+ ftp *ftp.Server
}
func initDatabase(dsn string) {
@@ -61,11 +65,18 @@ func initDatabase(dsn string) {
if err != nil {
log.Fatal(err.Error())
}
+ err = database.DB.AutoMigrate(&ftp.Record{})
+ if err != nil {
+ log.Fatal(err.Error())
+ }
+ err = database.DB.AutoMigrate(&ftp.Rule{})
+ if err != nil {
+ log.Fatal(err.Error())
+ }
}
-func initLog(level string) {
- var logLevel log.Level
+func initLog(level string) (logLevel log.Level) {
switch level {
case "debug":
@@ -93,7 +104,7 @@ func initLog(level string) {
log.ConsoleConfig{
Level: logLevel,
})
-
+ return logLevel
}
func initNotice(nc noticeConfig) {
@@ -123,11 +134,12 @@ func initNotice(nc noticeConfig) {
func New(c *Config) *Revsuit {
initDatabase(c.Database)
- initLog(c.LogLevel)
+ logLevel := initLog(c.LogLevel)
initNotice(c.Notice)
s := &Revsuit{
- http: http.GetServer(),
+ logLevel: logLevel,
+ http: http.GetServer(),
}
if c.DNS.Enable {
s.dns = dns.GetServer()
@@ -140,6 +152,10 @@ func New(c *Config) *Revsuit {
s.rmi = rmi.GetServer()
s.rmi.Config = c.RMI
}
+ if c.FTP.Enable {
+ s.ftp = ftp.GetServer()
+ s.ftp.Config = c.FTP
+ }
if c.Addr != "" {
s.http.SetAddr(c.Addr)
@@ -166,6 +182,9 @@ func (revsuit *Revsuit) Run() {
if revsuit.rmi != nil {
go revsuit.rmi.Run()
}
+ if revsuit.ftp != nil {
+ go revsuit.ftp.Run()
+ }
revsuit.http.Run()
}