commit 5326172a99025ad6da5bc7dca2d97b3db4a728ff Author: redhat <2292650292@qq.com> Date: Wed May 21 09:37:59 2025 +0800 1、first commit。 diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..47d3945 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,28 @@ +base: + name: "avcnet_dash" + port: 8080 + mode: "dev" + version: "v0.0.1" + +log: + level: "debug" + filename: "dash.log" + max_size: 2 + max_age: 180 + max_backups: 20 + +# 使用令牌桶限流 +rate: + fill_interval: 10 # 填充速率 每(ms)填充一个 + capacity: 10 #桶容量 + #quantum: 1 # 指定每次填充的个数 + #rate: 0.1 # 和capacity搭配使用,指定填充速率 rate = 0.1 capacity = 200 则表示每秒20填充20个令牌 + max_wait: 2 # 最长等待时间(s) + +snowflake: + start_time: "2025-05-20" + machine_id: 1 + +jwt: + salt: "redhat" + expire: 86400 \ No newline at end of file diff --git a/controller/author.go b/controller/author.go new file mode 100644 index 0000000..de23db9 --- /dev/null +++ b/controller/author.go @@ -0,0 +1,60 @@ +package controller + +import ( + "dashboard/models" + "dashboard/pkg/jwt" + "dashboard/utils" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" +) + +func LoginPage(c *gin.Context) { + log, _ := utils.GetLogFromContext(c) + + log.Info("Serving login page") + + c.File(models.FileLoginHtml) +} + +func UserSignInHandler(jwtC *jwt.Jwt) func(c *gin.Context) error { + return func(c *gin.Context) error { + var user models.UserInfoParams + + if err := c.ShouldBindJSON(&user); err != nil { + return err + } + + if user.Password != models.AdminPassword { + return models.ErrorPasswordErr + } + + tokenStr, err := jwtC.GenToken(map[string]interface{}{ + "username": "admin", + }) + + if err != nil { + fmt.Println(err) + return err + } + + fmt.Println(tokenStr) + + token := fmt.Sprintf("%s%s", models.GinAuthorPrefixKey, tokenStr) + + c.SetCookie(models.GinAuthorKey, token, 24*60*60, "/", "", false, true) + + c.JSON(http.StatusOK, gin.H{"message": "Login successful"}) + + return nil + } +} + +func UserLogOutHandler(c *gin.Context) { + log, _ := utils.GetLogFromContext(c) + + c.SetCookie(models.GinAuthorKey, "", -1, "/", "", false, true) + log.Sugar().Info("Logout successful") + c.JSON(http.StatusOK, gin.H{"message": "Logout successful"}) +} diff --git a/controller/files.go b/controller/files.go new file mode 100644 index 0000000..5a7da9f --- /dev/null +++ b/controller/files.go @@ -0,0 +1,244 @@ +package controller + +import ( + "archive/zip" + "dashboard/utils" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +type FileInfo struct { + Name string `json:"name"` + IsDir bool `json:"is_dir"` + Size int64 `json:"size"` + ModTime time.Time `json:"mod_time"` +} + +func FileUploadHandle(c *gin.Context) error { + log, _ := utils.GetLogFromContext(c) + + file, err := c.FormFile("file") + if err != nil { + log.Sugar().Errorf("Failed to get file: %v", err) + return err + } + + targetDir := c.PostForm("target_dir") + if targetDir == "" { + log.Sugar().Errorf("Target directory not specified") + return errors.New("Target directory not specified") + } + + // 防止路径穿越攻击 + cleanDir := filepath.Clean(targetDir) + if strings.Contains(cleanDir, "..") || !strings.HasPrefix(cleanDir, "/") { + log.Sugar().Errorf("Invalid target directory: %s", cleanDir) + return errors.New("Invalid target directory") + } + + // 限制文件大小(10MB) + const maxFileSize = 10 << 20 // 10MB + if file.Size > maxFileSize { + log.Sugar().Errorf("File too large: %d bytes", file.Size) + return errors.New("File size exceeds 10MB") + } + + // 确保目标目录存在 + if err := os.MkdirAll(cleanDir, 0755); err != nil { + log.Sugar().Errorf("Failed to create directory %s: %v", cleanDir, err) + return err + } + + // 保存文件 + targetPath := filepath.Join(cleanDir, file.Filename) + if err := c.SaveUploadedFile(file, targetPath); err != nil { + log.Sugar().Errorf("Failed to save file to %s: %v", targetPath, err) + return err + } + + log.Sugar().Debug("File uploaded successfully to %s", targetPath) + c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("File uploaded to %s", targetPath)}) + + return nil +} + +func FileListHandle(c *gin.Context) error { + log, _ := utils.GetLogFromContext(c) + + dirPath := c.Query("path") + if dirPath == "" { + log.Sugar().Errorf("Directory path not specified") + return errors.New("Directory path not specified") + } + + // 清理路径并规范化 + cleanPath := filepath.Clean(dirPath) + if !strings.HasPrefix(cleanPath, "/") { + log.Sugar().Errorf("Invalid directory path (not absolute): %s", cleanPath) + return errors.New("Invalid directory path") + } + + // 防止路径穿越攻击 + if strings.Contains(cleanPath, "/../") { + log.Sugar().Errorf("Invalid directory path (contains ..): %s", cleanPath) + return errors.New("Invalid directory path") + } + + // 读取目录 + entries, err := os.ReadDir(cleanPath) + if err != nil { + log.Sugar().Errorf("Failed to read directory %s: %v", cleanPath, err) + return err + } + + var files []FileInfo + for _, entry := range entries { + info, err := entry.Info() + if err != nil { + log.Sugar().Errorf("Failed to get info for %s: %v", entry.Name(), err) + continue + } + files = append(files, FileInfo{ + Name: entry.Name(), + IsDir: entry.IsDir(), + Size: info.Size(), + ModTime: info.ModTime(), + }) + } + + log.Sugar().Debug("Listed files in %s: %d entries", cleanPath, len(files)) + c.JSON(http.StatusOK, gin.H{"files": files}) + + return nil +} + +func FileDownloadHandle(c *gin.Context) error { + log, _ := utils.GetLogFromContext(c) + + filePath := c.Query("path") + if filePath == "" { + log.Sugar().Errorf("File path not specified") + return errors.New("File path not specified") + } + + // 清理路径并规范化 + cleanPath := filepath.Clean(filePath) + if !strings.HasPrefix(cleanPath, "/") { + log.Sugar().Errorf("Invalid file path (not absolute): %s", cleanPath) + return errors.New("Invalid file path") + } + + // 防止路径穿越攻击 + if strings.Contains(cleanPath, "/../") { + log.Sugar().Errorf("Invalid file path (contains ..): %s", cleanPath) + return errors.New("Invalid file path") + } + + // 检查路径是否存在 + fileInfo, err := os.Stat(cleanPath) + if err != nil { + log.Sugar().Errorf("Path not found %s: %v", cleanPath, err) + return errors.New("Path not found") + } + + if !fileInfo.IsDir() { + // 处理文件下载 + const maxFileSize = 10 << 20 // 10MB + if fileInfo.Size() > maxFileSize { + log.Sugar().Errorf("File too large: %d bytes", fileInfo.Size()) + return errors.New("File size exceeds 10MB") + } + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath))) + c.Header("Content-Type", "application/octet-stream") + c.File(cleanPath) + return nil + } + + // 处理目录下载(创建ZIP) + tmpFile, err := os.CreateTemp("", "dir-*.zip") + if err != nil { + log.Sugar().Errorf("Failed to create temp file: %v", err) + return errors.New("Failed to create zip file") + } + defer os.Remove(tmpFile.Name()) // 清理临时文件 + + zipWriter := zip.NewWriter(tmpFile) + totalSize := int64(0) + const maxZipSize = 10 << 20 // 10MB + + err = filepath.Walk(cleanPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 创建ZIP中的文件头 + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // 设置ZIP中的相对路径 + relPath, err := filepath.Rel(cleanPath, path) + if err != nil { + return err + } + header.Name = relPath + + if info.IsDir() { + header.Name += "/" + } else { + header.Method = zip.Deflate + totalSize += info.Size() + if totalSize > maxZipSize { + return fmt.Errorf("directory size exceeds 10MB") + } + } + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + zipWriter.Close() + log.Sugar().Errorf("Failed to create zip for %s: %v", cleanPath, err) + return err + } + + if err := zipWriter.Close(); err != nil { + log.Sugar().Errorf("Failed to close zip writer: %v", err) + return err + } + + // 设置下载头 + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)+".zip")) + c.Header("Content-Type", "application/zip") + c.File(tmpFile.Name()) + log.Sugar().Debug("Directory %s downloaded as zip", cleanPath) + + return nil +} diff --git a/controller/sysinfo.go b/controller/sysinfo.go new file mode 100644 index 0000000..de259a2 --- /dev/null +++ b/controller/sysinfo.go @@ -0,0 +1,105 @@ +package controller + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" +) + +type SystemInfo struct { + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + DiskUsage float64 `json:"disk_usage"` + LoadAvg float64 `json:"load_avg"` + CPUCores int `json:"cpu_cores"` + CPUMHz float64 `json:"cpu_mhz"` + MemoryTotal uint64 `json:"memory_total"` + MemoryFree uint64 `json:"memory_free"` + DiskPartitions []DiskPartition `json:"disk_partitions"` + Uptime uint64 `json:"uptime"` +} + +type DiskPartition struct { + Mountpoint string `json:"mountpoint"` + Total uint64 `json:"total"` + Used uint64 `json:"used"` + UsedPercent float64 `json:"used_percent"` +} + +func getSystemInfo() (SystemInfo, error) { + var info SystemInfo + + // CPU 使用率 + cpuPercent, err := cpu.Percent(time.Second, false) + if err == nil && len(cpuPercent) > 0 { + info.CPUUsage = cpuPercent[0] + } + + // CPU 详细信息 + cpuInfo, err := cpu.Info() + if err == nil && len(cpuInfo) > 0 { + info.CPUCores = int(cpuInfo[0].Cores) + info.CPUMHz = cpuInfo[0].Mhz + } + + // 内存信息 + vm, err := mem.VirtualMemory() + if err == nil { + info.MemoryUsage = vm.UsedPercent + info.MemoryTotal = vm.Total + info.MemoryFree = vm.Free + } + + // 磁盘使用率(根目录) + diskUsage, err := disk.Usage("/") + if err == nil { + info.DiskUsage = diskUsage.UsedPercent + } + + // 磁盘分区信息 + partitions, err := disk.Partitions(false) + if err == nil { + for _, p := range partitions { + usage, err := disk.Usage(p.Mountpoint) + if err == nil { + info.DiskPartitions = append(info.DiskPartitions, DiskPartition{ + Mountpoint: p.Mountpoint, + Total: usage.Total, + Used: usage.Used, + UsedPercent: usage.UsedPercent, + }) + } + } + } + + // 系统负载 + loadAvg, err := load.Avg() + if err == nil { + info.LoadAvg = loadAvg.Load1 + } + + // 系统运行时间 + uptime, err := host.Uptime() + if err == nil { + info.Uptime = uptime + } + + return info, nil +} + +func SystemInfoHandle(c *gin.Context) error { + info, err := getSystemInfo() + if err != nil { + return err + } + + c.JSON(http.StatusOK, info) + + return nil +} diff --git a/controller/websocket.go b/controller/websocket.go new file mode 100644 index 0000000..2ac3fbf --- /dev/null +++ b/controller/websocket.go @@ -0,0 +1,96 @@ +package controller + +import ( + "context" + "dashboard/utils" + "net/http" + "os/exec" + + "github.com/creack/pty" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "golang.org/x/sync/errgroup" +) + +func TerminalHandle() func(c *gin.Context) error { + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true // 允许跨域 + }, + } + return func(c *gin.Context) error { + log, _ := utils.GetLogFromContext(c) + + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Sugar().Errorf("WebSocket upgrade failed: %v", err) + return err + } + defer conn.Close() + + cmd := exec.Command("bash") + ptyFile, err := pty.Start(cmd) + if err != nil { + log.Sugar().Errorf("Failed to start pty: %v", err) + return err + } + defer ptyFile.Close() + + wg, ctx := errgroup.WithContext(context.Background()) + + wg.Go(func() error { + defer log.Sugar().Errorf("task read over") + for { + select { + case <-ctx.Done(): + log.Sugar().Errorf("task canceld read") + return ctx.Err() + default: + } + + _, msg, err := conn.ReadMessage() + if err != nil { + log.Sugar().Errorf("WebSocket read error: %v", err) + return err + } + _, err = ptyFile.Write(msg) + if err != nil { + log.Sugar().Errorf("PTY write error: %v", err) + return err + } + } + }) + + wg.Go(func() error { + defer log.Sugar().Errorf("task write over") + buf := make([]byte, 1024) + for { + select { + case <-ctx.Done(): + log.Sugar().Errorf("task canceld write") + return ctx.Err() + default: + } + + n, err := ptyFile.Read(buf) + if err != nil { + log.Sugar().Errorf("PTY read error: %v", err) + return err + } + err = conn.WriteMessage(websocket.BinaryMessage, buf[:n]) + if err != nil { + log.Sugar().Errorf("WebSocket write error: %v", err) + return err + } + } + }) + + if err := wg.Wait(); err != nil { + log.Sugar().Errorf("WebSocket error: %v", err) + } + + return nil + } +} diff --git a/dash b/dash new file mode 100755 index 0000000..69964a7 Binary files /dev/null and b/dash differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9fa4f0c --- /dev/null +++ b/go.mod @@ -0,0 +1,67 @@ +module dashboard + +go 1.23.0 + +toolchain go1.23.9 + +require ( + github.com/bwmarrin/snowflake v0.3.0 + github.com/creack/pty v1.1.24 + github.com/fsnotify/fsnotify v1.9.0 + github.com/gin-contrib/cors v1.7.5 + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/gorilla/websocket v1.5.3 + github.com/juju/ratelimit v1.0.2 + github.com/natefinch/lumberjack v2.0.0+incompatible + github.com/shirou/gopsutil/v4 v4.25.4 + github.com/spf13/viper v1.20.1 + go.uber.org/zap v1.27.0 + golang.org/x/sync v0.12.0 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/go-ole/go-ole v1.2.6 // 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.26.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..65918ad --- /dev/null +++ b/go.sum @@ -0,0 +1,161 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= +github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk= +github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= +github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +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/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw= +github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +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.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..79e4692 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,96 @@ +package logger + +import ( + "io" + "os" + + "github.com/natefinch/lumberjack" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Logger struct { + opts options + *zap.Logger + level zap.AtomicLevel +} + +func New(opts ...Option) (*Logger, error) { + logger := new(Logger) + + for _, o := range opts { + o(&logger.opts) + } + + err := logger.logInit() + + return logger, err +} + +func (l *Logger) SetLogLevel(levelStr string) error { + level := new(zapcore.Level) + if err := level.UnmarshalText([]byte(levelStr)); err != nil { + return err + } + + l.level.SetLevel(*level) + + return nil +} + +func (l *Logger) logWriter() zapcore.WriteSyncer { + var res io.Writer + res = os.Stdout + + if l.opts.mode == "release" { + lj := &lumberjack.Logger{ + Filename: l.opts.fileName, + MaxSize: l.opts.maxSize, + MaxBackups: l.opts.maxBackUp, + MaxAge: l.opts.maxAge, + } + + res = io.MultiWriter(lj, res) + } + + return zapcore.AddSync(res) +} + +func (l *Logger) logEncoder() zapcore.Encoder { + ec := zap.NewProductionEncoderConfig() + ec.EncodeTime = zapcore.ISO8601TimeEncoder + ec.TimeKey = "time" + ec.EncodeLevel = zapcore.CapitalColorLevelEncoder + ec.EncodeDuration = zapcore.SecondsDurationEncoder + ec.EncodeCaller = zapcore.ShortCallerEncoder + + return zapcore.NewConsoleEncoder(ec) +} + +func (l *Logger) logLevel() error { + level, err := zap.ParseAtomicLevel(l.opts.level) + if err != nil { + return err + } + + l.level = level + + return nil +} + +func (l *Logger) logInit() error { + if err := l.logLevel(); err != nil { + return err + } + + writeSyncer := l.logWriter() + encoder := l.logEncoder() + + core := zapcore.NewCore(encoder, writeSyncer, l.level) + + l.Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) + + zap.ReplaceGlobals(l.Logger) + + return nil +} diff --git a/logger/option.go b/logger/option.go new file mode 100644 index 0000000..1ec0c6f --- /dev/null +++ b/logger/option.go @@ -0,0 +1,48 @@ +package logger + +type options struct { + maxSize int + maxAge int + maxBackUp int + fileName string + level string + mode string +} + +type Option func(o *options) + +func WithMaxSize(maxSize int) Option { + return func(o *options) { + o.maxSize = maxSize + } +} + +func WithMaxAge(maxAge int) Option { + return func(o *options) { + o.maxAge = maxAge + } +} + +func WithMaxBackUp(maxBackUp int) Option { + return func(o *options) { + o.maxBackUp = maxBackUp + } +} + +func WithFileName(fileName string) Option { + return func(o *options) { + o.fileName = fileName + } +} + +func WithLevel(levle string) Option { + return func(o *options) { + o.level = levle + } +} + +func WithMode(mode string) Option { + return func(o *options) { + o.mode = mode + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2c406d0 --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "dashboard/logger" + "dashboard/routes" + "dashboard/settings" + "errors" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "go.uber.org/zap" +) + +var config = flag.String("f", "./config/config.yaml", "config file path") + +func main() { + flag.Parse() + + sets := settings.New(settings.WithName(*config)) + + log, err := logger.New( + logger.WithFileName(sets.LogConfig.Filename), + logger.WithLevel(sets.LogConfig.Level), + logger.WithMaxAge(sets.LogConfig.MaxAge), + logger.WithMaxSize(sets.LogConfig.MaxSize), + logger.WithMaxBackUp(sets.LogConfig.MaxBackUps), + logger.WithMode(sets.BaseConfig.Mode), + ) + if err != nil { + fmt.Println(err) + panic(err) + } + defer func() { + _ = log.Sync() + }() + + log.Info("Settings and log init ok") + + r := routes.Setup(log, *sets.RateLimitConfig, *sets.JwtConfig) + + listenAndServe(fmt.Sprintf(":%d", sets.BaseConfig.Port), r, log) +} + +func listenAndServe(addr string, handle http.Handler, log *logger.Logger) { + srv := &http.Server{ + Addr: addr, + Handler: handle, + } + + log.Sugar().Infof("Server listen on %s", addr) + go func() { + // 开启一个goroutine启动服务 + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Error("listen: %s\n", zap.Error(err)) + } + }() + + // 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时 + quit := make(chan os.Signal, 1) // 创建一个接收信号的通道 + // kill 默认会发送 syscall.SIGTERM 信号 + // kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号 + // kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它 + // signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞 + <-quit // 阻塞在此,当接收到上述两种信号时才会往下执行 + log.Info("Shutdown Server ...") + // 创建一个5秒超时的context + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出 + if err := srv.Shutdown(ctx); err != nil { + log.Error("Server Shutdown: ", zap.Error(err)) + } + + log.Info("Server exiting") +} diff --git a/models/errorcode.go b/models/errorcode.go new file mode 100644 index 0000000..57bc59c --- /dev/null +++ b/models/errorcode.go @@ -0,0 +1,34 @@ +package models + +type resCode int + +const codeBase = 1000 + +const ( + CodeSuccess resCode = iota + codeBase +) + +var codeMsg = map[resCode]string{ + CodeSuccess:"success", +} + +func (r resCode) String() string { + if res,ok:=codeMsg[r];ok{ + return res + } + + return codeMsg[CodeSuccess] +} + +type BaseError struct { + Code resCode + Msg string +} + +func (b *BaseError) Error() string { + if b.Msg != "" { + return b.Msg + } + + return b.Code.String() +} diff --git a/models/params.go b/models/params.go new file mode 100644 index 0000000..b2bc117 --- /dev/null +++ b/models/params.go @@ -0,0 +1,5 @@ +package models + +type UserInfoParams struct { + Password string `json:"password" binding:"required"` +} diff --git a/models/type.go b/models/type.go new file mode 100644 index 0000000..99b22cf --- /dev/null +++ b/models/type.go @@ -0,0 +1,32 @@ +package models + +import ( + "errors" + "os" +) + +const ( + GinContxtLog = "log" +) + +const ( + GinAuthorKey = "Authorization" + GinAuthorPrefixKey = "Bearer " +) + +var ( + ErrorInvalidData = errors.New("no such value") + ErrorPasswordErr = errors.New("user or password invalid") +) + +const ( + FileLoginHtml = "./static/login.html" +) + +var AdminPassword = os.Getenv("ADMIN_PASSWORD") + +func init() { + if AdminPassword == "" { + AdminPassword = "admin@123" + } +} diff --git a/pkg/jwt/jwt.go b/pkg/jwt/jwt.go new file mode 100644 index 0000000..fae2efd --- /dev/null +++ b/pkg/jwt/jwt.go @@ -0,0 +1,78 @@ +package jwt + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "maps" +) + +type Jwt struct { + options +} + +func New(opts ...Option) *Jwt { + res := new(Jwt) + for _, o := range opts { + o(&res.options) + } + + return res +} + +func (j *Jwt) GenToken(mp map[string]interface{}) (string, error) { + claims := jwt.MapClaims{ + "exp": time.Now().Add(j.expire).Unix(), + } + + maps.Copy(claims, mp) + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return token.SignedString(j.salt) +} + +func (j *Jwt) ParseToken(tokenStr string) (map[string]interface{}, error) { + token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + + return j.salt, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} + +func ErrorIsJwtExpired(err error) bool { + return errors.Is(err, jwt.ErrTokenExpired) +} + +func GetValueStringFromToken(jwt map[string]interface{}, key string) (string, bool) { + if value, ok := jwt[key]; ok { + if res, ok := value.(string); ok { + return res, true + } + } + + return "", false +} + +func GetValueInt64FromToken(jwt map[string]interface{}, key string) (int64, bool) { + if value, ok := jwt[key]; ok { + if res, ok := value.(int64); ok { + return res, true + } + } + + return 0, false +} diff --git a/pkg/jwt/option.go b/pkg/jwt/option.go new file mode 100644 index 0000000..536a185 --- /dev/null +++ b/pkg/jwt/option.go @@ -0,0 +1,22 @@ +package jwt + +import "time" + +type options struct { + expire time.Duration + salt []byte +} + +type Option func(*options) + +func WithSalt(salt string) Option { + return func(o *options) { + o.salt = []byte(salt) + } +} + +func WithExpire(expire time.Duration) Option { + return func(o *options) { + o.expire = expire + } +} diff --git a/pkg/rate/option.go b/pkg/rate/option.go new file mode 100644 index 0000000..06daec8 --- /dev/null +++ b/pkg/rate/option.go @@ -0,0 +1,36 @@ +package rate + +import "time" + +type options struct { + fillInterval time.Duration + capacity int64 + quantum int64 + rate float64 +} + +type Option func(o *options) + +func WithFillInterval(fi time.Duration) Option { + return func(o *options) { + o.fillInterval = fi + } +} + +func WithCapacity(cap int64) Option { + return func(o *options) { + o.capacity = cap + } +} + +func WithQuantum(qt int64) Option { + return func(o *options) { + o.quantum = qt + } +} + +func WithRate(rate float64) Option { + return func(o *options) { + o.rate = rate + } +} diff --git a/pkg/rate/ratelimit.go b/pkg/rate/ratelimit.go new file mode 100644 index 0000000..1955dac --- /dev/null +++ b/pkg/rate/ratelimit.go @@ -0,0 +1,39 @@ +package rate + +import ( + "errors" + + "github.com/juju/ratelimit" +) + +type Limit struct { + *ratelimit.Bucket +} + +func New(opts ...Option) (*Limit, error) { + opt := new(options) + for _, o := range opts { + o(opt) + } + + res := new(Limit) + if opt.fillInterval > 0 && opt.capacity > 0 && opt.quantum > 0 { + res.Bucket = ratelimit.NewBucketWithQuantum(opt.fillInterval, opt.capacity, opt.quantum) + + return res, nil + } + + if opt.fillInterval > 0 && opt.capacity > 0 { + res.Bucket = ratelimit.NewBucket(opt.fillInterval, opt.capacity) + + return res, nil + } + + if opt.rate > 0 && opt.capacity > 0 { + res.Bucket = ratelimit.NewBucketWithRate(opt.rate, opt.capacity) + + return res, nil + } + + return nil, errors.New("options error can not find func to init") +} diff --git a/pkg/snowflake/option.go b/pkg/snowflake/option.go new file mode 100644 index 0000000..1012e9c --- /dev/null +++ b/pkg/snowflake/option.go @@ -0,0 +1,20 @@ +package snowflake + +type options struct { + startTime string + machineId int64 +} + +type Option func(o *options) + +func WithStartTime(startTime string) Option { + return func(o *options) { + o.startTime = startTime + } +} + +func WithMachineId(machineId int64) Option { + return func(o *options) { + o.machineId = machineId + } +} diff --git a/pkg/snowflake/snowflake.go b/pkg/snowflake/snowflake.go new file mode 100644 index 0000000..c3701fc --- /dev/null +++ b/pkg/snowflake/snowflake.go @@ -0,0 +1,37 @@ +package snowflake + +import ( + "time" + + "github.com/bwmarrin/snowflake" +) + +type Snow struct { + *snowflake.Node +} + +func New(opts ...Option) (*Snow, error) { + opt := new(options) + for _, o := range opts { + o(opt) + } + + res := new(Snow) + + st, err := time.Parse("2006-01-02", opt.startTime) + if err != nil { + return nil, err + } + + snowflake.Epoch = st.UnixNano() / 1000000 + res.Node, err = snowflake.NewNode(opt.machineId) + if err != nil { + return nil, err + } + + return res, nil +} + +func (s *Snow) Gen() int64 { + return s.Generate().Int64() +} diff --git a/routes/middleware.go b/routes/middleware.go new file mode 100644 index 0000000..72ef1cb --- /dev/null +++ b/routes/middleware.go @@ -0,0 +1,155 @@ +package routes + +import ( + "dashboard/logger" + "dashboard/models" + "dashboard/pkg/jwt" + "dashboard/pkg/rate" + "dashboard/settings" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +func GinLogger(log *logger.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + + c.Next() + + cost := time.Since(start) + + log.Info(path, + zap.Int("status", c.Writer.Status()), + zap.String("method", c.Request.Method), + zap.String("path", path), + zap.String("query", query), + zap.String("ip", c.ClientIP()), + zap.String("user-agent", c.Request.UserAgent()), + zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), + zap.Duration("cost", cost), + ) + } +} + +func GinRecovery(log *logger.Logger, stack bool) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + expectStr := strings.ToLower(se.Error()) + if strings.Contains(expectStr, "broken pipe") || + strings.Contains(expectStr, "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(c.Request, false) + if brokenPipe { + log.Error(c.Request.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + + c.Error(err.(error)) + c.Abort() + return + } + + if stack { + log.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + zap.String("stack", string(debug.Stack())), + ) + } else { + log.Error("[Recovery from panic]", + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + + c.AbortWithStatus(http.StatusInternalServerError) + } + }() + + c.Next() + } +} + +func GinLog(log *logger.Logger) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set(models.GinContxtLog, log) + c.Next() + } +} + +func GinJwtAuthor(jwtC *jwt.Jwt) appHandler { + return func(c *gin.Context) error { + authHeader, err := c.Cookie(models.GinAuthorKey) + if err != nil || !strings.HasPrefix(authHeader, models.GinAuthorPrefixKey) { + c.Abort() + return &models.BaseError{ + Code: http.StatusUnauthorized, + Msg: "Missing or invalid token", + } + } + + tokenStr := strings.TrimPrefix(authHeader, models.GinAuthorPrefixKey) + + _, err = jwtC.ParseToken(tokenStr) + if err != nil { + c.Abort() + if jwt.ErrorIsJwtExpired(err) { + return &models.BaseError{ + Code: http.StatusUnauthorized, + Msg: "Expired token", + } + } + + return &models.BaseError{ + Code: http.StatusUnauthorized, + Msg: "Invalid token", + } + } + + c.Next() + + return nil + } +} + +func GinRateLimit(rateC settings.RateLimitConfig) appHandler { + lrate, err := rate.New(rate.WithCapacity(rateC.Capacity), + rate.WithFillInterval(time.Duration(rateC.FillInterval)), + ) + + if err != nil { + panic(err) + } + + return func(c *gin.Context) error { + if lrate.Available() ==0 { + return &models.BaseError{ + Code: http.StatusServiceUnavailable, + Msg: "Exceeded rate limit", + } + } + + c.Next() + + return nil + } +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..aad9b93 --- /dev/null +++ b/routes/routes.go @@ -0,0 +1,51 @@ +package routes + +import ( + "dashboard/controller" + "dashboard/logger" + "dashboard/pkg/jwt" + "dashboard/settings" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func Setup(log *logger.Logger, rate settings.RateLimitConfig, jwtC settings.JwtConfig) *gin.Engine { + cjwt := jwt.New(jwt.WithSalt(jwtC.Salt), jwt.WithExpire(time.Duration(jwtC.Expire)*time.Second)) + + r := gin.New() + + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST"}, + AllowHeaders: []string{"Origin", "Content-Type"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + })) + + r.Use(GinLogger(log), GinRecovery(log, true), GinLog(log), errWapper(GinRateLimit(rate))) + + // 静态文件服务 + r.Static("/static", "./static") + r.StaticFile("/", "./static/index.html") + + r.GET("login", controller.LoginPage) + + r.POST("/api/login", errWapper(controller.UserSignInHandler(cjwt))) + r.POST("/api/logout", controller.UserLogOutHandler) + + g1 := r.Group("/api") + g1.Use(errWapper(GinJwtAuthor(cjwt))) + { + g1.GET("/system-info", errWapper(controller.SystemInfoHandle)) + g1.POST("/upload", errWapper(controller.FileUploadHandle)) + g1.GET("/files", errWapper(controller.FileListHandle)) + g1.GET("/download", errWapper(controller.FileDownloadHandle)) + } + + r.GET("/ws/terminal", errWapper(GinJwtAuthor(cjwt)), errWapper(controller.TerminalHandle())) + + return r +} diff --git a/routes/wrapperr.go b/routes/wrapperr.go new file mode 100644 index 0000000..22b9d00 --- /dev/null +++ b/routes/wrapperr.go @@ -0,0 +1,37 @@ +package routes + +import ( + "dashboard/models" + "dashboard/utils" + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type appHandler func(*gin.Context) error + +func errWapper(appH appHandler) gin.HandlerFunc { + return func(c *gin.Context) { + log, _ := utils.GetLogFromContext(c) + + err := appH(c) + + if err != nil { + var baseErr *models.BaseError + if errors.As(err, &baseErr) { + log.Error("Base error", zap.Any("res", baseErr)) + + c.JSON(http.StatusBadRequest, baseErr) + + return + } + + log.Error("Other error", zap.Error(err)) + c.String(http.StatusBadGateway, err.Error()) + + return + } + } +} diff --git a/settings/option.go b/settings/option.go new file mode 100644 index 0000000..9edea40 --- /dev/null +++ b/settings/option.go @@ -0,0 +1,42 @@ +package settings + +import "github.com/fsnotify/fsnotify" + +type BackHandle func(fsnotify.Event) + +type options struct { + name string + ctype string + path string + cb BackHandle +} + +type Option func(*options) + +func (o *options)repair(){ + +} + +func WithName(name string) Option { + return func(o *options) { + o.name = name + } +} + +func WithType(ctype string) Option { + return func(o *options) { + o.ctype = ctype + } +} + +func WithPath(path string) Option { + return func(o *options) { + o.path = path + } +} + +func WithCallBack(cb BackHandle) Option { + return func(o *options) { + o.cb = cb + } +} diff --git a/settings/settings.go b/settings/settings.go new file mode 100644 index 0000000..4207b56 --- /dev/null +++ b/settings/settings.go @@ -0,0 +1,59 @@ +package settings + +import ( + "fmt" + + "github.com/fsnotify/fsnotify" + "github.com/spf13/viper" +) + +type Settings struct { + *AppConfig +} + +func New(opt ...Option) *Settings { + opts := new(options) + for _, o := range opt { + o(opts) + } + + opts.repair() + + res := &Settings{ + AppConfig: new(AppConfig), + } + + mviper := viper.New() + if opts.name != "" && opts.path != "" && opts.ctype != "" { + mviper.SetConfigName(opts.name) + mviper.AddConfigPath(opts.path) + mviper.SetConfigType(opts.ctype) + } else { + mviper.SetConfigFile(opts.name) + } + + if err := mviper.ReadInConfig(); err != nil { + fmt.Println(err) + return nil + } + + if err := mviper.Unmarshal(&res.AppConfig); err != nil { + fmt.Println(err) + return nil + } + + mviper.WatchConfig() + mviper.OnConfigChange(func(in fsnotify.Event) { + if err := mviper.Unmarshal(res.AppConfig); err != nil { + fmt.Println(err) + } + + fmt.Println("config change") + + if opts.cb != nil { + opts.cb(in) + } + }) + + return res +} diff --git a/settings/type.go b/settings/type.go new file mode 100644 index 0000000..c53ec48 --- /dev/null +++ b/settings/type.go @@ -0,0 +1,40 @@ +package settings + +type AppConfig struct { + *BaseConfig `mapstructure:"base"` + *LogConfig `mapstructure:"log"` + *SnowflakeConfig `mapstructure:"snowflake"` + *RateLimitConfig `mapstructure:"rate"` + *JwtConfig `mapstructure:"jwt"` +} + +type BaseConfig struct { + Port int `mapstructure:"port"` + Name string `mapstructure:"name"` + Mode string `mapstructure:"mode"` + Version string `mapstructure:"version"` +} + +type LogConfig struct { + Level string `mapstructure:"level"` + Filename string `mapstructure:"filename"` + MaxSize int `mapstructure:"max_size"` + MaxAge int `mapstructure:"max_age"` + MaxBackUps int `mapstructure:"max_backups"` +} + +type SnowflakeConfig struct { + StartTime string `mapstructure:"start_time"` + MachineId int64 `mapstructure:"machine_id"` +} + +type RateLimitConfig struct { + FillInterval int64 `mapstructure:"fill_interval"` + Capacity int64 `mapstructure:"capacity"` + MaxWait int64 `mapstructure:"max_wait"` +} + +type JwtConfig struct { + Salt string `mapstructure:"salt"` + Expire int64 `mapstructure:"expire"` +} diff --git a/static/index.html b/static/index.html new file mode 100755 index 0000000..ed989ba --- /dev/null +++ b/static/index.html @@ -0,0 +1,394 @@ + + + + + + Linux 系统运维监控 + + + + + + +
+

Linux 系统运维监控

+ + + +
+
+

CPU 使用率

+

0%

+
+
+
+

内存使用率

+

0%

+
+
+
+

磁盘使用率

+

0%

+
+
+
+

系统负载

+

0

+
+
+
+ + +
+

上传文件

+
+ + +
+
+ + +
+ + + +
+ + +
+

浏览文件

+
+ + + + +
+
+ + + + + + + + + + + +
名称类型大小修改时间操作
+
+ +
+ + +
+

交互式终端

+
+
+
+ + + + \ No newline at end of file diff --git a/static/login.html b/static/login.html new file mode 100755 index 0000000..6f99bd3 --- /dev/null +++ b/static/login.html @@ -0,0 +1,52 @@ + + + + + + 登录 - Linux 系统运维监控 + + + +
+

登录

+
+
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/utils/gin.go b/utils/gin.go new file mode 100644 index 0000000..42b1022 --- /dev/null +++ b/utils/gin.go @@ -0,0 +1,22 @@ +package utils + +import ( + "dashboard/logger" + "dashboard/models" + + "github.com/gin-gonic/gin" +) + +func GetLogFromContext(c *gin.Context) (*logger.Logger, error) { + res, ok := c.Get(models.GinContxtLog) + if !ok { + return nil, models.ErrorInvalidData + } + + log, ok := res.(*logger.Logger) + if !ok { + return nil, models.ErrorInvalidData + } + + return log, nil +}