mirror of
				https://github.com/bjdgyc/anylink.git
				synced 2025-10-31 16:43:28 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
		| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
							
								
								
									
										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"` | ||||
|   | ||||
							
								
								
									
										184
									
								
								server/dbdata/user_act_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								server/dbdata/user_act_log.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -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 ( | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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,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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										321
									
								
								web/src/components/audit/Access.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								web/src/components/audit/Access.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | ||||
| <template> | ||||
|   <div> | ||||
| <el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="search-form"> | ||||
|         <el-form-item label="用户名:" prop="username"> | ||||
|           <el-input size="mini" v-model="searchForm.username" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="源IP地址:" prop="src"> | ||||
|           <el-input size="mini" v-model="searchForm.src" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>     | ||||
|         <el-form-item label="目的IP地址:" prop="dst"> | ||||
|           <el-input size="mini" v-model="searchForm.dst" clearable style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>  | ||||
|         <el-form-item label="目的端口:" prop="dst_port"> | ||||
|           <el-input size="mini" v-model="searchForm.dst_port" clearable style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>  | ||||
|         <el-form-item label="访问协议:"> | ||||
|             <el-select size="mini" v-model="searchForm.access_proto" clearable placeholder="请选择" style="width: 100px"> | ||||
|                     <el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value"> | ||||
|                     </el-option> | ||||
|             </el-select>            | ||||
|         </el-form-item>   | ||||
|         <el-form-item label="日期范围:"> | ||||
|             <el-date-picker | ||||
|                 v-model="searchForm.date" | ||||
|                 type="datetimerange" | ||||
|                 value-format="yyyy-MM-dd HH:mm:ss" | ||||
|                 size="mini" | ||||
|                 align="left" | ||||
|                 start-placeholder="开始日期" | ||||
|                 end-placeholder="结束日期" | ||||
|                 :default-time="['00:00:00', '23:59:59']"> | ||||
|             </el-date-picker> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="详情:"> | ||||
|           <el-input size="mini" v-model="searchForm.info" placeholder="请输入关键字" clearable style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>          | ||||
|         <el-form-item> | ||||
|           <el-button | ||||
|               size="mini" | ||||
|               type="primary" | ||||
|               icon="el-icon-search" | ||||
|               @click="handleSearch">搜索 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|               size="mini" | ||||
|               icon="el-icon-refresh" | ||||
|               @click="rest">重置搜索 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|               size="mini" | ||||
|               icon="el-icon-download" | ||||
|               @click="handleExport">导出 | ||||
|           </el-button>           | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|  | ||||
|       <el-table | ||||
|           ref="multipleTable" | ||||
|           :data="tableData" | ||||
|           v-loading="loading" | ||||
|           element-loading-text="玩命加载中" | ||||
|           element-loading-spinner="el-icon-loading" | ||||
|           :default-sort="{ prop: 'id', order: 'descending' }" | ||||
|           @sort-change="sortChange"  | ||||
|           :header-cell-style="{backgroundColor:'#fcfcfc'}" | ||||
|           border> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="id" | ||||
|             label="ID" | ||||
|             sortable="custom" | ||||
|             width="100"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="username" | ||||
|             label="用户名" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="src" | ||||
|             label="源IP地址" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="dst" | ||||
|             label="目的IP地址" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="dst_port" | ||||
|             label="目的端口" | ||||
|             width="85"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="access_proto" | ||||
|             label="访问协议" | ||||
|             width="80" | ||||
|             :formatter="protoFormat"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="info" | ||||
|             label="详情"> | ||||
|         </el-table-column>         | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="created_at" | ||||
|             label="创建时间" | ||||
|             width="160" | ||||
|             :formatter="tableDateFormat"> | ||||
|         </el-table-column> | ||||
|       </el-table> | ||||
|  | ||||
|       <div class="sh-20"></div> | ||||
|  | ||||
|       <el-pagination | ||||
|           background | ||||
|           layout="prev, pager, next" | ||||
|           :pager-count="11" | ||||
|           :current-page.sync="currentPage" | ||||
|           @current-change="pageChange" | ||||
|           :total="count"> | ||||
|       </el-pagination> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
|  | ||||
| export default {     | ||||
|   name: "auditAccess", | ||||
|   mixins: [], | ||||
|   data() { | ||||
|     return { | ||||
|       tableData: [], | ||||
|       count: 10, | ||||
|       currentPage: 1, | ||||
|       idSort: 1, | ||||
|       activeName: "first", | ||||
|       accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"],  | ||||
|       defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]}, | ||||
|       searchForm: {}, | ||||
|       access_proto: [ | ||||
|             { text: 'UDP', value: '1' }, | ||||
|             { text: 'TCP', value: '2' }, | ||||
|             { text: 'HTTPS', value: '3' }, | ||||
|             { text: 'HTTP', value: '4' }, | ||||
|       ], | ||||
|       maxExportNum: 1000000, | ||||
|       loading: false, | ||||
|       rules: { | ||||
|         username: [ | ||||
|           {max: 30, message: '长度小于 30 个字符', trigger: 'blur'} | ||||
|         ], | ||||
|         src: [ | ||||
|           {  message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' }, | ||||
|         ],         | ||||
|         dst: [ | ||||
|           { message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' }, | ||||
|         ],                | ||||
|       },             | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     idSort: { | ||||
|         handler(newValue, oldValue) { | ||||
|             if (newValue != oldValue) { | ||||
|                 this.getData(1); | ||||
|             } | ||||
|         }, | ||||
|     }, | ||||
|   },   | ||||
|   methods: { | ||||
|     setSearchData() { | ||||
|         this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm)); | ||||
|     },     | ||||
|     handleSearch() { | ||||
|       this.$refs["searchForm"].validate((valid) => { | ||||
|         if (!valid) { | ||||
|           console.log('error submit!!'); | ||||
|           return false; | ||||
|         } | ||||
|         this.getData(1) | ||||
|       })           | ||||
|     }, | ||||
|     searchEnterFun(e) { | ||||
|         var keyCode = window.event ? e.keyCode : e.which; | ||||
|         if (keyCode == 13) { | ||||
|             this.handleSearch() | ||||
|         } | ||||
|     },         | ||||
|     getData(p) { | ||||
|       this.loading = true | ||||
|       if (! this.searchForm.date) { | ||||
|         this.searchForm.date = ["", ""]; | ||||
|       } | ||||
|       this.searchForm.sort = this.idSort      | ||||
|       axios.get('/set/audit/list', { | ||||
|         params: { | ||||
|           page: p, | ||||
|           search: this.searchForm, | ||||
|         } | ||||
|       }).then(resp => { | ||||
|         var data = resp.data.data | ||||
|         console.log(data);         | ||||
|         this.tableData = data.datas; | ||||
|         this.count = data.count | ||||
|         this.loading = false | ||||
|         this.currentPage = p; | ||||
|       }).catch(error => { | ||||
|         this.$message.error('哦,请求出错'); | ||||
|         console.log(error); | ||||
|       }); | ||||
|     }, | ||||
|     pageChange(p) {      | ||||
|         this.getData(p) | ||||
|     }, | ||||
|     handleExport() { | ||||
|       if (this.count > this.maxExportNum) { | ||||
|         var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){ | ||||
|            return s+',' | ||||
|         }) | ||||
|         this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出"); | ||||
|         return ; | ||||
|       } | ||||
|       if (! this.searchForm.date) { | ||||
|         this.searchForm.date = ["", ""]; | ||||
|       } | ||||
|       const exporting = this.$loading({ | ||||
|             lock: true, | ||||
|             text: '玩命导出中,请稍等片刻...', | ||||
|             spinner: 'el-icon-loading', | ||||
|             background: 'rgba(0, 0, 0, 0.7)' | ||||
|       }); | ||||
|       axios.get('/set/audit/export', { | ||||
|         params: { | ||||
|           search: this.searchForm, | ||||
|         } | ||||
|       }).then(resp => { | ||||
|         var rdata = resp.data | ||||
|         if (rdata.code && rdata.code != 0) { | ||||
|             exporting.close(); | ||||
|             this.$message.error(rdata.msg); | ||||
|             return ; | ||||
|         } | ||||
|         exporting.close(); | ||||
|         this.$message.success("成功导出CSV文件") | ||||
|         let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata | ||||
|         this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`) | ||||
|       }).catch(error => { | ||||
|         exporting.close(); | ||||
|         this.$message.error('哦,请求出错'); | ||||
|         console.log(error); | ||||
|       }); | ||||
|     }, | ||||
|     createDownLoadClick(content, fileName) { | ||||
|         const link = document.createElement('a') | ||||
|         link.href = encodeURI(content) | ||||
|         link.download = fileName | ||||
|         document.body.appendChild(link) | ||||
|         link.click() | ||||
|         document.body.removeChild(link) | ||||
|     },     | ||||
|     protoFormat(row) { | ||||
|         var access_proto = row.access_proto | ||||
|         if (row.access_proto == 0) { | ||||
|             switch (row.protocol) { | ||||
|                 case 6: access_proto = 2; break; | ||||
|                 case 17: access_proto = 1; break; | ||||
|             } | ||||
|         } | ||||
|         return this.accessProtoArr[access_proto] | ||||
|     }, | ||||
|     rest() { | ||||
|         console.log("rest"); | ||||
|         this.setSearchData(); | ||||
|         this.handleSearch(); | ||||
|     },  | ||||
|     validateIP(rule, value, callback) { | ||||
|         if (value === '' || typeof value === 'undefined' || value == null) { | ||||
|             callback() | ||||
|         } else { | ||||
|             const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ | ||||
|             if ((!reg.test(value)) && value !== '') { | ||||
|             callback(new Error('请输入正确的IP地址')) | ||||
|             } else { | ||||
|             callback() | ||||
|             } | ||||
|         } | ||||
|     },  | ||||
|     sortChange(column) { | ||||
|         let { order } = column; | ||||
|         if (order === 'ascending') { | ||||
|             this.idSort = 2; | ||||
|         } else { | ||||
|             this.idSort = 1; | ||||
|         } | ||||
|     },             | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .el-form-item { | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
| .el-table { | ||||
|     font-size: 12px; | ||||
| } | ||||
| .search-form >>> .el-form-item__label { | ||||
|   font-size: 12px; | ||||
| } | ||||
| /deep/ .el-table th { | ||||
|     padding: 5px 0; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										256
									
								
								web/src/components/audit/ActLog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								web/src/components/audit/ActLog.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | ||||
| <template> | ||||
|   <div> | ||||
|       <el-form  :model="searchForm" ref="searchForm" :inline="true" class="search-form"> | ||||
|         <el-form-item> | ||||
|           <el-input size="mini" v-model="searchForm.username" clearable placeholder="请输入用户名" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item> | ||||
|         <el-form-item> | ||||
|                 <el-date-picker | ||||
|                     v-model="searchForm.sdate" | ||||
|                     type="date" | ||||
|                     size="mini" | ||||
|                     placeholder="开始日期" | ||||
|                     format="yyyy-MM-dd" | ||||
|                     value-format="yyyy-MM-dd"   | ||||
|                     style="width: 130px" | ||||
|                 > | ||||
|                 </el-date-picker> | ||||
|             </el-form-item> | ||||
|             <el-form-item>     | ||||
|                 <el-date-picker | ||||
|                     v-model="searchForm.edate" | ||||
|                     type="date" | ||||
|                     size="mini" | ||||
|                     placeholder="结束日期" | ||||
|                     format="yyyy-MM-dd" | ||||
|                     value-format="yyyy-MM-dd"   | ||||
|                     style="width: 130px"               | ||||
|                 > | ||||
|             </el-date-picker> | ||||
|         </el-form-item> | ||||
|         <el-form-item > | ||||
|             <el-select size="mini" v-model="searchForm.status" clearable placeholder="操作类型" style="width: 130px"> | ||||
|                     <el-option v-for="(item,index) in statusOps" :key="index" :label="item.value" :value="item.key+1"> | ||||
|                     </el-option> | ||||
|             </el-select>            | ||||
|         </el-form-item> | ||||
|         <el-form-item> | ||||
|             <el-select size="mini" v-model="searchForm.os" clearable placeholder="操作系统" style="width: 130px"> | ||||
|                     <el-option v-for="(value,item,index) in osOps" :key="index" :label="value" :value="item+1"> | ||||
|                     </el-option> | ||||
|             </el-select>            | ||||
|         </el-form-item>         | ||||
|         <el-form-item> | ||||
|           <el-button | ||||
|               size="mini" | ||||
|               type="primary" | ||||
|               icon="el-icon-search" | ||||
|               @click="handleSearch">搜索 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|               size="mini" | ||||
|               icon="el-icon-refresh" | ||||
|               @click="rest">重置搜索 | ||||
|           </el-button> | ||||
|         </el-form-item> | ||||
|       </el-form> | ||||
|  | ||||
|       <el-table | ||||
|           ref="multipleTable" | ||||
|           :data="tableData" | ||||
|           :default-sort="{ prop: 'id', order: 'descending' }" | ||||
|           @sort-change="sortChange" | ||||
|           :header-cell-style="{backgroundColor:'#fcfcfc'}" | ||||
|           border> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="id" | ||||
|             label="ID" | ||||
|             sortable="custom" | ||||
|             width="60"> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             prop="username" | ||||
|             label="用户名" | ||||
|             width="80"> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             prop="group_name" | ||||
|             label="登陆组" | ||||
|             width="100"> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             prop="status" | ||||
|             label="操作类型" | ||||
|             width="92"> | ||||
|                 <template slot-scope="{ row }"> | ||||
|                     <span v-for="(item, index) in statusOps" :key="index"> | ||||
|                         <el-tag size="small" v-if="row.status == item.key" disable-transitions :type="item.tag">{{item.value}}</el-tag> | ||||
|                     </span> | ||||
|                 </template>             | ||||
|         </el-table-column>                | ||||
|         <el-table-column | ||||
|             prop="os" | ||||
|             label="操作系统" | ||||
|             width="82"> | ||||
|                 <template slot-scope="{ row }"> | ||||
|                     <span v-for="(value, item, index) in osOps" :key="index"> | ||||
|                     {{ row.os == item? value: "" }} | ||||
|                     </span> | ||||
|                 </template>              | ||||
|         </el-table-column>     | ||||
|         <el-table-column | ||||
|             prop="client" | ||||
|             label="客户端" | ||||
|             width="100"> | ||||
|                 <template slot-scope="{ row }"> | ||||
|                     <span v-for="(value, item, index) in clientOps" :key="index"> | ||||
|                     {{ row.client == item? value: "" }} | ||||
|                     </span> | ||||
|                     {{ row.version }}  | ||||
|                 </template>                            | ||||
|         </el-table-column>   | ||||
|         <el-table-column | ||||
|             prop="ip_addr" | ||||
|             label="内网IP" | ||||
|             width="120"> | ||||
|         </el-table-column> | ||||
|         <el-table-column | ||||
|             prop="remote_addr" | ||||
|             label="外网IP" | ||||
|             width="120"> | ||||
|         </el-table-column>   | ||||
|         <el-table-column | ||||
|             prop="info" | ||||
|             label="详情"> | ||||
|         </el-table-column>          | ||||
|         <el-table-column | ||||
|             prop="created_at" | ||||
|             label="操作时间" | ||||
|             width="130" | ||||
|             :formatter="tableDateFormat"> | ||||
|         </el-table-column>                                   | ||||
|       </el-table> | ||||
|  | ||||
|       <div class="sh-20"></div> | ||||
|         <el-pagination | ||||
|             background | ||||
|             layout="prev, pager, next"   | ||||
|             :pager-count="11" | ||||
|             @current-change="pageChange" | ||||
|             :current-page="page" | ||||
|             :total="count"> | ||||
|         </el-pagination> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
|  | ||||
| export default { | ||||
|   name: "List", | ||||
|   components: {}, | ||||
|   mixins: [], | ||||
|   created() { | ||||
|     this.$emit('update:route_path', this.$route.path) | ||||
|     this.$emit('update:route_name', ['用户信息', '登入日志']) | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       page: 1, | ||||
|       grouNames: [], | ||||
|       tableData: [], | ||||
|       idSort: 1, | ||||
|       count: 10, | ||||
|       searchForm: {username:'', sdate:'', edate:'', status:'', os:''}, | ||||
|       statusOps:[], | ||||
|       osOps:[], | ||||
|       clientOps:[],                   | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     idSort: { | ||||
|         handler(newValue, oldValue) { | ||||
|             if (newValue != oldValue) { | ||||
|                 this.getData(1); | ||||
|             } | ||||
|         }, | ||||
|     }, | ||||
|   }, | ||||
|   methods: {     | ||||
|     handleSearch() { | ||||
|       this.getData(1) | ||||
|     }, | ||||
|     pageChange(p) { | ||||
|       this.getData(p) | ||||
|     }, | ||||
|     searchEnterFun(e) { | ||||
|         var keyCode = window.event ? e.keyCode : e.which; | ||||
|         if (keyCode == 13) { | ||||
|             this.handleSearch() | ||||
|         } | ||||
|     },     | ||||
|     getData(page) { | ||||
|       console.log(this.searchForm) | ||||
|       this.page = page | ||||
|       axios.get('/set/audit/act_log_list', { | ||||
|         params: { | ||||
|           page: page, | ||||
|           username: this.searchForm.username || '', | ||||
|           sdate: this.searchForm.sdate || '', | ||||
|           edate: this.searchForm.edate || '', | ||||
|           status: this.searchForm.status || '', | ||||
|           os: this.searchForm.os || '', | ||||
|           sort: this.idSort, | ||||
|         } | ||||
|       }).then(resp => { | ||||
|         var data = resp.data.data | ||||
|         console.log(data); | ||||
|         this.tableData = data.datas; | ||||
|         this.count = data.count | ||||
|         this.statusOps = data.statusOps | ||||
|         this.osOps = data.osOps | ||||
|         this.clientOps = data.clientOps | ||||
|       }).catch(error => { | ||||
|         this.$message.error('哦,请求出错'); | ||||
|         console.log(error); | ||||
|       }); | ||||
|     }, | ||||
|     rest() { | ||||
|         console.log("rest"); | ||||
|         this.searchForm.username = ""; | ||||
|         this.searchForm.sdate = ""; | ||||
|         this.searchForm.edate = ""; | ||||
|         this.searchForm.status = ""; | ||||
|         this.searchForm.os = ""; | ||||
|         this.handleSearch(); | ||||
|     }, | ||||
|     sortChange(column) { | ||||
|         let { order } = column; | ||||
|         if (order === 'ascending') { | ||||
|             this.idSort = 2; | ||||
|         } else { | ||||
|             this.idSort = 1; | ||||
|         } | ||||
|     },     | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .el-form-item { | ||||
|     margin-bottom: 8px; | ||||
| } | ||||
| .el-table { | ||||
|     font-size: 12px; | ||||
| } | ||||
| .search-form >>> .el-form-item__label { | ||||
|   font-size: 12px; | ||||
| } | ||||
| /deep/ .el-table th { | ||||
|     padding: 5px 0; | ||||
| } | ||||
| /deep/ .el-table td { | ||||
|     padding: 5px 0; | ||||
| } | ||||
| </style> | ||||
| @@ -56,7 +56,7 @@ | ||||
|       <el-menu-item index="/admin/group/list">用户组列表</el-menu-item> | ||||
|     </el-submenu> | ||||
|  | ||||
|     <el-submenu index="3"> | ||||
|     <el-submenu index="4"> | ||||
|       <template slot="title"> | ||||
|         <i class="el-icon-s-order"></i> | ||||
|         <span slot="title">调试信息</span> | ||||
| @@ -91,4 +91,9 @@ export default { | ||||
| .layout-menu { | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| .el-menu-item a { | ||||
|   display: block; | ||||
|   color: #fff; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,300 +1,61 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <el-card>     | ||||
|       <el-form :model="searchForm" :rules="rules" ref="searchForm" :inline="true" class="form-inner-error"> | ||||
|         <el-form-item label="用户名:" prop="username"> | ||||
|           <el-input size="small" v-model="searchForm.username" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="源IP地址:" prop="src"> | ||||
|           <el-input size="small" v-model="searchForm.src" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>     | ||||
|         <el-form-item label="目的IP地址:" prop="dst"> | ||||
|           <el-input size="small" v-model="searchForm.dst" style="width: 130px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>  | ||||
|         <el-form-item label="目的端口:" prop="dst_port"> | ||||
|           <el-input size="small" v-model="searchForm.dst_port" style="width: 80px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>  | ||||
|         <el-form-item label="访问协议:"> | ||||
|             <el-select size="small" v-model="searchForm.access_proto" style="width: 100px"> | ||||
|                     <el-option v-for="(item,index) in access_proto" :key="index" :label="item.text" :value="item.value"> | ||||
|                     </el-option> | ||||
|             </el-select>            | ||||
|         </el-form-item>   | ||||
|         <div> | ||||
|         <el-form-item label="日期范围:"> | ||||
|             <el-date-picker | ||||
|                 v-model="searchForm.date" | ||||
|                 type="datetimerange" | ||||
|                 size="small" | ||||
|                 value-format="yyyy-MM-dd HH:mm:ss" | ||||
|                 range-separator="~" | ||||
|                 start-placeholder="开始日期" | ||||
|                 end-placeholder="结束日期"> | ||||
|             > | ||||
|             </el-date-picker> | ||||
|         </el-form-item> | ||||
|         <el-form-item label="详情:"> | ||||
|           <el-input size="small" v-model="searchForm.info" placeholder="请输入关键字" style="width: 200px" @keydown.enter.native="searchEnterFun"></el-input> | ||||
|         </el-form-item>          | ||||
|         <el-form-item> | ||||
|           <el-button | ||||
|               size="small" | ||||
|               type="primary" | ||||
|               icon="el-icon-search" | ||||
|               @click="handleSearch">搜索 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|               size="small" | ||||
|               icon="el-icon-refresh" | ||||
|               @click="rest">重置搜索 | ||||
|           </el-button> | ||||
|           <el-button | ||||
|               size="small" | ||||
|               icon="el-icon-download" | ||||
|               @click="handleExport">导出 | ||||
|           </el-button>           | ||||
|         </el-form-item> | ||||
|         </div> | ||||
|       </el-form> | ||||
|  | ||||
|       <el-table | ||||
|           ref="multipleTable" | ||||
|           :data="tableData" | ||||
|           v-loading="loading" | ||||
|           element-loading-text="玩命加载中" | ||||
|           element-loading-spinner="el-icon-loading" | ||||
|           border> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="id" | ||||
|             label="ID" | ||||
|             width="100"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="username" | ||||
|             label="用户名" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="src" | ||||
|             label="源IP地址" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="dst" | ||||
|             label="目的IP地址" | ||||
|             width="140"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="dst_port" | ||||
|             label="目的端口" | ||||
|             width="85"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="access_proto" | ||||
|             label="访问协议" | ||||
|             width="80" | ||||
|             :formatter="protoFormat"> | ||||
|         </el-table-column> | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="info" | ||||
|             label="详情"> | ||||
|         </el-table-column>         | ||||
|  | ||||
|         <el-table-column | ||||
|             prop="created_at" | ||||
|             label="创建时间" | ||||
|             width="150" | ||||
|             :formatter="tableDateFormat"> | ||||
|         </el-table-column> | ||||
|       </el-table> | ||||
|  | ||||
|       <div class="sh-20"></div> | ||||
|  | ||||
|       <el-pagination | ||||
|           background | ||||
|           layout="prev, pager, next" | ||||
|           :pager-count="11" | ||||
|           @current-change="pageChange" | ||||
|           :total="count"> | ||||
|       </el-pagination> | ||||
|  | ||||
|     <el-tabs v-model="activeName" @tab-click="handleClick"> | ||||
|         <el-tab-pane label="用户访问日志" name="access_audit"> | ||||
|             <AuditAccess ref="auditAccess"></AuditAccess> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane label="用户活动日志" name="act_log"> | ||||
|             <AuditActLog ref="auditActLog"></AuditActLog> | ||||
|         </el-tab-pane> | ||||
|     </el-tabs> | ||||
|     </el-card>       | ||||
|  | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from "axios"; | ||||
| import AuditAccess from "../../components/audit/Access"; | ||||
| import AuditActLog from "../../components/audit/ActLog"; | ||||
|  | ||||
| export default { | ||||
|   name: "Audit", | ||||
|   components: {}, | ||||
|   components:{ | ||||
|     AuditAccess, | ||||
|     AuditActLog | ||||
|   }, | ||||
|   mixins: [], | ||||
|   mounted() {     | ||||
|     this.upTab(); | ||||
|   },   | ||||
|   created() { | ||||
|     this.$emit('update:route_path', this.$route.path) | ||||
|     this.$emit('update:route_name', ['基础信息', '审计日志'])         | ||||
|   }, | ||||
|   mounted() {     | ||||
|     this.getData(1) | ||||
|     this.setSearchData() | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       tableData: [], | ||||
|       count: 10, | ||||
|       nowIndex: 0, | ||||
|       accessProtoArr:["", "UDP", "TCP", "HTTPS", "HTTP"],  | ||||
|       defSearchForm: {username:'', src:'', dst:'', dst_port:'', access_proto:'', info:'', date:["",""]}, | ||||
|       searchForm: {}, | ||||
|       access_proto: [ | ||||
|             { text: '请选择', value: '' }, | ||||
|             { text: 'UDP', value: '1' }, | ||||
|             { text: 'TCP', value: '2' }, | ||||
|             { text: 'HTTPS', value: '3' }, | ||||
|             { text: 'HTTP', value: '4' }, | ||||
|       ], | ||||
|       maxExportNum: 1000000, | ||||
|       loading: false, | ||||
|       rules: { | ||||
|         username: [ | ||||
|           {max: 30, message: '长度小于 30 个字符', trigger: 'blur'} | ||||
|         ], | ||||
|         src: [ | ||||
|           {  message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' }, | ||||
|         ],         | ||||
|         dst: [ | ||||
|           { message: '请输入正确的IP地址', validator: this.validateIP, trigger: 'blur' }, | ||||
|         ],                | ||||
|       },             | ||||
|       activeName: "access_audit", | ||||
|     } | ||||
|   }, | ||||
|   methods: {   | ||||
|     setSearchData() { | ||||
|         this.searchForm = JSON.parse(JSON.stringify(this.defSearchForm)); | ||||
|     },     | ||||
|     handleSearch() { | ||||
|       this.$refs["searchForm"].validate((valid) => { | ||||
|         if (!valid) { | ||||
|           console.log('error submit!!'); | ||||
|           return false; | ||||
|         } | ||||
|         this.getData(1) | ||||
|       })           | ||||
|     }, | ||||
|     searchEnterFun(e) { | ||||
|         var keyCode = window.event ? e.keyCode : e.which; | ||||
|         if (keyCode == 13) { | ||||
|             this.handleSearch() | ||||
|         } | ||||
|     },         | ||||
|     getData(p) { | ||||
|       this.loading = true | ||||
|       if (! this.searchForm.date) { | ||||
|         this.searchForm.date = ["", ""]; | ||||
|     upTab() { | ||||
|       var tabname = this.$route.query.tabname | ||||
|       if (tabname) { | ||||
|         this.activeName = tabname | ||||
|       } | ||||
|       axios.get('/set/audit/list', { | ||||
|         params: { | ||||
|           page: p, | ||||
|           search: this.searchForm, | ||||
|       this.handleClick(this.activeName)       | ||||
|     }, | ||||
|     handleClick() { | ||||
|         switch (this.activeName) { | ||||
|         case "access_audit": | ||||
|             this.$refs.auditAccess.setSearchData() | ||||
|             this.$refs.auditAccess.getData(1)             | ||||
|             break | ||||
|         case "act_log": | ||||
|             this.$refs.auditActLog.getData(1) | ||||
|             break           | ||||
|         } | ||||
|       }).then(resp => { | ||||
|         var data = resp.data.data | ||||
|         console.log(data); | ||||
|         this.tableData = data.datas; | ||||
|         this.count = data.count | ||||
|         this.loading = false | ||||
|       }).catch(error => { | ||||
|         this.$message.error('哦,请求出错'); | ||||
|         console.log(error); | ||||
|       }); | ||||
|         this.$router.push({path: this.$route.path, query: {tabname: this.activeName}}) | ||||
|     },     | ||||
|     pageChange(p) { | ||||
|       this.getData(p) | ||||
|     }, | ||||
|     handleExport() { | ||||
|       if (this.count > this.maxExportNum) { | ||||
|         var formatNum = (this.maxExportNum + "").replace(/\d{1,3}(?=(\d{3})+$)/g,function(s){ | ||||
|            return s+',' | ||||
|         }) | ||||
|         this.$message.error("你导出的数据量超过" + formatNum + "条,请调整搜索条件,再导出"); | ||||
|         return ; | ||||
|       } | ||||
|       if (! this.searchForm.date) { | ||||
|         this.searchForm.date = ["", ""]; | ||||
|       } | ||||
|       const exporting = this.$loading({ | ||||
|             lock: true, | ||||
|             text: '玩命导出中,请稍等片刻...', | ||||
|             spinner: 'el-icon-loading', | ||||
|             background: 'rgba(0, 0, 0, 0.7)' | ||||
|       }); | ||||
|       axios.get('/set/audit/export', { | ||||
|         params: { | ||||
|           search: this.searchForm, | ||||
|         } | ||||
|       }).then(resp => { | ||||
|         var rdata = resp.data | ||||
|         if (rdata.code && rdata.code != 0) { | ||||
|             exporting.close(); | ||||
|             this.$message.error(rdata.msg); | ||||
|             return ; | ||||
|         } | ||||
|         exporting.close(); | ||||
|         this.$message.success("成功导出CSV文件") | ||||
|         let csvData = 'data:text/csv;charset=utf-8,\uFEFF' + rdata | ||||
|         this.createDownLoadClick(csvData, `anylink_audit_log_` + Date.parse(new Date()) + `.csv`) | ||||
|       }).catch(error => { | ||||
|         exporting.close(); | ||||
|         this.$message.error('哦,请求出错'); | ||||
|         console.log(error); | ||||
|       }); | ||||
|     }, | ||||
|     createDownLoadClick(content, fileName) { | ||||
|         const link = document.createElement('a') | ||||
|         link.href = encodeURI(content) | ||||
|         link.download = fileName | ||||
|         document.body.appendChild(link) | ||||
|         link.click() | ||||
|         document.body.removeChild(link) | ||||
|     },     | ||||
|     protoFormat(row) { | ||||
|         var access_proto = row.access_proto | ||||
|         if (row.access_proto == 0) { | ||||
|             switch (row.protocol) { | ||||
|                 case 6: access_proto = 2; break; | ||||
|                 case 17: access_proto = 1; break; | ||||
|             } | ||||
|         } | ||||
|         return this.accessProtoArr[access_proto] | ||||
|     }, | ||||
|     rest() { | ||||
|         console.log("rest"); | ||||
|         this.setSearchData(); | ||||
|         this.handleSearch(); | ||||
|     },  | ||||
|     validateIP(rule, value, callback) { | ||||
|         if (value === '' || typeof value === 'undefined' || value == null) { | ||||
|             callback() | ||||
|         } else { | ||||
|             const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ | ||||
|             if ((!reg.test(value)) && value !== '') { | ||||
|             callback(new Error('请输入正确的IP地址')) | ||||
|             } else { | ||||
|             callback() | ||||
|             } | ||||
|         } | ||||
|     },         | ||||
|   }, | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|  | ||||
| </style> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user