mirror of
https://github.com/bjdgyc/anylink.git
synced 2025-08-08 06:32:04 +08:00
新增用户活动日志
This commit is contained in:
20
server/cron/clear_user_act_log.go
Normal file
20
server/cron/clear_user_act_log.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"github.com/bjdgyc/anylink/base"
|
||||
"github.com/bjdgyc/anylink/dbdata"
|
||||
)
|
||||
|
||||
// 清除用户活动日志
|
||||
func ClearUserActLog() {
|
||||
lifeDay, timesUp := isClearTime()
|
||||
if !timesUp {
|
||||
return
|
||||
}
|
||||
// 当审计日志永久保存时,则退出
|
||||
if lifeDay <= 0 {
|
||||
return
|
||||
}
|
||||
affected, err := dbdata.UserActLogIns.ClearUserActLog(getTimeAgo(lifeDay))
|
||||
base.Info("Cron ClearUserActLog: ", affected, err)
|
||||
}
|
@@ -11,6 +11,7 @@ func Start() {
|
||||
s := gocron.NewScheduler(time.Local)
|
||||
s.Cron("0 * * * *").Do(ClearAudit)
|
||||
s.Cron("0 * * * *").Do(ClearStatsInfo)
|
||||
s.Cron("0 * * * *").Do(ClearUserActLog)
|
||||
s.Every(1).Day().At("00:00").Do(sessdata.CloseUserLimittimeSession)
|
||||
s.StartAsync()
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ type SearchCon struct {
|
||||
AccessProto string `json:"access_proto"`
|
||||
Date []string `json:"date"`
|
||||
Info string `json:"info"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
func GetAuditSession(search string) *xorm.Session {
|
||||
@@ -47,6 +48,11 @@ func GetAuditSession(search string) *xorm.Session {
|
||||
if searchData.Info != "" {
|
||||
session.And("info LIKE ?", "%"+searchData.Info+"%")
|
||||
}
|
||||
if searchData.Sort == 1 {
|
||||
session.OrderBy("id desc")
|
||||
} else {
|
||||
session.OrderBy("id asc")
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
|
@@ -33,7 +33,7 @@ func initDb() {
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{})
|
||||
err = xdb.Sync2(&User{}, &Setting{}, &Group{}, &IpMap{}, &AccessAudit{}, &Policy{}, &StatsNetwork{}, &StatsCpu{}, &StatsMem{}, &StatsOnline{}, &UserActLog{})
|
||||
if err != nil {
|
||||
base.Fatal(err)
|
||||
}
|
||||
|
@@ -40,6 +40,20 @@ type User struct {
|
||||
UpdatedAt time.Time `json:"updated_at" xorm:"DateTime updated"`
|
||||
}
|
||||
|
||||
type UserActLog struct {
|
||||
Id int `json:"id" xorm:"pk autoincr not null"`
|
||||
Username string `json:"username" xorm:"varchar(60)"`
|
||||
GroupName string `json:"group_name" xorm:"varchar(60)"`
|
||||
IpAddr string `json:"ip_addr" xorm:"varchar(32)"`
|
||||
RemoteAddr string `json:"remote_addr" xorm:"varchar(32)"`
|
||||
Os uint8 `json:"os" xorm:"not null default 0 Int"`
|
||||
Client uint8 `json:"client" xorm:"not null default 0 Int"`
|
||||
Version string `json:"version" xorm:"varchar(15)"`
|
||||
Status uint8 `json:"status" xorm:"not null default 0 Int"`
|
||||
Info string `json:"info" xorm:"varchar(255) not null default ''"` // 详情
|
||||
CreatedAt time.Time `json:"created_at" xorm:"DateTime created"`
|
||||
}
|
||||
|
||||
type IpMap struct {
|
||||
Id int `json:"id" xorm:"pk autoincr not null"`
|
||||
IpAddr string `json:"ip_addr" xorm:"varchar(32) not null unique"`
|
||||
|
170
server/dbdata/user_act_log.go
Normal file
170
server/dbdata/user_act_log.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package dbdata
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ivpusic/grpool"
|
||||
"github.com/spf13/cast"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const (
|
||||
UserAuthFail = 0 // 认证失败
|
||||
UserAuthSuccess = 1 // 认证成功
|
||||
UserConnected = 2 // 连线成功
|
||||
UserLogout = 3 // 用户登出
|
||||
)
|
||||
|
||||
type UserActLogProcess struct {
|
||||
Pool *grpool.Pool
|
||||
StatusOps []string
|
||||
OsOps []string
|
||||
ClientOps []string
|
||||
InfoOps []string
|
||||
}
|
||||
|
||||
var (
|
||||
UserActLogIns = &UserActLogProcess{
|
||||
Pool: grpool.NewPool(1, 100),
|
||||
StatusOps: []string{ // 操作类型
|
||||
UserAuthFail: "认证失败",
|
||||
UserAuthSuccess: "认证成功",
|
||||
UserConnected: "连接成功",
|
||||
UserLogout: "用户登出",
|
||||
},
|
||||
OsOps: []string{ // 操作系统
|
||||
0: "Windows",
|
||||
1: "macOS",
|
||||
2: "Linux",
|
||||
3: "Android",
|
||||
4: "iOS",
|
||||
},
|
||||
ClientOps: []string{ // 客户端
|
||||
0: "AnyConnect",
|
||||
1: "OpenConnect",
|
||||
2: "unknown",
|
||||
},
|
||||
InfoOps: []string{ // 信息
|
||||
0: "用户掉线",
|
||||
1: "用户/客户端主动断开",
|
||||
2: "用户被踢下线(管理员/账号过期)",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// 异步写入用户操作日志
|
||||
func (ua *UserActLogProcess) Add(u UserActLog, userAgent string) {
|
||||
os_idx, client_idx, ver := ua.ParseUserAgent(userAgent)
|
||||
u.Os = os_idx
|
||||
u.Client = client_idx
|
||||
u.Version = ver
|
||||
u.RemoteAddr = strings.Split(u.RemoteAddr, ":")[0]
|
||||
// remove extra characters
|
||||
infoSlice := strings.Split(u.Info, " ")
|
||||
infoLen := len(infoSlice)
|
||||
if infoLen > 1 {
|
||||
if u.Username == infoSlice[0] {
|
||||
u.Info = strings.Join(infoSlice[1:], " ")
|
||||
}
|
||||
// delete - char
|
||||
if infoLen > 2 && infoSlice[1] == "-" {
|
||||
u.Info = u.Info[2:]
|
||||
}
|
||||
}
|
||||
UserActLogIns.Pool.JobQueue <- func() {
|
||||
_ = Add(u)
|
||||
}
|
||||
}
|
||||
|
||||
// 转义操作类型, 方便vue显示
|
||||
func (ua *UserActLogProcess) GetStatusOpsWithTag() interface{} {
|
||||
type StatusTag struct {
|
||||
Key int `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
var res []StatusTag
|
||||
for k, v := range ua.StatusOps {
|
||||
tag := "info"
|
||||
switch k {
|
||||
case UserAuthFail:
|
||||
tag = "danger"
|
||||
case UserAuthSuccess:
|
||||
tag = "success"
|
||||
case UserConnected:
|
||||
tag = ""
|
||||
}
|
||||
res = append(res, StatusTag{k, v, tag})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (ua *UserActLogProcess) GetInfoOpsById(id uint8) string {
|
||||
infoMap := ua.InfoOps
|
||||
return infoMap[id]
|
||||
}
|
||||
|
||||
func (ua *UserActLogProcess) ParseUserAgent(userAgent string) (os_idx, client_idx uint8, ver string) {
|
||||
// os
|
||||
os_idx = 2
|
||||
if strings.Contains(userAgent, "windows") {
|
||||
os_idx = 0
|
||||
} else if strings.Contains(userAgent, "mac os") || strings.Contains(userAgent, "darwin_i386") {
|
||||
os_idx = 1
|
||||
} else if strings.Contains(userAgent, "darwin_arm") || strings.Contains(userAgent, "apple") {
|
||||
os_idx = 4
|
||||
} else if strings.Contains(userAgent, "android") {
|
||||
os_idx = 3
|
||||
}
|
||||
// client
|
||||
client_idx = 2
|
||||
if strings.Contains(userAgent, "anyconnect") {
|
||||
client_idx = 0
|
||||
} else if strings.Contains(userAgent, "openconnect") {
|
||||
client_idx = 1
|
||||
}
|
||||
// ver
|
||||
uaSlice := strings.Split(userAgent, " ")
|
||||
ver = uaSlice[len(uaSlice)-1]
|
||||
if ver[0] == 'v' {
|
||||
ver = ver[1:]
|
||||
}
|
||||
if !regexp.MustCompile(`^(\d+\.?)+$`).MatchString(ver) {
|
||||
ver = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 清除用户操作日志
|
||||
func (ua *UserActLogProcess) ClearUserActLog(ts string) (int64, error) {
|
||||
affected, err := xdb.Where("created_at < '" + ts + "'").Delete(&UserActLog{})
|
||||
return affected, err
|
||||
}
|
||||
|
||||
// 后台筛选用户操作日志
|
||||
func (ua *UserActLogProcess) GetSession(values url.Values) *xorm.Session {
|
||||
session := xdb.Where("1=1")
|
||||
if values.Get("username") != "" {
|
||||
session.And("username = ?", values.Get("username"))
|
||||
}
|
||||
if values.Get("sdate") != "" {
|
||||
session.And("created_at >= ?", values.Get("sdate")+" 00:00:00'")
|
||||
}
|
||||
if values.Get("edate") != "" {
|
||||
session.And("created_at <= ?", values.Get("edate")+" 23:59:59'")
|
||||
}
|
||||
if values.Get("status") != "" {
|
||||
session.And("status = ?", cast.ToUint8(values.Get("status"))-1)
|
||||
}
|
||||
if values.Get("os") != "" {
|
||||
session.And("os = ?", cast.ToUint8(values.Get("os"))-1)
|
||||
}
|
||||
if values.Get("sort") == "1" {
|
||||
session.OrderBy("id desc")
|
||||
} else {
|
||||
session.OrderBy("id asc")
|
||||
}
|
||||
return session
|
||||
}
|
72
server/dbdata/user_act_log_test.go
Normal file
72
server/dbdata/user_act_log_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package dbdata
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseUserAgent(t *testing.T) {
|
||||
type args struct {
|
||||
userAgent string
|
||||
}
|
||||
type res struct {
|
||||
os_idx uint8
|
||||
client_idx uint8
|
||||
ver string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want res
|
||||
}{
|
||||
{
|
||||
name: "mac os 1",
|
||||
args: args{userAgent: "cisco anyconnect vpn agent for mac os x 4.10.05085"},
|
||||
want: res{os_idx: 1, client_idx: 0, ver: "4.10.05085"},
|
||||
},
|
||||
{
|
||||
name: "mac os 2",
|
||||
args: args{userAgent: "anyconnect darwin_i386 4.10.05085"},
|
||||
want: res{os_idx: 1, client_idx: 0, ver: "4.10.05085"},
|
||||
},
|
||||
{
|
||||
name: "windows",
|
||||
args: args{userAgent: "cisco anyconnect vpn agent for windows 4.8.02042"},
|
||||
want: res{os_idx: 0, client_idx: 0, ver: "4.8.02042"},
|
||||
},
|
||||
{
|
||||
name: "iPad",
|
||||
args: args{userAgent: "anyconnect applesslvpn_darwin_arm (ipad) 4.10.04060"},
|
||||
want: res{os_idx: 4, client_idx: 0, ver: "4.10.04060"},
|
||||
},
|
||||
{
|
||||
name: "iPhone",
|
||||
args: args{userAgent: "cisco anyconnect vpn agent for apple iphone 4.10.04060"},
|
||||
want: res{os_idx: 4, client_idx: 0, ver: "4.10.04060"},
|
||||
},
|
||||
{
|
||||
name: "android",
|
||||
args: args{userAgent: "anyconnect android 4.10.05096"},
|
||||
want: res{os_idx: 3, client_idx: 0, ver: "4.10.05096"},
|
||||
},
|
||||
{
|
||||
name: "linux",
|
||||
args: args{userAgent: "open anyconnect vpn agent v7.08"},
|
||||
want: res{os_idx: 2, client_idx: 0, ver: "7.08"},
|
||||
},
|
||||
{
|
||||
name: "openconnect",
|
||||
args: args{userAgent: "openconnect-gui 1.5.3 v7.08"},
|
||||
want: res{os_idx: 2, client_idx: 1, ver: "7.08"},
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
args: args{userAgent: "unknown 1.4.3 aabcd"},
|
||||
want: res{os_idx: 2, client_idx: 2, ver: ""},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if os_idx, client_idx, ver := UserActLogIns.ParseUserAgent(tt.args.userAgent); os_idx != tt.want.os_idx || client_idx != tt.want.client_idx || ver != tt.want.ver {
|
||||
t.Errorf("ParseUserAgent() = %v, %v, %v, want %v, %v, %v", os_idx, client_idx, ver, tt.want.os_idx, tt.want.client_idx, tt.want.ver)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -66,16 +66,27 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 用户活动日志
|
||||
ua := dbdata.UserActLog{
|
||||
Username: cr.Auth.Username,
|
||||
GroupName: cr.GroupSelect,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Status: dbdata.UserAuthSuccess,
|
||||
}
|
||||
// TODO 用户密码校验
|
||||
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
|
||||
if err != nil {
|
||||
base.Warn(err)
|
||||
ua.Info = err.Error()
|
||||
ua.Status = dbdata.UserAuthFail
|
||||
dbdata.UserActLogIns.Add(ua, userAgent)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames(), Error: "用户名或密码错误"}
|
||||
tplRequest(tpl_request, w, data)
|
||||
return
|
||||
}
|
||||
dbdata.UserActLogIns.Add(ua, userAgent)
|
||||
// if !ok {
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// data := RequestData{Group: cr.GroupSelect, Groups: base.Cfg.UserGroups, Error: "请先激活用户"}
|
||||
@@ -109,7 +120,7 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
|
||||
Banner: other.Banner, ProfileHash: profileHash}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
tplRequest(tpl_complete, w, rd)
|
||||
base.Debug("login", cr.Auth.Username)
|
||||
base.Debug("login", cr.Auth.Username, userAgent)
|
||||
}
|
||||
|
||||
const (
|
||||
|
@@ -55,6 +55,7 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
|
||||
// do nothing
|
||||
// base.Debug("recv keepalive", cSess.IpAddr)
|
||||
case 0x05: // DISCONNECT
|
||||
cSess.UserDisconnect = true
|
||||
base.Debug("DISCONNECT", cSess.IpAddr)
|
||||
return
|
||||
case 0x03: // DPD-REQ
|
||||
|
@@ -57,6 +57,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
|
||||
// do nothing
|
||||
// base.Debug("recv keepalive", cSess.IpAddr)
|
||||
case 0x05: // DISCONNECT
|
||||
cSess.UserDisconnect = true
|
||||
base.Debug("DISCONNECT DTLS", cSess.IpAddr)
|
||||
return
|
||||
case 0x03: // DPD-REQ
|
||||
|
@@ -69,6 +69,7 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
cSess.SetMtu(cstpMtu)
|
||||
cSess.MasterSecret = masterSecret
|
||||
cSess.RemoteAddr = r.RemoteAddr
|
||||
cSess.UserAgent = strings.ToLower(r.UserAgent())
|
||||
cSess.LocalIp = net.ParseIP(localIp)
|
||||
cstpKeepalive := base.Cfg.CstpKeepalive
|
||||
cstpDpd := base.Cfg.CstpDpd
|
||||
@@ -194,6 +195,13 @@ func LinkTunnel(w http.ResponseWriter, r *http.Request) {
|
||||
base.Error(err)
|
||||
return
|
||||
}
|
||||
dbdata.UserActLogIns.Add(dbdata.UserActLog{
|
||||
Username: sess.Username,
|
||||
GroupName: sess.Group,
|
||||
IpAddr: cSess.IpAddr.String(),
|
||||
RemoteAddr: cSess.RemoteAddr,
|
||||
Status: dbdata.UserConnected,
|
||||
}, cSess.UserAgent)
|
||||
|
||||
go LinkCstp(conn, bufRW, cSess)
|
||||
}
|
||||
|
@@ -36,6 +36,9 @@ type ConnSession struct {
|
||||
Mtu int
|
||||
IfName string
|
||||
Client string // 客户端 mobile pc
|
||||
UserAgent string // 客户端信息
|
||||
UserDisconnect bool // 用户/客户端主动登出
|
||||
UserKickout bool // 被踢下线
|
||||
CstpDpd int
|
||||
Group *dbdata.Group
|
||||
Limit *LimitRater
|
||||
@@ -241,6 +244,7 @@ func (cs *ConnSession) Close() {
|
||||
|
||||
ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
|
||||
LimitClient(cs.Username, true)
|
||||
AddUserActLog(cs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -396,6 +400,7 @@ func CloseSess(token string) {
|
||||
}
|
||||
|
||||
delete(sessions, token)
|
||||
sess.CSess.UserKickout = true
|
||||
sess.CSess.Close()
|
||||
}
|
||||
|
||||
@@ -418,3 +423,22 @@ func DelSessByStoken(stoken string) {
|
||||
delete(sessions, token)
|
||||
sessMux.Unlock()
|
||||
}
|
||||
|
||||
func AddUserActLog(cs *ConnSession) {
|
||||
ua := dbdata.UserActLog{
|
||||
Username: cs.Sess.Username,
|
||||
GroupName: cs.Sess.Group,
|
||||
IpAddr: cs.IpAddr.String(),
|
||||
RemoteAddr: cs.RemoteAddr,
|
||||
Status: dbdata.UserLogout,
|
||||
}
|
||||
infoId := uint8(0)
|
||||
switch {
|
||||
case cs.UserDisconnect:
|
||||
infoId = 1
|
||||
case cs.UserKickout:
|
||||
infoId = 2
|
||||
}
|
||||
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(infoId)
|
||||
dbdata.UserActLogIns.Add(ua, cs.UserAgent)
|
||||
}
|
||||
|
Reference in New Issue
Block a user