Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
bjdgyc
2022-11-10 14:57:23 +08:00
19 changed files with 1013 additions and 288 deletions

View File

@@ -51,3 +51,29 @@ func SetAuditExport(w http.ResponseWriter, r *http.Request) {
gocsv.Marshal(datas, w)
}
func UserActLogList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var datas []dbdata.UserActLog
session := dbdata.UserActLogIns.GetSession(r.Form)
count, err := dbdata.FindAndCount(session, &datas, dbdata.PageSize, page)
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": dbdata.PageSize,
"datas": datas,
"statusOps": dbdata.UserActLogIns.GetStatusOpsWithTag(),
"osOps": dbdata.UserActLogIns.OsOps,
"clientOps": dbdata.UserActLogIns.ClientOps,
}
RespSucess(w, data)
}

View File

@@ -178,7 +178,7 @@ func UserOnline(w http.ResponseWriter, r *http.Request) {
func UserOffline(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
token := r.FormValue("token")
sessdata.CloseSess(token)
sessdata.CloseSess(token, dbdata.UserLogoutAdmin)
RespSucess(w, nil)
}

View File

@@ -45,6 +45,7 @@ func StartAdmin() {
r.HandleFunc("/set/other/audit_log/edit", SetOtherAuditLogEdit)
r.HandleFunc("/set/audit/list", SetAuditList)
r.HandleFunc("/set/audit/export", SetAuditExport)
r.HandleFunc("/set/audit/act_log_list", UserActLogList)
r.HandleFunc("/user/list", UserList)
r.HandleFunc("/user/detail", UserDetail)

View 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)
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"`

View File

@@ -0,0 +1,184 @@
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 // 用户登出
UserLogoutLose = 0 // 用户掉线
UserLogoutBanner = 1 // 用户banner弹窗取消
UserLogoutClient = 2 // 用户主动登出
UserLogoutTimeout = 3 // 用户超时登出
UserLogoutAdmin = 4 // 账号被管理员踢下线
UserLogoutExpire = 5 // 账号过期被踢下线
)
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",
5: "Unknown",
},
ClientOps: []string{ // 客户端
0: "AnyConnect",
1: "OpenConnect",
2: "Unknown",
},
InfoOps: []string{ // 信息
UserLogoutLose: "用户掉线",
UserLogoutBanner: "用户取消弹窗",
UserLogoutClient: "用户/客户端主动断开",
UserLogoutTimeout: "Session过期被踢下线",
UserLogoutAdmin: "账号被管理员踢下线",
UserLogoutExpire: "账号过期被踢下线",
},
}
)
// 异步写入用户操作日志
func (ua *UserActLogProcess) Add(u UserActLog, userAgent string) {
// os, client, ver
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 {
return ua.InfoOps[id]
}
func (ua *UserActLogProcess) ParseUserAgent(userAgent string) (os_idx, client_idx uint8, ver string) {
// Unknown
if len(userAgent) == 0 {
return 5, 2, ""
}
// 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
}

View 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)
}
})
}
}

View File

@@ -43,7 +43,6 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
return
}
// fmt.Printf("%+v \n", cr)
setCommonHeader(w)
if cr.Type == "logout" {
// 退出删除session信息
@@ -66,16 +65,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: "请先激活用户"}
@@ -89,6 +99,8 @@ func LinkAuth(w http.ResponseWriter, r *http.Request) {
sess.Group = cr.GroupSelect
sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal
sess.UserAgent = userAgent
sess.RemoteAddr = r.RemoteAddr
// 获取客户端mac地址
macHw, err := net.ParseMAC(sess.MacAddr)
if err != nil {
@@ -109,7 +121,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 (

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/bjdgyc/anylink/sessdata"
)
@@ -55,6 +56,7 @@ func LinkCstp(conn net.Conn, bufRW *bufio.ReadWriter, cSess *sessdata.ConnSessio
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserLogoutCode = dbdata.UserLogoutClient
base.Debug("DISCONNECT", cSess.IpAddr)
return
case 0x03: // DPD-REQ

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/bjdgyc/anylink/sessdata"
)
@@ -57,6 +58,7 @@ func LinkDtls(conn net.Conn, cSess *sessdata.ConnSession) {
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
cSess.UserLogoutCode = dbdata.UserLogoutClient
base.Debug("DISCONNECT DTLS", cSess.IpAddr)
return
case 0x03: // DPD-REQ

View File

@@ -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)
}

View File

@@ -36,6 +36,8 @@ type ConnSession struct {
Mtu int
IfName string
Client string // 客户端 mobile pc
UserAgent string // 客户端信息
UserLogoutCode uint8 // 用户/客户端主动登出
CstpDpd int
Group *dbdata.Group
Limit *LimitRater
@@ -73,6 +75,8 @@ type Session struct {
Group string
AuthStep string
AuthPass string
RemoteAddr string
UserAgent string
LastLogin time.Time
IsActive bool
@@ -110,7 +114,7 @@ func checkSession() {
// 删除过期session
for _, v := range outToken {
CloseSess(v)
CloseSess(v, dbdata.UserLogoutTimeout)
}
}
}()
@@ -130,7 +134,7 @@ func CloseUserLimittimeSession() {
}
sessMux.RUnlock()
for _, v := range limitTimeToken {
CloseSess(v)
CloseSess(v, dbdata.UserLogoutExpire)
}
}
@@ -247,6 +251,7 @@ func (cs *ConnSession) Close() {
ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
LimitClient(cs.Username, true)
AddUserActLog(cs)
})
}
@@ -407,7 +412,7 @@ func DelSess(token string) {
// sessions.Delete(token)
}
func CloseSess(token string) {
func CloseSess(token string, code ...uint8) {
sessMux.Lock()
defer sessMux.Unlock()
sess, ok := sessions[token]
@@ -419,8 +424,13 @@ func CloseSess(token string) {
delete(dtlsIds, sess.DtlsSid)
if sess.CSess != nil {
if len(code) > 0 {
sess.CSess.UserLogoutCode = code[0]
}
sess.CSess.Close()
return
}
AddUserActLogBySess(sess)
}
func CloseCSess(token string) {
@@ -440,5 +450,29 @@ func DelSessByStoken(stoken string) {
stoken = strings.TrimSpace(stoken)
sarr := strings.Split(stoken, "@")
token := sarr[1]
CloseSess(token)
CloseSess(token, dbdata.UserLogoutBanner)
}
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,
}
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(cs.UserLogoutCode)
dbdata.UserActLogIns.Add(ua, cs.UserAgent)
}
func AddUserActLogBySess(sess *Session) {
ua := dbdata.UserActLog{
Username: sess.Username,
GroupName: sess.Group,
IpAddr: "",
RemoteAddr: sess.RemoteAddr,
Status: dbdata.UserLogout,
}
ua.Info = dbdata.UserActLogIns.GetInfoOpsById(1)
dbdata.UserActLogIns.Add(ua, sess.UserAgent)
}