Commit abfc4893 authored by liming6's avatar liming6
Browse files

feature 基础代码

parent 4c2688a9
......@@ -17,3 +17,8 @@ sshd-tool监听这个unix socket,过滤出需要的信息
最终,sshd-tool提供查询服务
## todo
- 适配ubuntu系统,对于Ubuntu,系统who -u中的pid是ssh日志中pid的子进程,需要处理一下
- 能查询出以前的,没有被sshd-tool记录的在线情况
package asset
import (
"regexp"
"strings"
"testing"
"time"
)
var (
ReSSHLogin = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: Accepted (\S+) for (\S+) from (\S+) port (\d+) ssh(?:|\d+)$`)
ReSSHLoginPK = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: Accepted publickey for (\S+) from (\S+) port (?:\d+) ssh(?:|\d+):\s+(\S+)\s+(?:sha|SHA)256:(.*)$`)
ReSSHLogout = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: pam_unix\(sshd:session\): session closed for user (.*)$`)
)
func Test1(t *testing.T) {
target := "<86>Sep 28 14:52:37 login01 sshd[4092]: Accepted keyboard-interactive/pam for fengchao from 10.206.8.202 port 55805 ssh2"
if ReSSHLogin.MatchString(target) {
start := time.Now()
f := ReSSHLogin.FindStringSubmatch(target)
s := time.Since(start)
t.Logf("regexp use %d ms", s.Milliseconds())
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func Test2(t *testing.T) {
target := "<86>Dec 23 15:57:07 bw11 sshd[1575855]: Accepted publickey for root from 10.16.4.1 port 60058 ssh2: ED25519 SHA256:0o84iZ8MJUCRzTIipR8eLzX2g+Rx96MKMVq/RakG/GA"
if ReSSHLoginPK.MatchString(target) {
f := ReSSHLoginPK.FindStringSubmatch(target)
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func Test3(t *testing.T) {
target := "<86>Dec 22 19:09:50 liming-ecs sshd[3831482]: pam_unix(sshd:session): session closed for user root"
if ReSSHLogout.MatchString(target) {
f := ReSSHLogout.FindStringSubmatch(target)
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func Test4(t *testing.T) {
a := "abc==="
t.Log(strings.Trim(a, "="))
}
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8lrLRp2q+ssghlnz5uLZvCxG36n3guNrtlFG7r85+Gz7ufWCtHk1DtlywbIN7k5jrhUiYTNFCWx0iUfeliIhLfrwBBEXOb7W2JRdJiQMnGlhzjPHiYbMncU6IJz8cmpgTaV+qcxnhj+18t851BZ3S2hOkoyHx9+FVcxMVzFiDEoUm6yth2OFHxaxYFbgUJ0Ll3d8lPOF9uEW++XtiFdAQqJ6zN3771y8THxKbXAbQwJwD/QqfrEb47umlWZQMpwRoK7/kKENlqJ5SK2WDcBwvIEbyYsSqa7BWIxSzUPzhG4oT18DWHUlQ9GsAeOxOPidHFiYY81sy2Of9YBjLvs+F3ehMK3omXjzQIDZVCIBSI32p4sfiNzZDCmfT+9O6SEUBCaFawGAamHcMrDAnvJn/SroYw5ruRUSE7Csfh/fB7SxTTC6fXw2g6sCtfpVn2ml4uixmG4pev/mvgzyCuP4mMZluEoF9YGedXOa/M2FgJmflHrvW8OfGCnljndF1+Ok= root@admin02
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDKJzNDzIjx+GYJVbJOqcdm6xH6Bnd156ZxwiH8BxHzB1zugRv/c03cMwpad+IXnI+r2REv8CZpH2xL15b5Vqv39JV9ewO2Wk0Kw5YmYfB92p8f3QBrQUiyr+LxcQkGWiTvWjE2O12DAy5I47H+2dZ9x9p83eM6R29QeKZOtaa1tTrE8eEC7Gf6GrPRThnrMJhOHULA54zAf1hX9yR86c8RcH5ib9iyg4FH7rTIhRSOgl/CGCdtED8j1K8CyV18ifrHHa2F1gLYvW9GdrqXyO+84afOFBW3TXpzJIB0CkQ4S/nDJrn2EV3v2hIliqI6avHDcb5+NmVmNekKLDm5sHeHFXsjmeIX5PBDmXrdg9ChXLOc9VZHkukRAE6iK4vFXQEOmYHF+LP+rhJv8xp0RlBfrfxIjFFpSaJunSiKdM/kNoK5ZzZwdvezmosBT5QmXfpC6dUA+70MpSoDWmjDf5O9p6EhaRxD7VblK9affsfA8uMMFd6BytsIV3C8PkPc+20= 925456133@qq.com
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDGEfg3oY0gIZDMn4g0paz8i+1NnDcqFScJ9HIEpu1t825MIYX6uiesQGW1KMaSMkcPO4ciUzAkJjPjcMOZ5HquXDsbrJW31bLzTdoN/fEOJwD1WDRs82eFMoBcnko5D6IzV5Uli1IwEh5tvLgx+x4q0kmyUTFhDfhSBW+EOlAMS+/8ch1/+VZzhgHUVaKVUmqKppJfE9USTn1PGBrYPzBOqwTMJGGRkh+P7O+5NYNpVIMonT5UXmwjBm/Z5DeRMjbxQeRKk+W9B3MlTF3z3N1BlRVxQEsav200zgy41EWxYVQmfcNUMRUUtAg2Z2BzK8G/nCd3csg3zSz1Yxz4e9ruwJBjUbFy69FbMS0hSR3dRXpkCQqJiBiUZGmMbeBS0TyEbAYvKRbgnJCdYzDRYJGd4s7eIq0nI152wg+XKo8WPXitayYGYO6dH1jikSDVZpX34gWTmoaYN53WzlcPqNyiBDkvaH4Xfcr1alhZmN0qMFpDmlr+rIxka5WEtBACELU= root@admin01
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBN/UmnD/E2zsG/7HpDBzjMEehqcU/c4+k3FLGZF0fw1l4KL5owm1odV2LJxwCWoQv/VPTVgaqna1USlLG3xa52zqya1pzPKZ4xOpJ8LXTtDg91thltdynaDuL04PTMNEhx/inHzatQyeNBvkpaJAGiTGEhulJ0fPf9hk9QwWjtE7Ud7rQcN2sxanTnO1vHdd8W1HDwcxs0ZIquKvlnAtrAblCpaOS0Mgb7Rqs8Gliv5M1IQmi0RZnmF3zxRrKesdawDlTCxOnkscED+mSneelD2qQujtnTvFVFCFQTwo+UKw+ceuLRqShMT2KvjZKlEo9MxjSjcunSnqrLzOAleS2fdFA3BJTyDdRIlPLD8aIy/QymS3+iJwkz9RhtmLaRZiIVtOLDeeQf4zzTjiB0banASQQJFuC0d1AR/bJSH1yaZh8ZNH/wgnTiBmnHTKb/8LOYbLdE3GNdRP2nmkZqg0z4Mse2MXLNpMmx7dzn29nQfHndgiTUAXRe5RVo4DN7Zk= liming6@sugon.com
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ//DeJIwpNQ3bboddfladiUs/xMbsbRikgvJ/m+30Uj liming
\ No newline at end of file
This diff is collapsed.
# rsyslog配置,将指定服务的日志发送到socket里
$ModLoad omuxsock
$OMUxSockSocket /tmp/mysock
authpriv.* :omuxsock:
Dec 22 17:52:31 bw11 sshd[59102]: Accepted publickey for root from 10.16.4.1 port 36752 ssh2: ED25519 SHA256:0o84iZ8MJUCRzTIipR8eLzX2g+Rx96MKMVq/RakG/GA
Dec 22 17:52:31 bw11 sshd[59102]: pam_unix(sshd:session): session opened for user root by (uid=0)
Dec 22 17:55:34 bw11 sshd[59109]: Received disconnect from 10.16.4.1 port 36752:11:
Dec 22 17:55:34 bw11 sshd[59109]: Disconnected from user root 10.16.4.1 port 36752
Dec 22 17:55:34 bw11 sshd[59102]: pam_unix(sshd:session): session closed for user root
Dec 22 19:09:47 liming-ecs sshd[3831482]: Accepted password for root from 125.46.29.2 port 58138 ssh2
Dec 22 19:09:47 liming-ecs sshd[3831482]: pam_unix(sshd:session): session opened for user root by (uid=0)
Dec 22 19:09:50 liming-ecs sshd[3831486]: Received disconnect from 125.46.29.2 port 58138:11:
Dec 22 19:09:50 liming-ecs sshd[3831486]: Disconnected from user root 125.46.29.2 port 58138
Dec 22 19:09:50 liming-ecs sshd[3831482]: pam_unix(sshd:session): session closed for user root
Sep 28 14:52:37 login01 sshd[4092]: Accepted keyboard-interactive/pam for fengchao from 10.206.8.202 port 55805 ssh2
"<86>Dec 23 15:57:07 bw11 sshd[1575855]: Accepted publickey for root from 10.16.4.1 port 60058 ssh2: ED25519 SHA256:0o84iZ8MJUCRzTIipR8eLzX2g+Rx96MKMVq/RakG/GA"
\ No newline at end of file
root tty1 2025-12-22 19:38 . 1603
root pts/0 2025-12-22 19:34 00:04 1486 (192.168.200.1)
root pts/1 2025-12-22 19:39 . 1486 (192.168.200.1)
gaoya pts/0 2025-12-22 10:45 05:20 218163 (10.16.4.4)
gaoya pts/1 2025-12-22 10:52 08:47 218822 (10.16.5.2)
fanwl pts/3 2025-12-22 14:23 05:23 239781 (10.16.4.4)
gaoya pts/4 2025-12-22 11:02 00:44 221233 (10.16.5.2)
fanth pts/5 2025-12-22 15:10 01:34 246862 (10.16.4.4)
fanth pts/6 2025-12-22 15:27 01:34 249322 (10.16.4.4)
gaoya pts/7 2025-12-22 18:00 01:44 402386 (10.16.5.2)
ops :1 2025-12-22 19:45 ? 430638 (:1)
root pts/8 2025-12-22 19:48 . 432149 (10.16.4.1)
\ No newline at end of file
package main
import (
"log"
"net"
"os"
"github.com/gin-gonic/gin"
)
var (
GIN_SOCK_PATH = "/tmp/gin.sock"
)
func InitGin() {
os.RemoveAll(GIN_SOCK_PATH)
sock, err := net.Listen("unix", GIN_SOCK_PATH)
if err != nil {
log.Fatalf("error listen unix socket %s: %v", GIN_SOCK_PATH, err)
}
err = os.Chmod(GIN_SOCK_PATH, 0666)
if err != nil {
sock.Close()
log.Fatalf("error chmod of %s: %v", GIN_SOCK_PATH, err)
}
g := gin.Default()
g.Any("/", getLoginedUserInfo)
go g.RunListener(sock)
}
package main
import (
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
)
var (
wg = sync.WaitGroup{}
)
func main() {
InitSSH()
InitGin()
socketPath := "/tmp/mysock"
err := os.RemoveAll(socketPath)
if err != nil {
log.Fatalf("error delete %s: %v", socketPath, err)
}
conn, err := net.ListenPacket("unixgram", socketPath)
if err != nil {
log.Fatalf("listen unix socket %s failed: %s", socketPath, err.Error())
}
err = os.Chmod(socketPath, 0666)
if err != nil {
log.Fatalf("chmod 666 %s failed: %v", socketPath, err)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
wg.Add(1)
go func(wg *sync.WaitGroup) {
select {
case <-sigChan:
conn.Close()
os.Remove(socketPath)
case <-globalCtx.Done():
conn.Close()
os.Remove(socketPath)
}
wg.Done()
}(&wg)
buffer := make([]byte, 16384)
for {
n, _, err := conn.ReadFrom(buffer)
if err != nil {
// log.Printf("error read unix socket: %v", err)
globalCancelFunc()
break
}
go ParseSSHLog(string(buffer[:n]))
}
wg.Wait()
}
#!/bin/bash
curl -s --unix-socket /tmp/gin.sock http://localhost | jq
\ No newline at end of file
package main
import (
"context"
"fmt"
"log"
"maps"
"os"
"regexp"
"slices"
"sshd-tool/utils"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
)
var (
sysuerInfo map[int]utils.LinuxSysUser = nil // key uid
sysuserLock = sync.RWMutex{}
username2uid map[string]int = nil
haveNis bool = false
globalCtx context.Context = nil
globalCancelFunc context.CancelFunc = nil
onlineInfo map[string]utils.OnlineUser = make(map[string]utils.OnlineUser) // key pidstr
onlineLock = sync.RWMutex{}
hostname string
loginedUser map[string]LoginedUser = make(map[string]LoginedUser)
loginedLock = sync.RWMutex{}
)
// getUserInfo 根据用户名获取系统用户信息
func getUserInfo(name string) *utils.LinuxSysUser {
rl := sysuserLock.RLocker()
rl.Lock()
uid, have := username2uid[name]
if !have {
rl.Unlock()
user, _ := utils.GetOneSysUser(name)
if user == nil && !haveNis {
return nil
}
if user == nil {
user, _ = utils.GetOneNisUser(name)
}
if user == nil {
return nil
}
sysuserLock.Lock()
sysuerInfo[user.Uid] = utils.LinuxSysUser{
Name: user.Name,
Home: user.Home,
Shell: user.Shell,
Uid: user.Uid,
Gid: user.Gid,
SSHkeyInfo: maps.Clone(user.SSHkeyInfo),
}
username2uid[user.Name] = user.Uid
sysuserLock.Unlock()
return user
}
user := sysuerInfo[uid]
rl.Unlock()
result := utils.LinuxSysUser{
Name: user.Name,
Home: user.Home,
Shell: user.Shell,
Uid: user.Uid,
Gid: user.Gid,
SSHkeyInfo: maps.Clone(user.SSHkeyInfo),
}
return &result
}
type LoginedUser struct {
Online utils.OnlineUser `json:"online"` // 在线信息
AuthType string `json:"authType"` // 登录时的认证方式
KeyHash *string `json:"keyHash,omitempty"` // 登录时使用的公钥hash
KeyUser *string `json:"keyUser,omitempty"` // 登录公钥的用户信息
}
// updateOnline 更新在线用户信息
func updateOnline() {
us, err := utils.GetOnlineUser()
if err != nil {
log.Fatalf("error get online user: %v", err)
}
onlineLock.Lock()
clear(onlineInfo)
for _, v := range us {
onlineInfo[v.PidString()] = v
}
onlineLock.Unlock()
}
// Init 初始化
func InitSSH() {
ypcat := utils.FindCmd(utils.NIS_YPCAT)
if ypcat != nil {
u, err := utils.GetNisUser()
if err != nil {
log.Fatalf("error get nis user: %v", err)
}
sysuerInfo = u
haveNis = true
}
sysU, err := utils.GetSysUser()
if err != nil {
log.Fatalf("error get sys user: %v", err)
}
if sysuerInfo == nil {
sysuerInfo = sysU
} else {
maps.Copy(sysuerInfo, sysU)
}
username2uid = make(map[string]int)
for _, v := range sysuerInfo {
username2uid[v.Name] = v.Uid
}
globalCtx, globalCancelFunc = context.WithCancel(context.Background())
n, err := os.Hostname()
if err != nil {
log.Fatalf("error get hostname: %v", err)
}
hostname = n
us, err := utils.GetOnlineUser()
if err != nil {
log.Fatalf("error get online user: %v", err)
}
for _, v := range us {
onlineInfo[v.PidString()] = v
}
go updateSysuser()
}
// updateSysuser 每10秒刷新一下用户信息
func updateSysuser() {
ticker := time.NewTicker(time.Second * 10)
for {
select {
case <-ticker.C:
if haveNis {
u, err := utils.GetNisUser()
if err != nil {
log.Printf("error get nis user: %v", err)
globalCancelFunc()
continue
}
sysU, err := utils.GetSysUser()
if err != nil {
log.Printf("error get sys user: %v", err)
globalCancelFunc()
continue
}
maps.Copy(u, sysU)
sysuserLock.Lock()
sysuerInfo = u
for _, v := range sysuerInfo {
username2uid[v.Name] = v.Uid
}
sysuserLock.Unlock()
} else {
sysU, err := utils.GetSysUser()
if err != nil {
log.Printf("error get sys user: %v", err)
globalCancelFunc()
continue
}
sysuserLock.Lock()
sysuerInfo = sysU
for _, v := range sysuerInfo {
username2uid[v.Name] = v.Uid
}
sysuserLock.Unlock()
}
case <-globalCtx.Done():
ticker.Stop()
return
}
}
}
/*
登录
Sep 30 11:08:47 login01 sshd[1768988]: Accepted keyboard-interactive/pam for caiyu from 61.153.50.229 port 4733 ssh2
退出
Sep 30 11:08:19 login01 sshd[1767042]: pam_unix(sshd:session): session closed for user <username>
*/
var (
ReSSHLogin = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: Accepted (\S+) for (\S+) from (\S+) port (\d+) ssh(?:|\d+)$`)
ReSSHLoginPK = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: Accepted publickey for (\S+) from (\S+) port (?:\d+) ssh(?:|\d+):\s+(\S+)\s+(?:sha|SHA)256:(.*)$`)
ReSSHLogout = regexp.MustCompile(`^<\d+>[A-Z][a-z]{2} \d+ \d+:\d+:\d+ (\S+) sshd\[(\d+)\]: pam_unix\(sshd:session\): session closed for user (.*)$`)
)
// ParseSSHLog 过滤出登录和退出ssh的sshd日志,并对全局信息做出修改
func ParseSSHLog(str string) {
if !strings.Contains(str, "sshd") {
// 不是sshd相关日志
return
}
if ReSSHLogin.MatchString(str) {
go handleSSHLogin(str)
return
}
if ReSSHLoginPK.MatchString(str) {
go handleSSHLoginPK(str)
return
}
if ReSSHLogout.MatchString(str) {
go handleSSHLogout(str)
return
}
}
func handleSSHLogout(str string) {
fields := ReSSHLogout.FindStringSubmatch(str)
user := fields[3]
pidstr := strList(fields[2])
rl := loginedLock.RLocker()
rl.Lock()
u, have := loginedUser[pidstr]
rl.Unlock()
if !have {
return
}
if u.Online.Name == user {
loginedLock.Lock()
delete(loginedUser, pidstr)
loginedLock.Unlock()
onlineLock.Lock()
delete(onlineInfo, pidstr)
onlineLock.Unlock()
}
}
func handleSSHLoginPK(str string) {
fields := ReSSHLoginPK.FindStringSubmatch(str)
name := fields[3]
pidstr := strList(fields[2])
keyHash := strings.Trim(fields[6], "=")
auth := "publickey"
user := getUserInfo(name)
if user == nil {
log.Fatal("unknow error, can't find user")
}
if user.SSHkeyInfo == nil {
log.Fatal("error, login use publickey, but can't find the key")
}
key, have := user.SSHkeyInfo[keyHash]
if !have {
log.Fatal("error, login use publickey, but can't find the key")
}
updateOnline()
u := LoginedUser{}
u.AuthType = auth
u.KeyHash = &keyHash
keyUser := key.UserInfo
u.KeyUser = &keyUser
rl := onlineLock.RLocker()
rl.Lock()
on, have := onlineInfo[pidstr]
rl.Unlock()
if !have {
log.Fatalf("sshd login, but who not find: %s", pidstr)
}
u.Online = utils.OnlineUser{
Name: on.Name,
Type: on.Type,
When: on.When,
Pids: slices.Clone(on.Pids),
LoginFrom: on.LoginFrom,
}
loginedLock.Lock()
loginedUser[u.Online.PidString()] = u
loginedLock.Unlock()
}
func handleSSHLogin(str string) {
fields := ReSSHLogin.FindStringSubmatch(str)
from := fields[5]
auth := fields[3]
pidstr := strList(fields[2])
updateOnline()
rl := onlineLock.RLocker()
rl.Lock()
on, have := onlineInfo[pidstr]
rl.Unlock()
if !have {
log.Fatalf("sshd login, but who not find: %s", pidstr)
}
u := LoginedUser{}
u.AuthType = auth
u.Online = utils.OnlineUser{
Name: on.Name,
Type: on.Type,
When: on.When,
Pids: slices.Clone(on.Pids),
LoginFrom: from,
}
loginedLock.Lock()
loginedUser[u.Online.PidString()] = u
loginedLock.Unlock()
}
func strList(str string) string {
pids := strings.Split(str, ",")
pid := make([]int, 0, 4)
for _, p := range pids {
pp, err := strconv.Atoi(strings.Trim(p, " "))
if err != nil {
log.Fatalf("error convert string to int: %v", err)
}
pid = append(pid, pp)
}
slices.Sort(pid)
return fmt.Sprintf("%v", pid)
}
func getLoginedUserInfo(ctx *gin.Context) {
rl := loginedLock.RLocker()
rl.Lock()
defer rl.Unlock()
ctx.JSON(200, loginedUser)
}
package common
module sshd-tool
go 1.24.9
require github.com/gin-gonic/gin v1.11.0
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package utils
import (
"os"
"strings"
)
var (
PATH = make([]string, 0, 128)
)
func parsePath() {
envPath, have := os.LookupEnv("PATH")
if !have {
return
}
envPath = strings.Trim(envPath, "\n")
plist := strings.Split(envPath, ":")
clear(PATH)
for _, v := range plist {
PATH = append(PATH, strings.TrimSuffix(v, "\n"))
}
}
// FindCmd 查询命令是否存在
func FindCmd(cmd string) *string {
if len(PATH) == 0 {
parsePath()
if len(PATH) == 0 {
return nil
}
}
for _, v := range PATH {
target := v + "/" + cmd
stat, err := os.Stat(target)
if err != nil {
continue
}
p := stat.Mode().Perm()
if p.IsDir() {
continue
}
if p&0111 != 0 {
return &target
}
}
return nil
}
package utils
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
"time"
)
const (
NIS_YPCAT = "ypcat"
SYS_PASSWD = "/etc/passwd"
)
// LinuxSysUser linux系统用户信息
type LinuxSysUser struct {
Name string
Home string
Shell string
Uid int
Gid int
SSHkeyInfo map[string]SSHPubKeyInfo
}
func GetOneSysUser(name string) (*LinuxSysUser, error) {
content, err := os.ReadFile(SYS_PASSWD)
if err != nil {
return nil, err
}
lines := strings.Split(strings.Trim(string(content), "\n"), "\n")
for _, line := range lines {
if strings.HasPrefix(line, name) {
fields := strings.Split(line, ":")
if len(fields) != 7 || fields[0] != name {
continue
}
uid, err := strconv.Atoi(fields[2])
if err != nil {
continue
}
if uid < 999 && uid != 0 {
continue
}
u := LinuxSysUser{}
u.Uid = uid
gid, err := strconv.Atoi(fields[3])
if err != nil {
continue
}
u.Gid = gid
u.Name = fields[0]
u.Shell = fields[6]
u.Home = fields[5]
s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
if err == nil {
u.SSHkeyInfo = s
}
return &u, nil
}
}
return nil, nil
}
func GetOneNisUser(name string) (*LinuxSysUser, error) {
ypcat := FindCmd(NIS_YPCAT)
if ypcat == nil {
return nil, nil
}
content, err := exec.Command(NIS_YPCAT, "passwd").Output()
if err != nil {
return nil, err
}
lines := strings.Split(strings.Trim(string(content), "\n"), "\n")
for _, line := range lines {
if strings.HasPrefix(line, name) {
fields := strings.Split(line, ":")
if len(fields) != 7 || fields[0] != name {
continue
}
uid, err := strconv.Atoi(fields[2])
if err != nil {
continue
}
if uid < 999 && uid != 0 {
continue
}
u := LinuxSysUser{}
u.Uid = uid
gid, err := strconv.Atoi(fields[3])
if err != nil {
continue
}
u.Gid = gid
u.Name = fields[0]
u.Shell = fields[6]
u.Home = fields[5]
s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
if err == nil {
u.SSHkeyInfo = s
}
return &u, nil
}
}
return nil, nil
}
// GetSysUser 获取系统用户信息
func GetSysUser() (map[int]LinuxSysUser, error) {
content, err := os.ReadFile(SYS_PASSWD)
if err != nil {
return nil, err
}
r := parsePasswd(string(content))
return r, nil
}
// GetNisUser 获取Nis用户信息
func GetNisUser() (map[int]LinuxSysUser, error) {
ypcat := FindCmd(NIS_YPCAT)
if ypcat == nil {
return make(map[int]LinuxSysUser), nil
}
output, err := exec.Command(NIS_YPCAT, "passwd").Output()
if err != nil {
return nil, err
}
r := parsePasswd(string(output))
return r, nil
}
// GetClusUser 获取Clusconf用户信息
func GetClusUser() {
// todo
}
type SSHPubKeyInfo struct {
Type string
Content string // base64格式的公钥
UserInfo string
Sha256 string // 公钥经过sha256后,再base64,如果结尾是=,删除=
}
// ParseSSHAuthKey 解析ssh认证文件的内容
func ParseSSHAuthKey(path string) (map[string]SSHPubKeyInfo, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
pub := string(content)
lines := strings.Split(strings.Trim(pub, "\n"), "\n")
result := make(map[string]SSHPubKeyInfo)
for _, line := range lines {
if len(line) == 0 {
continue
}
fields := strings.Fields(line)
if len(fields) != 3 {
continue
}
if !strings.HasPrefix(fields[0], "ssh-") {
continue
}
item := SSHPubKeyInfo{}
item.Type, _ = strings.CutPrefix(fields[0], "ssh-")
item.Content = fields[1]
item.UserInfo = fields[2]
key, err := base64.StdEncoding.DecodeString(item.Content)
if err != nil {
continue
}
b := sha256.Sum256(key)
item.Sha256 = strings.Trim(base64.StdEncoding.EncodeToString(b[:]), "=")
result[item.Sha256] = item
}
return result, nil
}
func parsePasswd(str string) map[int]LinuxSysUser {
result := make(map[int]LinuxSysUser)
pwd := strings.Trim(string(str), "\n")
lines := strings.Split(pwd, "\n")
for _, line := range lines {
fields := strings.Split(line, ":")
if len(fields) != 7 {
continue
}
uid, err := strconv.Atoi(fields[2])
if err != nil {
continue
}
if uid < 999 && uid != 0 {
continue
}
u := LinuxSysUser{}
u.Uid = uid
gid, err := strconv.Atoi(fields[3])
if err != nil {
continue
}
u.Gid = gid
u.Name = fields[0]
u.Shell = fields[6]
u.Home = fields[5]
s, err := ParseSSHAuthKey(u.Home + "/.ssh/authorized_keys")
if err == nil {
u.SSHkeyInfo = s
}
result[u.Uid] = u
}
return result
}
type OnlineUser struct {
Name string `json:"name"`
Type string `json:"type"`
When time.Time `json:"loginTime"`
Pids []int `json:"pids"`
LoginFrom string `json:"loginForm"`
}
func (ou OnlineUser) String() string {
return fmt.Sprintf("name:%s type:%s when:%s pids:%v login from:%s", ou.Name, ou.Type, ou.When.Format("2006-01-02 15:04"), ou.Pids, ou.LoginFrom)
}
func (ou OnlineUser) Sha256sum() [32]byte {
return sha256.Sum256([]byte(ou.String()))
}
func (ou OnlineUser) PidString() string {
if len(ou.Pids) == 0 {
return "[]"
}
return fmt.Sprintf("%v", ou.Pids)
}
var (
ReOnLineUser = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+([a-zA-Z0-9/]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+(?:old|\.|\d{2}:\d{2})\s+(\d*(?:,\d*)*)\s+\((.*)\)$`) // sshd远程登录的
ReOnLineUserTTY = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+(tty[0-9]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+(?:old|\.|\d{2}:\d{2})\s+(\d*(?:,\d*)*)$`) // 通过控制台登录的
ReOnLineUserX = regexp.MustCompile(`^(?i)([a-zA-Z_0-9]*)\s+(:[0-9]*)\s+(\d{4}-\d{1,2}-\d{1,2} \d{2}:\d{2})\s+\?\s+(\d*(?:,\d*)*).*$`) // 通过图像界面
)
// GetOnlineUser
func GetOnlineUser() ([]OnlineUser, error) {
output, err := exec.Command("who", "-u").Output()
if err != nil {
return nil, err
}
lines := strings.Split(strings.Trim(string(output), "\n"), "\n")
result := make([]OnlineUser, 0, 16)
for _, line := range lines {
if ReOnLineUser.MatchString(line) {
m := ReOnLineUser.FindStringSubmatch(line)
if len(m) != 6 {
continue
}
u := OnlineUser{}
u.Name = m[1]
u.Type = m[2]
t, err := time.Parse("2006-01-02 15:04", m[3])
if err != nil {
return nil, err
}
u.When = t
u.LoginFrom = m[5]
pids := strings.Split(m[4], ",")
u.Pids = make([]int, 0, len(pids))
for _, v := range pids {
p, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
u.Pids = append(u.Pids, p)
}
slices.Sort(u.Pids)
result = append(result, u)
} else if ReOnLineUserTTY.MatchString(line) {
// todo
}
}
return result, nil
}
package utils
import (
"crypto/sha256"
"encoding/base64"
"testing"
)
func TestFindCmd(t *testing.T) {
target := "ypcat"
p := FindCmd(target)
if p == nil {
t.Logf("not find %s", target)
return
}
t.Logf("find : %s", *p)
}
func TestKeySha256(t *testing.T) {
// AAAAC3NzaC1lZDI1NTE5AAAAIJ//DeJIwpNQ3bboddfladiUs/xMbsbRikgvJ/m+30Uj
// 0o84iZ8MJUCRzTIipR8eLzX2g+Rx96MKMVq/RakG/GA
b, err := base64.StdEncoding.DecodeString("AAAAC3NzaC1lZDI1NTE5AAAAIJ//DeJIwpNQ3bboddfladiUs/xMbsbRikgvJ/m+30Uj")
if err != nil {
t.Error(err)
}
sh := sha256.Sum256(b)
t.Log(base64.StdEncoding.EncodeToString(sh[:]))
}
func TestGetSysUser(t *testing.T) {
u, err := GetSysUser()
if err != nil {
t.Error(err)
}
for k, v := range u {
t.Logf("%d: %s, home: %s, shell: %s", k, v.Name, v.Home, v.Shell)
if len(v.SSHkeyInfo) > 0 {
for _, s := range v.SSHkeyInfo {
t.Logf(" %s: %s", s.UserInfo, s.Type)
}
}
}
}
func TestGetNisUser(t *testing.T) {
u, err := GetNisUser()
if err != nil {
t.Error(err)
}
for k, v := range u {
t.Logf("%d: %s, home: %s, shell: %s", k, v.Name, v.Home, v.Shell)
if len(v.SSHkeyInfo) > 0 {
for _, s := range v.SSHkeyInfo {
t.Logf(" %s: %s", s.UserInfo, s.Type)
}
}
}
}
func TestReOnLineUser(t *testing.T) {
target := "gaojm pts/2 2025-12-22 14:13 00:08 12010 (10.16.4.4)"
m := ReOnLineUser.MatchString(target)
if m {
f := ReOnLineUser.FindStringSubmatch(target)
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func TestReOnLineUserX(t *testing.T) {
target := "ops :1 2025-12-22 19:45 ? 430638 (:1)"
m := ReOnLineUserX.MatchString(target)
if m {
f := ReOnLineUserX.FindStringSubmatch(target)
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func TestReOnLineUserTTY(t *testing.T) {
target := "root tty1 2025-12-22 19:38 . 1603"
m := ReOnLineUserTTY.MatchString(target)
if m {
f := ReOnLineUserTTY.FindStringSubmatch(target)
for _, v := range f {
t.Log(v)
}
} else {
t.Error("not match")
}
}
func TestGetOnlineUser(t *testing.T) {
u, err := GetOnlineUser()
if err != nil {
t.Error(err)
}
for _, v := range u {
t.Logf("%+v\n", v)
}
}
func TestGetOneSysUser(t *testing.T) {
u, err := GetOneSysUser("root")
if err != nil {
t.Error(err)
}
t.Logf("%d: %s, home: %s, shell: %s", u.Uid, u.Name, u.Home, u.Shell)
if len(u.SSHkeyInfo) > 0 {
for _, s := range u.SSHkeyInfo {
t.Logf(" %s: %s", s.UserInfo, s.Type)
}
}
}
func TestGetOneNisUser(t *testing.T) {
u, err := GetOneNisUser("liming6")
if err != nil {
t.Error(err)
}
t.Logf("%d: %s, home: %s, shell: %s", u.Uid, u.Name, u.Home, u.Shell)
if len(u.SSHkeyInfo) > 0 {
for _, s := range u.SSHkeyInfo {
t.Logf(" %s: %s", s.UserInfo, s.Type)
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment