3 次代碼提交 5155b0fe03 ... a65a3f1a47

作者 SHA1 備註 提交日期
  tuonian a65a3f1a47 feat: 增加Agent代理服务-70% 2 天之前
  tuonian 2b3c58a0a3 feat: 增加Agent代理服务-60% 2 天之前
  tuonian 111d8538d5 feat: 增加Agent代理服务 2 天之前
共有 54 個文件被更改,包括 1769 次插入56 次删除
  1. 1 0
      .gitignore
  2. 41 0
      agent.go
  3. 268 0
      agent/agent.go
  4. 142 0
      agent/handler.go
  5. 1 0
      frontend/dist/assets/index-BDKWdQ_l.js
  6. 0 0
      frontend/dist/assets/index-CdH8Zg1S.css
  7. 0 1
      frontend/dist/assets/index-DxsAmTEk.js
  8. 0 0
      frontend/dist/assets/index-jCNbVT1V.css
  9. 1 1
      frontend/dist/cdn/rich.js
  10. 2 2
      frontend/dist/index.html
  11. 21 0
      frontend/src/api/user.ts
  12. 1 0
      frontend/src/components/curd/index.tsx
  13. 32 0
      frontend/src/components/form/date/index.tsx
  14. 4 0
      frontend/src/models/nginx.ts
  15. 1 1
      frontend/src/pages/nginx/components/StopStartButton.tsx
  16. 6 1
      frontend/src/pages/nginx/list.tsx
  17. 0 0
      frontend/src/pages/user/token/index.less
  18. 139 0
      frontend/src/pages/user/token/index.tsx
  19. 6 0
      frontend/src/routes/routes.tsx
  20. 12 2
      go.mod
  21. 113 0
      go.sum
  22. 1 2
      server/base/controller.go
  23. 29 0
      server/base/resp.go
  24. 0 3
      server/config/constants.go
  25. 6 0
      server/constants/constants.go
  26. 1 0
      server/db/db.go
  27. 4 0
      server/init/init.go
  28. 15 0
      server/init/sql.go
  29. 37 15
      server/middleware/auth.go
  30. 39 0
      server/models/agent.go
  31. 30 0
      server/models/auth.go
  32. 5 0
      server/models/nginx.go
  33. 61 0
      server/modules/agent_server/hub.go
  34. 48 0
      server/modules/agent_server/server.go
  35. 143 0
      server/modules/agent_server/ws_client.go
  36. 115 0
      server/modules/auth_token/controller.go
  37. 104 0
      server/modules/auth_token/service.go
  38. 3 3
      server/modules/ldap/server_controller.go
  39. 4 4
      server/modules/ldap/server_service.go
  40. 4 4
      server/modules/ldap/user_service.go
  41. 1 2
      server/modules/nginx/nginx_controller/base.go
  42. 20 0
      server/modules/nginx/nginx_controller/file.go
  43. 7 4
      server/modules/nginx/nginx_controller/nginx.go
  44. 47 0
      server/modules/nginx/nginx_service/handler.go
  45. 82 4
      server/modules/nginx/nginx_service/nginx.go
  46. 1 1
      server/modules/proxy/websocket.go
  47. 4 4
      server/modules/user/service.go
  48. 94 0
      server/nginx/agent.go
  49. 4 0
      server/nginx/local.go
  50. 10 2
      server/nginx/manager.go
  51. 14 0
      server/routers/router.go
  52. 28 0
      server/utils/ip.go
  53. 11 0
      server/utils/ip_test.go
  54. 6 0
      server/utils/snow.go

+ 1 - 0
.gitignore

@@ -37,6 +37,7 @@ conf/app.local.conf
 server/data/sessions
 
 /data/sessions
+/data/cache
 /conf/app.local.conf
 /build/
 

+ 41 - 0
agent.go

@@ -0,0 +1,41 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"nginx-ui/agent"
+	"nginx-ui/server/models"
+	"os"
+)
+
+// 代理服务
+func main() {
+
+	serverUrl := flag.String("server", "127.0.0.1:8080/nginx-ui/api", "agent host and path ,eg:127.0.0.1:8080/nginx-ui/api")
+	ssl := flag.String("ssl", "N", "use ssl,https or wss,Y or N")
+	token := flag.String("token", "", "token")
+	flag.Parse()
+	envUrl := os.Getenv("SERVER_URL")
+	envSSL := os.Getenv("SSL")
+	envToken := os.Getenv("TOKEN")
+	if len(envUrl) > 0 {
+		serverUrl = &envUrl
+	}
+	if len(envSSL) > 0 {
+		ssl = &envSSL
+	}
+	if len(envToken) > 0 {
+		token = &envToken
+	}
+	log.Printf("agent server url: %s", *serverUrl)
+	c := agent.NewAgent(*serverUrl, *ssl, *token)
+	c.SetMessageHandler(models.NginxUpdateType, agent.OnNginxUpdated)
+	c.SetMessageHandler(models.AgentCmdType, agent.OnCMD)
+	c.SetMessageHandler(models.SendFileType, agent.OnSendFile)
+	c.SetMessageHandler(models.ServerConnected, agent.OnServerConnected)
+	err := c.Run()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+}

+ 268 - 0
agent/agent.go

@@ -0,0 +1,268 @@
+package agent
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"log"
+	"nginx-ui/server/constants"
+	"nginx-ui/server/models"
+	"nginx-ui/server/utils"
+	"sync"
+	"time"
+)
+
+const (
+	// Time allowed to write a message to the peer.
+	writeWait = 12 * time.Second
+	readWait  = 12 * time.Second
+
+	// Send pings to peer with this period. Must be less than pongWait.
+	// 心跳必须要小于writeWait ,且每次发送心跳都需要重新设置,否则会导致连接断开
+	pingPeriod = 5 * time.Second
+
+	// Maximum message size allowed from peer.
+	maxMessageSize = 1024
+)
+
+var mutex = &sync.Mutex{}
+
+// Agent 客户端的代码
+// - SSL Y or N
+type Agent struct {
+	Url               string `json:"url"`
+	Token             string `json:"token"`
+	SSL               string `json:"ssl"`
+	dialer            *websocket.Dialer
+	ReconnectInterval time.Duration
+	Ctx               context.Context
+	Cancel            context.CancelFunc
+	conn              *websocket.Conn
+	errChan           chan error
+	callbacks         map[string]chan *models.AgentData
+	handlers          map[string]func(agent *Agent, message *models.AgentData) (interface{}, error)
+	Nginx             *models.Nginx
+}
+
+func NewAgent(url string, ssl string, token string) *Agent {
+	ctx, cancel := context.WithCancel(context.Background())
+	return &Agent{
+		Url:               url,
+		Token:             token,
+		SSL:               ssl,
+		dialer:            &websocket.Dialer{ReadBufferSize: 1024, WriteBufferSize: 1024},
+		ReconnectInterval: 10 * time.Second,
+		Ctx:               ctx,
+		Cancel:            cancel,
+		errChan:           make(chan error, 3),
+		callbacks:         make(map[string]chan *models.AgentData),
+		handlers:          make(map[string]func(agent *Agent, message *models.AgentData) (interface{}, error)),
+	}
+}
+
+func (a *Agent) SetMessageHandler(key string, handler func(agent *Agent, message *models.AgentData) (interface{}, error)) {
+	a.handlers[key] = handler
+}
+
+func (a *Agent) Reset() {
+	mutex.Lock()
+	defer mutex.Unlock()
+	for k, _ := range a.callbacks {
+		delete(a.callbacks, k)
+	}
+	a.Cancel() // 通知协程退出
+	log.Printf("reset")
+}
+
+func (a *Agent) setCallback(key string, c chan *models.AgentData) {
+	mutex.Lock()
+	defer mutex.Unlock()
+	a.callbacks[key] = c
+}
+
+func (a *Agent) removeCallback(key string) {
+	mutex.Lock()
+	defer mutex.Unlock()
+	delete(a.callbacks, key)
+}
+
+func (a *Agent) getCallback(key string) chan *models.AgentData {
+	mutex.Lock()
+	defer mutex.Unlock()
+	ret, ok := a.callbacks[key]
+	if ok {
+		return ret
+	}
+	return nil
+}
+
+func (a *Agent) read() {
+	log.Printf("read start")
+	defer func() {
+		log.Printf("read closed")
+	}()
+	for {
+		select {
+		case <-a.Ctx.Done():
+			return
+		default:
+			data := &models.AgentData{}
+			err := a.conn.ReadJSON(data)
+			if err != nil {
+				log.Printf("parse receive message fail: %v", err)
+				a.errChan <- err
+				break
+			}
+			log.Printf("agent: recv: %v", data)
+			callback := a.getCallback(data.RequestId)
+			if callback != nil {
+				callback <- data
+				continue
+			}
+			handler, ok := a.handlers[data.Type]
+			if !ok {
+				log.Printf("message type: %v has not handler", data.Type)
+				continue
+			}
+			go func() {
+				result, err := handler(a, data)
+				log.Printf("agent: handler result: %v, err: %v", result, err)
+				if err != nil {
+					data.Data = make([]byte, 0)
+					data.Success = false
+					data.Msg = err.Error()
+				} else {
+					data.Data = result
+					data.Success = true
+					data.Msg = ""
+				}
+				a.JustSend(data)
+			}()
+		}
+	}
+}
+
+func (a *Agent) ping() {
+	log.Printf("ping start")
+	ticker := time.NewTicker(pingPeriod)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-a.Ctx.Done():
+			return
+		case <-ticker.C:
+			if a.conn == nil {
+				continue
+			}
+			_ = a.conn.SetWriteDeadline(time.Now().Add(writeWait))
+			err := a.conn.WriteMessage(websocket.PingMessage, nil)
+			log.Printf("ping: %v", err)
+			if err != nil {
+				a.errChan <- err
+				return
+			}
+		}
+	}
+}
+
+func (a *Agent) Run() error {
+	protocol := "ws"
+	if a.SSL == "Y" {
+		protocol = "wss"
+	}
+	url := fmt.Sprintf("%v://%v%v", protocol, a.Url, constants.AgentConnectUrl)
+	for {
+		conn, _, err := a.dialer.Dial(url, map[string][]string{"token": {a.Token}})
+		if err != nil {
+			log.Printf("agent: failed to connect to %s: %s\n", a.Url, err)
+			time.Sleep(a.ReconnectInterval)
+			continue
+		}
+		log.Printf("agent: connected to %s\n", a.Url)
+
+		conn.SetReadLimit(maxMessageSize)
+		_ = conn.SetReadDeadline(time.Now().Add(readWait))
+		conn.SetPongHandler(func(string) error {
+			log.Printf("pong")
+			_ = conn.SetReadDeadline(time.Now().Add(readWait))
+			return nil
+		})
+		a.conn = conn
+		go a.ping()
+		go a.read()
+
+		handler := a.handlers[models.ServerConnected]
+		if handler != nil {
+			go func() {
+				_, _ = handler(a, &models.AgentData{
+					Type: models.ServerConnected,
+				})
+			}()
+		}
+		log.Printf("agent: waiting for closed")
+		// 等待错误或连接关闭
+		select {
+		case err := <-a.errChan:
+			log.Println("连接异常:", err)
+			a.Reset()
+			conn.Close() // 关闭当前连接
+			log.Println("连接已关闭,5秒后重连...")
+			time.Sleep(a.ReconnectInterval)
+		}
+	}
+}
+
+/*
+Send 发送消息
+参数:
+
+	message - 消息体
+	result	- 返回结构体指针
+	timeout - 超时时间
+
+返回值:
+
+	error - 错误
+*/
+func (a *Agent) Send(data *models.AgentData, timeout time.Duration) (*models.AgentData, error) {
+	var rec = make(chan *models.AgentData)
+	if len(data.RequestId) == 0 {
+		data.RequestId = utils.GenerateUUID()
+	}
+	requestId := data.RequestId
+	a.setCallback(requestId, rec)
+	timer := time.NewTimer(timeout)
+	defer func() {
+		timer.Stop()
+		a.removeCallback(requestId)
+		log.Printf("release send chan")
+		close(rec)
+	}()
+	err := a.conn.WriteJSON(data)
+	if err != nil {
+		log.Printf("write message fail: %v", err)
+		return nil, err
+	}
+	log.Printf("Send: %v", data)
+	for {
+		select {
+		case <-a.Ctx.Done():
+			return nil, errors.New("timeout")
+		case res, ok := <-rec:
+			if !ok {
+				return nil, errors.New("receive data fail")
+			}
+			return res, err
+
+		case <-timer.C:
+			return nil, errors.New("timeout")
+		}
+	}
+}
+
+// JustSend 仅发送,不管是否成功,用于消息回复
+func (a *Agent) JustSend(message interface{}) {
+	err := a.conn.WriteJSON(message)
+	log.Printf("JustSend: %v", err)
+}

+ 142 - 0
agent/handler.go

@@ -0,0 +1,142 @@
+package agent
+
+import (
+	"errors"
+	"fmt"
+	"github.com/labstack/gommon/log"
+	"io"
+	"net/http"
+	"nginx-ui/server/constants"
+	"nginx-ui/server/models"
+	nginx2 "nginx-ui/server/nginx"
+	"nginx-ui/server/utils"
+	"os"
+	"strings"
+	"time"
+)
+
+var OnAgentMessageHandler func(agent *Agent, message *models.AgentData) (interface{}, error)
+
+// OnNginxUpdated 不管是Agent注册,还是服务端修改Nginx信息,均会调用该方法
+func OnNginxUpdated(c *Agent, data *models.AgentData) (interface{}, error) {
+	nginx := &models.Nginx{}
+	err := data.ReadData(nginx)
+	if err != nil {
+		return nil, err
+	}
+	c.Nginx = nginx
+	return make(map[string]interface{}), nil
+
+}
+
+func OnServerConnected(c *Agent, data *models.AgentData) (interface{}, error) {
+	fmt.Printf("OnServerConnected: %v,%v\n", c.Token, data)
+	if c.Token == "" {
+		log.Warn("missing token")
+		return nil, nil
+	}
+	nginx := &models.Nginx{
+		Name:      "Agent",
+		IsServer:  false,
+		Proxy:     true,
+		IsLocal:   true,
+		Password:  "",
+		Remark:    "agent",
+		IpAddr:    utils.GetNetIP(),
+		DataDir:   "/data/nginx",
+		NginxPath: "/usr/sbin/nginx",
+		NginxDir:  "/etc/nginx",
+	}
+	request := &models.AgentData{
+		Type: models.RegisterNginxType,
+		Data: nginx,
+	}
+	res, err := c.Send(request, 20*time.Second)
+	if err != nil {
+		log.Warn("register fail for: %v\n", err)
+		return nil, nil
+	}
+	if res != nil {
+		err = res.ReadData(nginx)
+		if err != nil {
+			log.Warn("register fail: %v\n", err)
+			return nil, nil
+		}
+		c.Nginx = nginx
+	}
+	return nil, nil
+}
+
+// OnCMD 执行Nginx命令
+func OnCMD(c *Agent, data *models.AgentData) (interface{}, error) {
+	nginx := c.Nginx
+	if nginx == nil {
+		go func() {
+			_, _ = OnServerConnected(c, data)
+		}()
+		return nil, errors.New("nginx agent not register")
+	}
+	request := &models.AgentCMD{}
+	err := data.ReadData(request)
+	if err != nil {
+		return nil, err
+	}
+
+	instance := nginx2.NewLocalNginx(nginx)
+	res, err := instance.Run(request.Cmd)
+	return res, err
+}
+
+// OnSendFile 服务端复制文件到客户端
+// 先使用Http接口下载文件,然后本地解压复制
+func OnSendFile(c *Agent, data *models.AgentData) (interface{}, error) {
+	nginx := c.Nginx
+	if nginx == nil {
+		go func() {
+			_, _ = OnServerConnected(c, data)
+		}()
+		return nil, errors.New("nginx agent not register")
+	}
+	request := &models.AgentSendFile{}
+	err := data.ReadData(request)
+	if err != nil {
+		log.Warn("send file fail for: %v\n", nginx)
+		return nil, err
+	}
+
+	protocol := "http"
+	if c.SSL == "Y" {
+		protocol = "https"
+	}
+	url := fmt.Sprintf("%v://%v%v", protocol, c.Url, constants.DownloadFileUrl)
+	url = strings.Replace(url, ":nginxId", fmt.Sprintf("%d", nginx.Id), 1)
+	url = strings.Replace(url, ":fileName", request.FileName, 1)
+	log.Printf("download file: %s\n", url)
+
+	out, err := os.Create(request.Dst)
+	if err != nil {
+		log.Warn("create file fail for: %v\n", nginx)
+		return nil, err
+	}
+	defer out.Close()
+
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		log.Warn("create file fail for: %v\n", nginx)
+		return nil, err
+	}
+	req.Header.Set("Token", c.Token)
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		log.Warn("download file fail for: %v\n", nginx)
+		return nil, err
+	}
+	defer resp.Body.Close()
+	_, err = io.Copy(out, resp.Body)
+	if err != nil {
+		log.Warn("download file fail for: %v\n", nginx)
+		return nil, err
+	}
+	return make(map[string]interface{}), nil
+}

File diff suppressed because it is too large
+ 1 - 0
frontend/dist/assets/index-BDKWdQ_l.js


File diff suppressed because it is too large
+ 0 - 0
frontend/dist/assets/index-CdH8Zg1S.css


File diff suppressed because it is too large
+ 0 - 1
frontend/dist/assets/index-DxsAmTEk.js


File diff suppressed because it is too large
+ 0 - 0
frontend/dist/assets/index-jCNbVT1V.css


+ 1 - 1
frontend/dist/cdn/rich.js

@@ -1,2 +1,2 @@
-import{a as f,R as w,w as D}from"./react-all-18.2.0.js";import"./ace-builds-1.23.0.js";import{j as S}from"../assets/index-DxsAmTEk.js";var b=function(t){var e;return typeof t=="string"||!t||isNaN(t)?e=t:e="".concat(t,"px"),e};function j(r,t){return x(r)||E(r,t)||T(r,t)||I()}function I(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
+import{a as f,R as w,w as D}from"./react-all-18.2.0.js";import"./ace-builds-1.23.0.js";import{j as S}from"../assets/index-BDKWdQ_l.js";var b=function(t){var e;return typeof t=="string"||!t||isNaN(t)?e=t:e="".concat(t,"px"),e};function j(r,t){return x(r)||E(r,t)||T(r,t)||I()}function I(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
 In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function T(r,t){if(r){if(typeof r=="string")return O(r,t);var e=Object.prototype.toString.call(r).slice(8,-1);if(e==="Object"&&r.constructor&&(e=r.constructor.name),e==="Map"||e==="Set")return Array.from(r);if(e==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return O(r,t)}}function O(r,t){(t==null||t>r.length)&&(t=r.length);for(var e=0,o=new Array(t);e<t;e++)o[e]=r[e];return o}function E(r,t){var e=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(e!=null){var o,i,u,l,n=[],a=!0,s=!1;try{if(u=(e=e.call(r)).next,t!==0)for(;!(a=(o=u.call(e)).done)&&(n.push(o.value),n.length!==t);a=!0);}catch(c){s=!0,i=c}finally{try{if(!a&&e.return!=null&&(l=e.return(),Object(l)!==l))return}finally{if(s)throw i}}return n}}function x(r){if(Array.isArray(r))return r}var L=function(t,e){return!e||!t.saveAsObj?e:t.mode==="json"?JSON.parse(e):t.mode==="yaml"||t.mode==="yml"?S.load(e):e},M=function(t,e){return typeof e=="string"||!e?e:t.mode==="json"?JSON.stringify(e,null,2):t.mode==="yaml"||t.mode==="yml"?S.dump(e):e};const k=function(r){var t=r.value,e=r.className,o=e===void 0?"":e,i=r.column,u=r.onChange,l=r.readonly,n=i,a=n.mode;a==="yml"?a="yaml":a||(a="json");var s=f.useState(),c=j(s,2),A=c[0],y=c[1],C=f.useState(""),h=j(C,2),N=h[0],p=h[1],m=f.useRef(),R=function(v){y(v);try{var g=L(n,v);m.current=g,u==null||u(g),p("")}catch{p("has-err")}};return f.useEffect(function(){if(!(m.current&&t===m.current))try{y(M(n,t))}catch(d){console.warn("fromObjData: ",d)}},[t]),w.createElement(D,{mode:a,value:A,showPrintMargin:n.printMargin,showGutter:!n.hiddenLines,highlightActiveLine:!0,theme:"chrome",height:b(i.height||120),width:b(i.width)||"80%",onChange:R,readOnly:!!i.disabled||l,setOptions:n.options,className:"rich-input json-input ".concat(o," ").concat(N),placeholder:n.placeholder})};export{k as default,M as fromObjData,L as toObjData};

+ 2 - 2
frontend/dist/index.html

@@ -6,10 +6,10 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>NginxUI</title>
     <script type="application/javascript" src="/nginx-ui/config.js"></script>
-    <script type="module" crossorigin src="/nginx-ui/assets/index-DxsAmTEk.js"></script>
+    <script type="module" crossorigin src="/nginx-ui/assets/index-BDKWdQ_l.js"></script>
     <link rel="modulepreload" crossorigin href="/nginx-ui/cdn/ace-builds-1.23.0.js">
     <link rel="modulepreload" crossorigin href="/nginx-ui/cdn/react-all-18.2.0.js">
-    <link rel="stylesheet" crossorigin href="/nginx-ui/assets/index-jCNbVT1V.css">
+    <link rel="stylesheet" crossorigin href="/nginx-ui/assets/index-CdH8Zg1S.css">
   </head>
   <body>
     <div id="nginx_ui_root"></div>


+ 21 - 0
frontend/src/api/user.ts

@@ -34,6 +34,17 @@ export namespace User {
         refCount: number
         enable: boolean
     }
+
+    export type AuthToken = {
+        id: number
+        name: string
+        uid: number
+        clientIps?: string
+        token: string
+        expiredAt: number
+        enabled: boolean
+        remark: string
+    }
 }
 
 /**
@@ -85,3 +96,13 @@ export const userApis = {
         return request.post<BaseResp<void>>(`/role/remove`, {id})
     },
 }
+
+
+export const userTokenApis = {
+    getDetails: (id : number) => request2.get<User.AuthToken>(`/user-token/detail`, { params: { id}}),
+    create: (data: Partial<User.AuthToken>) => request2.post<User.AuthToken>(`/auth-token/create`, data),
+    getList: (req:PageReq) => request2.post<PageData<User.AuthToken>>('/auth-token/list', req),
+    update: (user: Partial<User.AuthToken>) => request2.post<User.AuthToken>(`/auth-token/update`, user),
+    remove: (id: number) => request2.post<any>(`/auth-token/remove`, {id}),
+
+}

+ 1 - 0
frontend/src/components/curd/index.tsx

@@ -218,6 +218,7 @@ export function CurdPage<T extends ICurdData>(
                            cell: Cell,
                        }
                    }}
+                   size="middle"
                    expandable={expandable}
                    bordered={config?.bordered}
                    rowKey={config?.rowKey ?? 'id'}

+ 32 - 0
frontend/src/components/form/date/index.tsx

@@ -0,0 +1,32 @@
+import React, {useMemo} from "react";
+import {AutoTypeInputProps} from "auto-antd/dist/esm/Components";
+import {DatePicker} from "antd";
+import moment from "moment";
+
+export const DateInput: React.FC<AutoTypeInputProps> = (
+    {
+        value, onChange
+    }) => {
+
+
+    const dateValue = useMemo(() => {
+        if (!value) return null;
+        console.log('v....',value)
+        return moment(value * 1000);
+    }, [value]);
+
+    const onValueChange = (date: moment.Moment | null) => {
+        if (date) {
+            const unix = date.unix()
+            onChange?.(unix);
+        }else {
+            onChange?.(undefined);
+        }
+    }
+
+    return (<DatePicker
+        onChange={onValueChange}
+        value={dateValue}
+        format="YYYY-MM-DD"
+    />)
+}

+ 4 - 0
frontend/src/models/nginx.ts

@@ -19,6 +19,10 @@ export type INginx = {
    */
   nginxPath?:string
   isLocal: boolean
+  /**
+   * Agent代理
+   */
+  proxy?:boolean
   ipAddr: string
   port: number
   user: string

+ 1 - 1
frontend/src/pages/nginx/components/StopStartButton.tsx

@@ -75,7 +75,7 @@ export const StopStartButton = () => {
 
     useEffect(()=>{
         fetchStatus()
-    },[])
+    },[nginx])
 
     if (!nginx){
         return null

+ 6 - 1
frontend/src/pages/nginx/list.tsx

@@ -121,7 +121,12 @@ export const NginxList = ()=>{
     {
       dataIndex: 'isLocal',
       title: '实例类型',
-      render: value => value ? '本地实例':<Tag color="orange">远程实例</Tag>
+      render: (value,record) => {
+        if (record.proxy){
+          return (<Tag color="blue">Agent</Tag>)
+        }
+        return value ? '本地实例':<Tag color="orange">远程实例</Tag>
+      }
     },
     {
       dataIndex: 'ipAddr',

+ 0 - 0
frontend/src/pages/user/token/index.less


+ 139 - 0
frontend/src/pages/user/token/index.tsx

@@ -0,0 +1,139 @@
+import {useCallback, useMemo, useState} from "react";
+import {Alert} from "antd";
+import './index.less'
+import {CurdColumn, CurdPage} from "../../../components/curd";
+import {ICurdConfig} from "../../../components/curd/types.ts";
+import {PageData} from "../../../models/api.ts";
+import {User, userTokenApis} from "../../../api/user.ts";
+import {DateInput} from "../../../components/form/date";
+import moment from "moment";
+
+const columns: CurdColumn[] = [
+    {
+        key: 'id',
+        title: 'ID',
+        type: 'number',
+        required: true,
+        width: 120,
+        addable: false,
+        editable: false,
+    },
+    {
+        key: 'name',
+        title: '名称',
+        type: 'string',
+        editable: true,
+        placeholder: 'Token名称',
+        required: true,
+        addable: true,
+    },
+    {
+        key: 'token',
+        title: 'TOKEN',
+        type: 'string',
+        required: true,
+        placeholder: '静态access_token',
+        editable: false,
+        addable: false,
+    },
+    {
+        key: 'enabled',
+        title: '是否启用',
+        type: 'switch',
+        required: true,
+        width: 100
+    },
+    {
+        key: 'expiredAt',
+        title: '过期时间',
+        type: 'date',
+        renderInput: DateInput,
+        render: (value: any) => {
+            if (!value || isNaN(value)) return <span>-</span>;
+            return (
+                <span>{moment(value * 1000).format("YYYY-MM-DD")}</span>
+            )
+        }
+    },
+    {
+        key: 'remark',
+        type: 'textarea',
+        title: '备注',
+        value: '',
+        required: false,
+        rows: 4
+    }
+]
+
+const serverConfig: ICurdConfig<User.Role> = {
+    editDialogWidth: 550,
+    labelSpan: 4,
+    hideAdd: false,
+    bordered: true,
+    operationWidth: 120,
+}
+
+/**
+ * 用户列表的操作
+ * @constructor
+ */
+export const List = () => {
+
+    const [success, setSuccess] = useState('')
+
+    const getList = useCallback((query: any) => {
+
+        return userTokenApis.getList({
+            ...query,
+            uid: 0,
+        }).then(res => {
+            console.log('res', res)
+            return {
+                list: res.list,
+                total: res.total,
+                current: 1,
+                pageSize: 1000,
+            } as PageData<User.AuthToken>
+        })
+    }, [])
+
+    const getDetail = (data: Partial<User.AuthToken>) => {
+        return Promise.resolve({...data} as User.AuthToken)
+    }
+
+    const onSave = (data: Partial<User.AuthToken>) => {
+        if (data.id) {
+            return userTokenApis.update(data)
+        }
+        return userTokenApis.create(data)
+    }
+
+    const handleDelete = (record: User.AuthToken) => {
+        return userTokenApis.remove(record.id)
+    }
+
+
+    const config = useMemo(() => {
+        return {
+            ...serverConfig,
+        } as ICurdConfig<User.AuthToken>
+
+    }, [])
+
+
+    return (<>
+        {
+            success ? (<Alert type="success" message={success} style={{margin: 5}} closable={true}
+                              onClose={() => setSuccess('')}/>) : null
+        }
+        <CurdPage columns={columns}
+                  getList={getList}
+                  getDetail={getDetail}
+                  operationRender={<></>}
+                  operation={{delete: true, add: false}}
+                  onSave={onSave}
+                  onDelete={handleDelete}
+                  config={config}
+        />
+    </>)
+}

+ 6 - 0
frontend/src/routes/routes.tsx

@@ -5,6 +5,7 @@ import {NginxLayout} from "../pages/nginx/layout.tsx";
 import {ldapRoutes} from "../pages/ldap/layout.tsx";
 import {UserList} from "../pages/user/list";
 import {List as RoleList} from "../pages/user/role";
+import { List as TokenList } from '../pages/user/token'
 import {RouteObject} from "react-router/dist/lib/context";
 import {SettingPage} from "../pages/settings";
 import {ResetPassword} from "../pages/user/resetPassword";
@@ -68,6 +69,11 @@ export const routes: RouteObject[] = [
                 path: 'role',
                 Component: RoleList,
             },
+            {
+                id: 'USER_TOKEN',
+                path: 'token',
+                Component: TokenList,
+            }
         ]
     },
     {

+ 12 - 2
go.mod

@@ -34,15 +34,16 @@ require (
 
 // replace github.com/wailsapp/wails/v2 v2.6.0 => D:\CacheData\go\pkg\mod
 
-require github.com/astaxie/beego v1.12.1
+require github.com/astaxie/beego v1.12.3
 
 require (
 	github.com/beego/beego/v2 v2.1.0
 	github.com/go-ldap/ldap/v3 v3.4.8
 	github.com/go-sql-driver/mysql v1.7.0
 	github.com/hashicorp/go-uuid v1.0.3
-	github.com/mattn/go-sqlite3 v1.14.17
+	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/mholt/archiver/v4 v4.0.0-alpha.8
+	github.com/mitchellh/mapstructure v1.5.0
 	github.com/pkg/sftp v1.13.5
 	github.com/smartystreets/goconvey v1.6.4
 	golang.org/x/oauth2 v0.10.0
@@ -52,9 +53,11 @@ require (
 require (
 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
 	github.com/andybalholm/brotli v1.0.4 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bodgit/plumbing v1.2.0 // indirect
 	github.com/bodgit/sevenzip v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.0 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/connesc/cipherio v0.2.1 // indirect
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
@@ -63,15 +66,22 @@ require (
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
+	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/golang-lru v0.5.4 // indirect
 	github.com/jtolds/gls v4.20.0+incompatible // indirect
 	github.com/klauspost/compress v1.15.9 // indirect
 	github.com/klauspost/pgzip v1.2.5 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/leaanthony/u v1.1.0 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
 	github.com/pierrec/lz4/v4 v4.1.15 // indirect
+	github.com/prometheus/client_golang v1.15.1 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
+	github.com/prometheus/procfs v0.9.0 // indirect
 	github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
 	github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
 	github.com/therootcompany/xz v1.0.1 // indirect

+ 113 - 0
go.sum

@@ -21,16 +21,28 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
 github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/astaxie/beego v1.12.1 h1:dfpuoxpzLVgclveAXe4PyNKqkzgm5zF4tgF2B3kkM2I=
 github.com/astaxie/beego v1.12.1/go.mod h1:kPBWpSANNbSdIqOc8SUL9h+1oyBMZhROeYsXQDbidWQ=
+github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
+github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
 github.com/beego/beego/v2 v2.1.0 h1:Lk0FtQGvDQCx5V5yEu4XwDsIgt+QOlNjt5emUa3/ZmA=
 github.com/beego/beego/v2 v2.1.0/go.mod h1:6h36ISpaxNrrpJ27siTpXBG8d/Icjzsc7pU1bWpp0EE=
 github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
 github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
 github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
 github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
@@ -42,6 +54,9 @@ github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU
 github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
 github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -50,7 +65,9 @@ github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h
 github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
 github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
 github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
+github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
 github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
+github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
 github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
 github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -60,26 +77,34 @@ github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
 github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
 github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
 github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
 github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
 github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
 github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 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-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
 github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -97,9 +122,17 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -113,6 +146,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -126,6 +160,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+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/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -135,6 +171,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
 github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -152,10 +190,13 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
 github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
 github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
@@ -163,8 +204,10 @@ github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHU
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -185,6 +228,7 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
 github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
 github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
 github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
 github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -200,28 +244,66 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
 github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
 github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
 github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
 github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
 github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
 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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
+github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
 github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -229,19 +311,27 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
 github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
 github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
 github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 h1:v9ezJDHA1XGxViAUSIoO/Id7Fl63u6d0YmsAm+/p2hs=
 github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02/go.mod h1:RF16/A3L0xSa0oSERcnhd8Pu3IXSDZSK2gmGIMsttFE=
+github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
 github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
+github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
 github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
 github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 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=
@@ -249,11 +339,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
 github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
 github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
 github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
 github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -270,12 +362,14 @@ github.com/wailsapp/wails/v2 v2.9.1 h1:irsXnoQrCpeKzKTYZ2SUVlRRyeMR6I0vCO9Q1cvlE
 github.com/wailsapp/wails/v2 v2.9.1/go.mod h1:7maJV2h+Egl11Ak8QZN/jlGLj2wg05bsQS+ywJPT0gI=
 github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
 go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -319,6 +413,7 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -326,6 +421,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -360,10 +456,13 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -371,10 +470,13 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -482,26 +584,37 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
 google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=

+ 1 - 2
server/base/controller.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"github.com/astaxie/beego"
 	"github.com/astaxie/beego/logs"
-	"nginx-ui/server/middleware"
 	"nginx-ui/server/models"
 	"strconv"
 )
@@ -103,7 +102,7 @@ func (c *Controller) GetIntQuery(k string) (int, error) {
 func (c *Controller) GetUser(required bool) *models.User {
 	data := c.GetSession("user")
 	if data == nil && required {
-		middleware.WriteForbidden(c.Ctx.ResponseWriter)
+		WriteForbidden(c.Ctx.ResponseWriter)
 		return nil
 	}
 	if data == nil {

+ 29 - 0
server/base/resp.go

@@ -0,0 +1,29 @@
+package base
+
+import (
+	"fmt"
+	"github.com/astaxie/beego/logs"
+	"net/http"
+)
+
+var UnauthorizedResp = `{"code": 401, "msg":"未登录或者登录已过期!"}`
+
+func WriteForbidden(w http.ResponseWriter) {
+	w.WriteHeader(401)
+	w.Header().Set("Content-Type", "application/json")
+	_, err := w.Write([]byte(UnauthorizedResp))
+	if err != nil {
+		logs.Warn("writeForbidden write error", err)
+		return
+	}
+}
+
+func WriteError(w http.ResponseWriter, err error) {
+	w.WriteHeader(500)
+	w.Header().Set("Content-Type", "application/json")
+	resp := fmt.Sprintf("{\"code\": 500, \"msg\":\"%s\"}", err.Error())
+	_, err = w.Write([]byte(resp))
+	if err != nil {
+		logs.Warn("write write error", err)
+	}
+}

+ 0 - 3
server/config/constants.go

@@ -1,3 +0,0 @@
-package config
-
-const ReplacePassword = "******"

+ 6 - 0
server/constants/constants.go

@@ -0,0 +1,6 @@
+package constants
+
+const ReplacePassword = "******"
+
+var DownloadFileUrl = "/file/:nginxId/file/:fileName"
+var AgentConnectUrl = "/agent/connect"

+ 1 - 0
server/db/db.go

@@ -47,6 +47,7 @@ func Init() {
 	orm.RegisterModel(new(models.Setting))
 	orm.RegisterModel(new(models.SettingRoute))
 	orm.RegisterModel(new(models.ProxyEntity))
+	orm.RegisterModel(new(models.AuthToken))
 
 	orm.RunSyncdb("default", false, true)
 

+ 4 - 0
server/init/init.go

@@ -7,6 +7,8 @@ import (
 	"nginx-ui/server/config"
 	"nginx-ui/server/db"
 	"nginx-ui/server/models"
+	"nginx-ui/server/modules/agent_server"
+	"nginx-ui/server/modules/nginx/nginx_service"
 	"nginx-ui/server/modules/proxy"
 	_ "nginx-ui/server/routers"
 	"nginx-ui/server/utils"
@@ -30,4 +32,6 @@ func init() {
 	ensureRoutes()
 	ensureIndexHtml()
 	proxy.Instance.RefreshProxies()
+
+	agent_server.SetMessageHandler(models.RegisterNginxType, nginx_service.AgentRegister)
 }

+ 15 - 0
server/init/sql.go

@@ -143,6 +143,21 @@ func ensureRoutes() {
 			Deleted: false,
 			SortNum: 3,
 		},
+		{
+			Id:      "USER_TOKEN",
+			Path:    "token",
+			Index:   false,
+			Pid:     "USER_MANAGER",
+			Uid:     0,
+			Roles:   "",
+			Type:    "MENU",
+			Target:  "TAB",
+			Title:   "授权管理",
+			Brief:   "授权管理",
+			NavLink: "",
+			Deleted: false,
+			SortNum: 4,
+		},
 		{
 			Id:      "SETTING_ID",
 			Path:    "settings",

+ 37 - 15
server/middleware/auth.go

@@ -7,9 +7,11 @@ import (
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/session"
 	"github.com/beego/beego/v2/client/httplib"
-	"net/http"
+	"log"
+	"nginx-ui/server/base"
 	"nginx-ui/server/config"
 	"nginx-ui/server/models"
+	"nginx-ui/server/modules/auth_token"
 	"strings"
 )
 
@@ -25,8 +27,6 @@ var whitelist = map[string]bool{
 	"/wechat/webhook/gitlab": true,
 }
 
-var UnauthorizedResp = `{"code": 401, "msg":"未登录或者登录已过期!"}`
-
 func init() {
 	beego.BConfig.WebConfig.Session.SessionAutoSetCookie = true
 }
@@ -58,6 +58,27 @@ func checkThirdSession(ctx *context.Context, sess session.Store) {
 	logs.Debug("check third session ok ", user)
 }
 
+func checkAuthToken(ctx *context.Context, sess session.Store, path string) (bool, error) {
+	c := sess.Get("client")
+	if c != nil {
+		_, ok := c.(models.AuthToken)
+		if ok {
+			return true, nil
+		}
+	}
+	token := ctx.Request.Header.Get("Token")
+	if token == "" {
+		return false, nil
+	}
+	t, err := auth_token.VerifyToken(token, path, ctx.Input.IP())
+	if err != nil {
+		return false, err
+	}
+	log.Printf("auth token: %v", t)
+	_ = sess.Set("client", t)
+	return true, nil
+}
+
 func AuthFilter(ctx *context.Context) {
 	path := ctx.Request.URL.Path
 	path = strings.TrimSuffix(path, "/")
@@ -67,6 +88,7 @@ func AuthFilter(ctx *context.Context) {
 		return
 	}
 	logs.Info(fmt.Sprintf("auth: %s,%s", ctx.Request.RequestURI, path))
+
 	sess := ctx.Input.CruSession
 	if sess == nil {
 		logs.Warn("no session found in request")
@@ -79,23 +101,23 @@ func AuthFilter(ctx *context.Context) {
 	}
 	data = sess.Get("user")
 	if data == nil {
-		WriteForbidden(ctx.ResponseWriter)
+		ok, err := checkAuthToken(ctx, sess, path)
+		if err != nil {
+			base.WriteError(ctx.ResponseWriter, err)
+			return
+		}
+		if ok {
+			return
+		}
+	}
+	if data == nil {
+		base.WriteForbidden(ctx.ResponseWriter)
 		return
 	}
 	user := data.(models.User)
 	if len(user.Account) == 0 {
-		WriteForbidden(ctx.ResponseWriter)
+		base.WriteForbidden(ctx.ResponseWriter)
 		return
 	}
 	logs.Info(fmt.Sprintf("request uri: %s, uid: %s", ctx.Request.RequestURI, user.Account))
 }
-
-func WriteForbidden(w http.ResponseWriter) {
-	w.WriteHeader(401)
-	w.Header().Set("Content-Type", "application/json")
-	_, err := w.Write([]byte(UnauthorizedResp))
-	if err != nil {
-		logs.Warn("writeForbidden write error", err)
-		return
-	}
-}

+ 39 - 0
server/models/agent.go

@@ -0,0 +1,39 @@
+package models
+
+import "github.com/mitchellh/mapstructure"
+
+const (
+	AgentCmdType      = "AGENT_CMD_TYPE"
+	SendFileType      = "AGENT_SEND_FILE"
+	RegisterNginxType = "REGISTER_NGINX"
+	NginxUpdateType   = "NGINX_UPDATE"
+	AgentConnected    = "AGENT_CONNECTED"
+	ServerConnected   = "SERVER_CONNECTED"
+)
+
+/*
+AgentData agent与server之间通信的消息类型
+属性:
+  - Type 操作类型
+*/
+type AgentData struct {
+	RequestId string      `json:"requestId"`
+	Data      interface{} `json:"data"`
+	Success   bool        `json:"success"`
+	Msg       string      `json:"msg"`
+	Type      string      `json:"type"`
+}
+
+func (r *AgentData) ReadData(result interface{}) error {
+	err := mapstructure.Decode(r.Data, result)
+	return err
+}
+
+type AgentCMD struct {
+	Cmd string `json:"cmd"`
+}
+
+type AgentSendFile struct {
+	FileName string `json:"fileName"`
+	Dst      string `json:"dst"`
+}

+ 30 - 0
server/models/auth.go

@@ -0,0 +1,30 @@
+package models
+
+import (
+	"errors"
+	"time"
+)
+
+// AuthToken 静态Token
+// - Uid 用户ID,谁创建的Token
+type AuthToken struct {
+	Id   int    `orm:"pk;auto" json:"id"`
+	Uid  int    `json:"uid"`
+	Name string `json:"name"`
+	// 客户端IP,白名单,多个用逗号隔开
+	ClientIps string `json:"clientIps"`
+	Token     string `json:"token"`
+	ExpiredAt int64  `json:"expiredAt"`
+	Enabled   bool   `json:"enabled"`
+	Remark    string `json:"remark"`
+}
+
+func (token *AuthToken) CheckValid() error {
+	if !token.Enabled {
+		return errors.New("token is disabled")
+	}
+	if time.Now().Unix() > token.ExpiredAt {
+		return errors.New("token is expired")
+	}
+	return nil
+}

+ 5 - 0
server/models/nginx.go

@@ -3,6 +3,7 @@ package models
 import "github.com/astaxie/beego"
 
 // Nginx nginx data
+// - Token: 客户端连接时的token,唯一
 type Nginx struct {
 	Id          int    `orm:"pk;auto" json:"id"`
 	Name        string `json:"name"`
@@ -10,6 +11,10 @@ type Nginx struct {
 	VersionInfo string `orm:"size(2550)" json:"versionInfo"`
 	// 是否以服务的形式进行托管
 	IsServer bool `json:"isServer"`
+	// 走Agent代理
+	Proxy bool `json:"proxy"`
+
+	Token string `json:"token"`
 
 	NginxPath string `json:"nginxPath"`
 	// nginx的配置文件所在目录,即nginx.conf所在的目录

+ 61 - 0
server/modules/agent_server/hub.go

@@ -0,0 +1,61 @@
+package agent_server
+
+import "nginx-ui/server/models"
+
+// Hub maintains the set of active clients and broadcasts messages to the
+// clients.
+type Hub struct {
+	// Registered clients.
+	clientMap map[string]*WsClient
+	// Inbound messages from the clients.
+	broadcast chan []byte
+
+	// Register requests from the clients.
+	register chan *WsClient
+
+	// Unregister requests from clients.
+	unregister chan *WsClient
+	handlers   map[string]func(c *WsClient, message *models.AgentData) (interface{}, error)
+}
+
+func newHub() *Hub {
+	return &Hub{
+		broadcast: make(chan []byte),
+		register:  make(chan *WsClient),
+		clientMap: make(map[string]*WsClient),
+		handlers:  make(map[string]func(c *WsClient, message *models.AgentData) (interface{}, error)),
+	}
+}
+
+func (h *Hub) SetMessageHandler(key string, handler func(c *WsClient, message *models.AgentData) (interface{}, error)) {
+	h.handlers[key] = handler
+}
+
+func (h *Hub) FindClient(key string) (*WsClient, bool) {
+	c, ok := h.clientMap[key]
+	return c, ok
+}
+
+func (h *Hub) run() {
+	for {
+		select {
+		case client := <-h.register:
+			h.clientMap[client.Token] = client
+		case client := <-h.unregister:
+			if _, ok := h.clientMap[client.Token]; ok {
+				delete(h.clientMap, client.Token)
+				close(client.send)
+			}
+			delete(h.clientMap, client.Token)
+		case message := <-h.broadcast:
+			for _, client := range h.clientMap {
+				select {
+				case client.send <- message:
+				default:
+					close(client.send)
+					delete(h.clientMap, client.Token)
+				}
+			}
+		}
+	}
+}

+ 48 - 0
server/modules/agent_server/server.go

@@ -0,0 +1,48 @@
+package agent_server
+
+import (
+	"github.com/gorilla/websocket"
+	"log"
+	"net/http"
+	"nginx-ui/server/models"
+)
+
+var upgrader = websocket.Upgrader{
+	ReadBufferSize:  4096,
+	WriteBufferSize: 4096,
+}
+
+type Server struct {
+	hub *Hub
+}
+
+var AgentHub = newHub()
+
+func NewServer() *Server {
+	go AgentHub.run()
+	return &Server{
+		hub: AgentHub,
+	}
+}
+
+func SetMessageHandler(key string, handler func(c *WsClient, message *models.AgentData) (interface{}, error)) {
+	AgentHub.SetMessageHandler(key, handler)
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Println(err)
+		return
+	}
+	token := r.Header.Get("Token")
+	client := &WsClient{
+		Token:     token,
+		hub:       s.hub,
+		conn:      conn,
+		send:      make(chan []byte, 1024),
+		callbacks: make(map[string]chan *models.AgentData),
+	}
+	client.hub.register <- client
+	go client.readPump()
+}

+ 143 - 0
server/modules/agent_server/ws_client.go

@@ -0,0 +1,143 @@
+package agent_server
+
+import (
+	"errors"
+	"github.com/gorilla/websocket"
+	"github.com/hashicorp/go-uuid"
+	"log"
+	"nginx-ui/server/models"
+	"time"
+)
+
+const (
+	// Time allowed to write a message to the peer.
+	writeWait = 12 * time.Second
+	readWait  = 12 * time.Second
+
+	// Time allowed to read the next pong message from the peer. ping period must be less
+	pongWait = 10 * time.Second
+
+	// Send pings to peer with this period. Must be less than pongWait.
+	pingPeriod = (pongWait * 9) / 10
+
+	// Maximum message size allowed from peer.
+	maxMessageSize = 4096
+)
+
+// WsClient is a middleman between the websocket connection and the hub.
+type WsClient struct {
+	Token string `json:"id"`
+	hub   *Hub
+	// The websocket connection.
+	conn *websocket.Conn
+	// Buffered channel of outbound messages.
+	send      chan []byte
+	callbacks map[string]chan *models.AgentData
+}
+
+// readPump pumps messages from the websocket connection to the hub.
+//
+// The application runs readPump in a per-connection goroutine. The application
+// ensures that there is at most one reader on a connection by executing all
+// reads from this goroutine.
+func (c *WsClient) readPump() {
+	defer func() {
+		c.hub.unregister <- c
+		_ = c.conn.Close()
+		log.Printf("readPump closed")
+	}()
+	c.conn.SetReadLimit(maxMessageSize)
+	_ = c.conn.SetReadDeadline(time.Now().Add(readWait))
+	_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+	c.conn.SetPingHandler(func(string) error {
+		log.Printf("ping: %v", time.Now())
+		_ = c.conn.SetReadDeadline(time.Now().Add(readWait))
+		_ = c.conn.SetWriteDeadline(time.Now().Add(writeWait))
+		_ = c.conn.WriteMessage(websocket.PongMessage, []byte{})
+		return nil
+	})
+	for {
+		agentData := &models.AgentData{}
+		err := c.conn.ReadJSON(&agentData)
+		log.Printf("recv: %v", err)
+		if err != nil {
+			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
+				log.Printf("error: %v", err)
+			}
+			log.Printf("read message: %v", err)
+			break
+		}
+		r, ok := c.callbacks[agentData.RequestId]
+		if ok {
+			r <- agentData
+			continue
+		}
+		handler, ok := c.hub.handlers[agentData.Type]
+		if ok {
+			go func() {
+				result, err := handler(c, agentData)
+				log.Printf("result: %v,err: %v", result, err)
+				if err != nil {
+					agentData.Success = false
+					agentData.Data = make(map[string]interface{})
+					agentData.Msg = err.Error()
+				} else {
+					agentData.Success = true
+					agentData.Data = result
+					agentData.Msg = ""
+				}
+				c.JustSend(agentData)
+			}()
+		}
+	}
+}
+
+/*
+Send 发送消息
+参数:
+
+	message - 消息体
+	result	- 返回结构体指针
+	timeout - 超时时间
+
+返回值:
+
+	error - 错误
+*/
+func (c *WsClient) Send(data *models.AgentData, timeout time.Duration) (*models.AgentData, error) {
+	var rec = make(chan *models.AgentData)
+	if data.RequestId == "" {
+		id, _ := uuid.GenerateUUID()
+		data.RequestId = id
+	}
+	c.callbacks[data.RequestId] = rec
+
+	timer := time.NewTimer(timeout)
+	defer func() {
+		delete(c.callbacks, data.RequestId)
+		timer.Stop()
+	}()
+	err := c.conn.WriteJSON(data)
+	if err != nil {
+		return nil, err
+	}
+
+	for {
+		select {
+		case res, ok := <-rec:
+			if !ok {
+				return nil, errors.New("receive data fail")
+			}
+			return res, nil
+
+		case <-timer.C:
+			return nil, errors.New("timeout")
+		}
+	}
+}
+
+// JustSend 仅发送,不管是否成功,用于消息回复
+func (c *WsClient) JustSend(message interface{}) {
+	err := c.conn.WriteJSON(message)
+	log.Printf("JustSend: message: %s, %v", message, err)
+}

+ 115 - 0
server/modules/auth_token/controller.go

@@ -0,0 +1,115 @@
+package auth_token
+
+import (
+	"github.com/astaxie/beego/logs"
+	"nginx-ui/server/base"
+	"nginx-ui/server/models"
+	"nginx-ui/server/vo"
+)
+
+type Controller struct {
+	base.Controller
+	service *Service
+}
+
+func NewController() *Controller {
+	return &Controller{
+		service: NewService(),
+	}
+}
+
+var AuthController = NewController()
+
+// List 获取全部用户信息
+func (c *Controller) List() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := vo.PageReq{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	query := &ListQuery{Uid: user.Id}
+	resp, err := c.service.List(query, &req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(resp).Json()
+}
+
+// Create 用户注册
+func (c *Controller) Create() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := models.AuthToken{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	req.Uid = user.Id
+	err := c.service.Creat(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(req).Json()
+}
+
+// Update 更新Token
+func (c *Controller) Update() {
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	req := models.AuthToken{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	err := c.service.Update(&req)
+	if err != nil {
+		logs.Warn("Users get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(req).Json()
+}
+
+func (c *Controller) GetDetail() {
+	// 获取全部用户信息
+	user := c.RequiredUser()
+	if user == nil {
+		return
+	}
+	id, err := c.GetIntQuery("id")
+	if err != nil {
+		logs.Warn("AuthToken get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	query, err := c.service.GetById(id)
+	if err != nil {
+		logs.Warn("AuthToken get fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.SetData(query).Json()
+}
+
+func (c *Controller) DeleteById() {
+	req := models.AuthToken{}
+	if !c.ReadBody(&req) {
+		return
+	}
+	err := c.service.DeleteById(req.Id)
+	if err != nil {
+		logs.Warn("AuthToken delete fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	c.Json()
+}

+ 104 - 0
server/modules/auth_token/service.go

@@ -0,0 +1,104 @@
+package auth_token
+
+import (
+	"github.com/astaxie/beego/orm"
+	"log"
+	"nginx-ui/server/models"
+	"nginx-ui/server/utils"
+	"nginx-ui/server/vo"
+)
+
+type Service struct {
+}
+
+type ListQuery struct {
+	Uid int `json:"uid"`
+}
+
+func NewService() *Service {
+	return &Service{}
+}
+
+func (u *Service) List(query *ListQuery, req *vo.PageReq) (*vo.PageResp, error) {
+	req.Ensure()
+	qs := orm.NewOrm().QueryTable(new(models.AuthToken)).Filter("Uid", query.Uid)
+	qs = qs.Offset(req.Offset).Limit(req.PageSize).OrderBy("-Id")
+
+	var list []models.AuthToken
+	_, err := qs.All(&list)
+	if err != nil {
+		return nil, err
+	}
+	count, err := qs.Count()
+	if err != nil {
+		return nil, err
+	}
+	resp := vo.PageResp{
+		PageSize: req.PageSize,
+		Current:  req.Current,
+		Total:    count,
+		List:     list,
+	}
+	return &resp, err
+}
+
+func (u *Service) Creat(token *models.AuthToken) error {
+	o := orm.NewOrm()
+	token.Token = utils.GenerateUUID()
+	_, err := o.Insert(token)
+	return err
+}
+
+func (u *Service) Update(token *models.AuthToken) error {
+	o := orm.NewOrm()
+	if token.Token == "" {
+		token.Token = utils.GenerateUUID()
+	}
+	_, err := o.Update(token)
+	return err
+}
+
+func (u *Service) GetById(id int) (*models.AuthToken, error) {
+	o := orm.NewOrm()
+	exist := models.AuthToken{Id: id}
+	err := o.Read(&exist)
+	if err != nil {
+		return nil, err
+	}
+	return &exist, nil
+}
+
+func GetByToken(token string) (*models.AuthToken, error) {
+	o := orm.NewOrm()
+	exist := models.AuthToken{Token: token}
+	err := o.Read(&exist, "Token")
+	if err != nil {
+		return nil, err
+	}
+	return &exist, nil
+}
+
+func (u *Service) DeleteById(id int) error {
+	o := orm.NewOrm()
+	exist := models.AuthToken{Id: id}
+	_, err := o.Delete(&exist, "Id")
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func VerifyToken(token string, path string, ip string) (*models.AuthToken, error) {
+	log.Printf("verify token: %s, path: %s, ip: %s", token, path, ip)
+	o := orm.NewOrm()
+	authToken := models.AuthToken{Token: token}
+	err := o.Read(&authToken, "Token")
+	if err != nil {
+		return nil, err
+	}
+	err = authToken.CheckValid()
+	if err != nil {
+		return nil, err
+	}
+	return &authToken, nil
+}

+ 3 - 3
server/modules/ldap/server_controller.go

@@ -4,7 +4,7 @@ import (
 	"errors"
 	"github.com/astaxie/beego/orm"
 	"nginx-ui/server/base"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
 	"nginx-ui/server/vo"
 )
@@ -48,7 +48,7 @@ func (c *ServerController) GetServerDetail() {
 		c.ErrorJson(err)
 		return
 	}
-	server.Password = config.ReplacePassword
+	server.Password = constants.ReplacePassword
 	c.SetData(server).Json()
 }
 
@@ -103,7 +103,7 @@ func (c *ServerController) Verify() {
 			return
 		}
 		for _, user := range list {
-			user.Password = config.ReplacePassword
+			user.Password = constants.ReplacePassword
 		}
 		if body.Active {
 			o := orm.NewOrm()

+ 4 - 4
server/modules/ldap/server_service.go

@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"github.com/astaxie/beego/orm"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
 	"nginx-ui/server/vo"
 )
@@ -64,7 +64,7 @@ func (c *ServerService) GetServers(current *models.User, req *vo.PageReq) (*vo.P
 	var list []*models.LdapServer
 	_, err = qs.All(&list)
 	for _, v := range list {
-		v.Password = config.ReplacePassword
+		v.Password = constants.ReplacePassword
 	}
 
 	if err != nil {
@@ -107,7 +107,7 @@ func (c *ServerService) Update(current *models.User, body *models.LdapServer) (*
 		if err != nil {
 			return nil, err
 		}
-		if config.ReplacePassword == body.Password {
+		if constants.ReplacePassword == body.Password {
 			body.Password = exist.Password
 		}
 		_, err = o.Update(body)
@@ -152,7 +152,7 @@ func (c *ServerService) Add(current *models.User, body *models.LdapServer) (*mod
 		if err != nil {
 			return nil, err
 		}
-		if config.ReplacePassword == body.Password {
+		if constants.ReplacePassword == body.Password {
 			body.Password = exist.Password
 		}
 		_, err = o.Update(body)

+ 4 - 4
server/modules/ldap/user_service.go

@@ -6,7 +6,7 @@ import (
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
 	"github.com/go-ldap/ldap/v3"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
 )
 
@@ -52,7 +52,7 @@ func (c *UserService) Add(body *models.LdapUser) (*models.LdapUser, error) {
 	if err != nil {
 		return nil, err
 	}
-	if body.Password != config.ReplacePassword {
+	if body.Password != constants.ReplacePassword {
 		err := client.ModifyPasswordByAdmin(body.DN, body.Password)
 		if err != nil {
 			return nil, errors.New("新增成功,但密码修改失败!")
@@ -79,7 +79,7 @@ func (c *UserService) GetDetail(id int) (*models.LdapUser, error) {
 	if err != nil {
 		return nil, err
 	}
-	user.Password = config.ReplacePassword
+	user.Password = constants.ReplacePassword
 	return &user, nil
 }
 
@@ -90,7 +90,7 @@ func (c *UserService) GetByAccount(account string) (*models.LdapUser, error) {
 	if err != nil {
 		return nil, err
 	}
-	user.Password = config.ReplacePassword
+	user.Password = constants.ReplacePassword
 	return &user, nil
 }
 

+ 1 - 2
server/modules/nginx/nginx_controller/base.go

@@ -5,7 +5,6 @@ import (
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
 	"nginx-ui/server/base"
-	"nginx-ui/server/middleware"
 	"nginx-ui/server/models"
 	"strconv"
 )
@@ -30,7 +29,7 @@ func (c *BaseController) CheckNginxPermission() (*models.Nginx, error) {
 func (c *BaseController) CheckNginxPermissionById(nginxId int) (*models.Nginx, error) {
 	current := c.RequiredUser()
 	if current == nil {
-		middleware.WriteForbidden(c.Ctx.ResponseWriter)
+		base.WriteForbidden(c.Ctx.ResponseWriter)
 		return nil, errors.New("当前未登录,无法操作")
 	}
 	if nginxId < 1 {

+ 20 - 0
server/modules/nginx/nginx_controller/file.go

@@ -123,6 +123,26 @@ func (c *FileController) Deploy() {
 	c.Json()
 }
 
+// Download 下载文件
+// GET /file/:id/file/:fileName
+func (c *FileController) Download() {
+	fileName := c.Ctx.Input.Param("fileName")
+	nginxId := c.Ctx.Input.Param("nginxId")
+	logs.Info("download file: {} for nginxId: {}", fileName, nginxId)
+	root, err := getRootDir()
+	if err != nil {
+		logs.Warn("getRootDir fail: %v", err)
+		c.ErrorJson(err)
+		return
+	}
+	filePath := fmt.Sprintf("%s/%s", root, fileName)
+	if !utils.IsExist(filePath) {
+		logs.Warn("dir not exist: ", root)
+		c.ErrorJson(errors.New("未上传文件或者文件已被删除!"))
+	}
+	c.Ctx.Output.Download(filePath, fileName)
+}
+
 func HandleDeploy(req models.DeployReq) error {
 	root, err := getRootDir()
 	if err != nil {

+ 7 - 4
server/modules/nginx/nginx_controller/nginx.go

@@ -4,11 +4,14 @@ import (
 	"encoding/json"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
+	"nginx-ui/server/modules/nginx/nginx_service"
 	ngx "nginx-ui/server/nginx"
 )
 
+var ngxService = nginx_service.NewNginxService()
+
 type NginxController struct {
 	BaseController
 }
@@ -30,7 +33,7 @@ func (c *NginxController) Get() {
 	for i := range list {
 		item := list[i]
 		if item.Password != "" {
-			item.Password = config.ReplacePassword
+			item.Password = constants.ReplacePassword
 		}
 	}
 	if err != nil {
@@ -111,7 +114,7 @@ func (c *NginxController) Update() {
 	nginx.Check()
 	o := orm.NewOrm()
 
-	if nginx.Password == config.ReplacePassword {
+	if nginx.Password == constants.ReplacePassword {
 		nginx.Password = exist.Password
 	}
 	nginx.HttpConf = exist.HttpConf
@@ -215,7 +218,7 @@ func (c *NginxController) GetNginx() {
 		return
 	}
 	if nginx.Password != "" {
-		nginx.Password = config.ReplacePassword
+		nginx.Password = constants.ReplacePassword
 	}
 	c.AddRespData("nginx", nginx)
 

+ 47 - 0
server/modules/nginx/nginx_service/handler.go

@@ -0,0 +1,47 @@
+package nginx_service
+
+import (
+	"errors"
+	"github.com/astaxie/beego/orm"
+	"log"
+	"nginx-ui/server/models"
+	"nginx-ui/server/modules/agent_server"
+	"nginx-ui/server/modules/auth_token"
+)
+
+func AgentRegister(c *agent_server.WsClient, message *models.AgentData) (interface{}, error) {
+	log.Printf("register agent: %s,%v\n", c.Token, message)
+	token, err := auth_token.GetByToken(c.Token)
+	if err != nil {
+		return nil, err
+	}
+	err = token.CheckValid()
+	if err != nil {
+		return nil, err
+	}
+	nginx := &models.Nginx{}
+	err = message.ReadData(nginx)
+	if err != nil {
+		return nil, err
+	}
+	o := orm.NewOrm()
+	exist := &models.Nginx{
+		Token: c.Token,
+	}
+	err = o.Read(exist, "Token")
+	if err == nil {
+		return exist, nil
+	}
+	if !errors.Is(err, orm.ErrNoRows) {
+		return nil, err
+	}
+	nginx.Proxy = true
+	nginx.Token = c.Token
+	nginx.Uid = string(rune(token.Uid))
+	id, err := o.Insert(nginx)
+	if err != nil {
+		return nil, err
+	}
+	nginx.Id = int(id)
+	return nginx, nil
+}

+ 82 - 4
server/modules/nginx/nginx_service/nginx.go

@@ -5,7 +5,7 @@ import (
 	"errors"
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
 	ngx "nginx-ui/server/nginx"
 	"strconv"
@@ -14,6 +14,10 @@ import (
 type NginxService struct {
 }
 
+func NewNginxService() *NginxService {
+	return &NginxService{}
+}
+
 // CheckNginxPermission 从path中获取nginx的参数
 func (c *NginxService) CheckNginxPermission(user *models.User, nginxId string) (*models.Nginx, error) {
 	id, err := strconv.Atoi(nginxId)
@@ -58,7 +62,7 @@ func (c *NginxService) ListNginx(current *models.User) *models.RespData {
 	for i := range list {
 		item := list[i]
 		if item.Password != "" {
-			item.Password = config.ReplacePassword
+			item.Password = constants.ReplacePassword
 		}
 	}
 	if err != nil {
@@ -110,6 +114,33 @@ func (c *NginxService) Add(current *models.User, req []byte) *models.RespData {
 	return models.SuccessResp(nginx)
 }
 
+func AddNginx(nginx *models.Nginx) error {
+	nginx.Check()
+	o := orm.NewOrm()
+	nginx.NginxPath = "/usr/sbin/nginx"
+	nginx.NginxDir = "/etc/nginx"
+	nginx.DataDir = "/app/data"
+	_, err := o.Insert(nginx)
+
+	if err != nil {
+		return err
+	}
+	logs.Info("post", nginx)
+
+	instance := ngx.GetInstance(nginx)
+	err = instance.Connect()
+	if err != nil {
+		return err
+	}
+	out, err := instance.GetVersion()
+	if err != nil {
+		return err
+	}
+	nginx.VersionInfo = out
+	_, _ = o.Update(nginx, "VersionInfo")
+	return nil
+}
+
 // Update modify nginx instance
 // post /nginx/:id
 func (c *NginxService) Update(nginxId string, current *models.User, req []byte) *models.RespData {
@@ -134,7 +165,7 @@ func (c *NginxService) Update(nginxId string, current *models.User, req []byte)
 	nginx.Check()
 	o := orm.NewOrm()
 
-	if nginx.Password == config.ReplacePassword {
+	if nginx.Password == constants.ReplacePassword {
 		nginx.Password = exist.Password
 	}
 	nginx.HttpConf = exist.HttpConf
@@ -159,6 +190,40 @@ func (c *NginxService) Update(nginxId string, current *models.User, req []byte)
 	return models.SuccessResp(nginx)
 }
 
+func (c *NginxService) UpdateNginx(nginx *models.Nginx) error {
+	nginx.Check()
+	o := orm.NewOrm()
+
+	exist := models.Nginx{Id: nginx.Id}
+	err := o.Read(&nginx)
+	if err != nil {
+		return err
+	}
+	if nginx.Password == constants.ReplacePassword {
+		nginx.Password = exist.Password
+	}
+	nginx.HttpConf = exist.HttpConf
+	_, err = o.Update(&nginx)
+
+	if err != nil {
+		return nil
+	}
+	logs.Info("post", nginx)
+
+	instance := ngx.GetInstance(nginx)
+	err = instance.Connect()
+	if err != nil {
+		return err
+	}
+	out, err := instance.GetVersion()
+	if err != nil {
+		return err
+	}
+	nginx.VersionInfo = out
+	_, _ = o.Update(&nginx, "VersionInfo")
+	return nil
+}
+
 // StartNginx startNginx
 // post /nginx/:id/start
 func (c *NginxService) StartNginx(nginxId string, user *models.User) *models.RespData {
@@ -245,7 +310,7 @@ func (c *NginxService) GetNginx(nginxId string, user *models.User) *models.RespD
 		return models.NewErrorResp(err)
 	}
 	if nginx.Password != "" {
-		nginx.Password = config.ReplacePassword
+		nginx.Password = constants.ReplacePassword
 	}
 	var resp = map[string]interface{}{}
 	resp["nginx"] = nginx
@@ -260,6 +325,19 @@ func (c *NginxService) GetNginx(nginxId string, user *models.User) *models.RespD
 	return models.SuccessResp(resp)
 }
 
+func FindById(nginxId int) (*models.Nginx, error) {
+	if nginxId < 1 {
+		return nil, errors.New("nginx ID must gt 0!")
+	}
+	nginx := models.Nginx{Id: nginxId}
+	o := orm.NewOrm()
+	err := o.Read(&nginx)
+	if err != nil {
+		return nil, err
+	}
+	return &nginx, nil
+}
+
 // DelNginx delete a instance
 // delete /nginx/:id
 func (c *NginxService) DelNginx(nginxId string, user *models.User) *models.RespData {

+ 1 - 1
server/modules/proxy/websocket.go

@@ -36,7 +36,7 @@ type WebsocketProxy struct {
 
 type Options func(wp *WebsocketProxy)
 
-// You must carry a port number,ws://ip:80/ssss, wss://ip:443/aaaa
+// NewWebsocketProxy You must carry a port number,ws://ip:80/ssss, wss://ip:443/aaaa
 // ex: ws://ip:port/ajaxchattest
 func NewWebsocketProxy(addr string, beforeHandshake func(r *http.Request) error, options ...Options) (*WebsocketProxy, error) {
 	u, err := url.Parse(addr)

+ 4 - 4
server/modules/user/service.go

@@ -6,7 +6,7 @@ import (
 	"github.com/astaxie/beego/logs"
 	"github.com/astaxie/beego/orm"
 	"github.com/hashicorp/go-uuid"
-	"nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/models"
 	"nginx-ui/server/modules/ldap"
 	"nginx-ui/server/utils"
@@ -79,7 +79,7 @@ func (u *Service) Users(req *vo.PageReq) (*vo.PageResp, error) {
 
 	var resList []models.User
 	for _, user := range list {
-		user.Password = config.ReplacePassword
+		user.Password = constants.ReplacePassword
 		resList = append(resList, user)
 	}
 	resp := vo.PageResp{
@@ -106,7 +106,7 @@ func (u *Service) Update(req *models.User) (*models.User, error) {
 		}
 		return req, nil
 	}
-	if req.Password == "" || req.Password == config.ReplacePassword {
+	if req.Password == "" || req.Password == constants.ReplacePassword {
 		req.Password = exist.Password
 	} else {
 		req.Password = utils.GetSHA256HashCode(req.Password)
@@ -126,7 +126,7 @@ func (u *Service) GetDetail(id int) (*models.User, error) {
 	if err != nil {
 		return nil, errors.New("该用户不存在或者已被删除!")
 	}
-	exist.Password = config.ReplacePassword
+	exist.Password = constants.ReplacePassword
 	return &exist, nil
 }
 

+ 94 - 0
server/nginx/agent.go

@@ -0,0 +1,94 @@
+package nginx
+
+import (
+	"errors"
+	"github.com/astaxie/beego/logs"
+	"log"
+	"nginx-ui/server/models"
+	"nginx-ui/server/modules/agent_server"
+	"path/filepath"
+	"time"
+)
+
+// AgentInstance 远程,agent代理
+type AgentInstance struct {
+	nginx      *models.Nginx
+	client     *agent_server.WsClient
+	LastResult string
+}
+
+func (n *AgentInstance) SetNginx(nginx *models.Nginx) {
+	log.Println("SetNginx:", nginx)
+	n.nginx = nginx
+	c, ok := agent_server.AgentHub.FindClient(n.nginx.Token)
+	if !ok {
+		return
+	}
+	n.client = c
+	data := &models.AgentData{
+		Data: nginx,
+		Type: models.NginxUpdateType,
+	}
+	res, err := c.Send(data, 10*time.Second)
+	if err != nil {
+		logs.Error("send agent data err: %v\n", err)
+	}
+	logs.Info("SetNginx: %v\n", res)
+}
+
+// Connect 连接到nginx实例客户端,检查客户端是否可用
+func (n *AgentInstance) Connect() error {
+	return nil
+}
+
+// Run RemoteInstance 这里应该要处理session断开的流程吧
+func (n *AgentInstance) Run(cmd string) (string, error) {
+	logs.Info("Run: ", cmd)
+	if n.client == nil {
+		return "", errors.New("agent not online")
+	}
+	data := &models.AgentData{
+		Data: &models.AgentCMD{
+			Cmd: cmd,
+		},
+		Type: models.AgentCmdType,
+	}
+	res, err := n.client.Send(data, 10*time.Second)
+	if err != nil {
+		return "", err
+	}
+	if !res.Success {
+		return "", errors.New(res.Msg)
+	}
+	n.LastResult = res.Msg
+	logger.Printf("out: %v", n.LastResult)
+	return n.LastResult, err
+}
+
+func (n *AgentInstance) Close(onlySession bool) {
+	log.Printf("agent clone,skip")
+}
+
+// SendFile RemoteInstance 这里应该要处理session断开的流程吧
+func (n *AgentInstance) SendFile(src string, remote string) error {
+	logs.Info("SendFile: ", src, remote)
+	if n.client == nil {
+		return errors.New("agent not online")
+	}
+	data := &models.AgentData{
+		Data: &models.AgentSendFile{
+			FileName: filepath.Base(src),
+			Dst:      remote,
+		},
+		Type: models.SendFileType,
+	}
+	res, err := n.client.Send(data, 120*time.Second)
+	if err != nil {
+		return err
+	}
+	if !res.Success {
+		return errors.New(res.Msg)
+	}
+	logger.Printf("out: %v", n.LastResult)
+	return nil
+}

+ 4 - 0
server/nginx/local.go

@@ -17,6 +17,10 @@ type LocalInstance struct {
 	nginx *models.Nginx
 }
 
+func NewLocalNginx(nginx *models.Nginx) *LocalInstance {
+	return &LocalInstance{nginx: nginx}
+}
+
 func (n *LocalInstance) Connect() error {
 	return nil
 }

+ 10 - 2
server/nginx/manager.go

@@ -7,7 +7,7 @@ import (
 var INSTANCES = map[int]*Instance{}
 
 func GetInstance(nginx *models.Nginx) *Instance {
-	var instance *Instance = INSTANCES[nginx.Id]
+	var instance = INSTANCES[nginx.Id]
 	if instance != nil {
 		old := instance.nginx
 		if old.IpAddr != nginx.IpAddr || old.Port != nginx.Port || old.User != nginx.User || old.Password != nginx.Password {
@@ -20,7 +20,14 @@ func GetInstance(nginx *models.Nginx) *Instance {
 			return instance
 		}
 	}
-	if nginx.IsLocal {
+	if nginx.Proxy {
+		instance = &Instance{
+			&AgentInstance{
+				nginx: nginx,
+			},
+			nginx,
+		}
+	} else if nginx.IsLocal {
 		instance = &Instance{
 			&LocalInstance{
 				nginx: nginx,
@@ -35,6 +42,7 @@ func GetInstance(nginx *models.Nginx) *Instance {
 			nginx,
 		}
 	}
+	instance.SetNginx(nginx)
 	INSTANCES[nginx.Id] = instance
 	return instance
 }

+ 14 - 0
server/routers/router.go

@@ -7,7 +7,10 @@ import (
 	"github.com/astaxie/beego/logs"
 	"nginx-ui/server/base"
 	config2 "nginx-ui/server/config"
+	"nginx-ui/server/constants"
 	"nginx-ui/server/middleware"
+	"nginx-ui/server/modules/agent_server"
+	"nginx-ui/server/modules/auth_token"
 	"nginx-ui/server/modules/ldap"
 	"nginx-ui/server/modules/nginx/nginx_controller"
 	"nginx-ui/server/modules/oauth2"
@@ -34,6 +37,8 @@ func init() {
 
 	logs.Info("baseApi", config.BaseApi)
 
+	agentServer := agent_server.NewServer()
+
 	ns := beego.NewNamespace(config.BaseApi,
 		beego.NSRouter(NginxR, &nginx_controller.NginxController{}),
 		beego.NSRouter(NginxGetR, &nginx_controller.NginxController{}, "post:Update"),
@@ -52,6 +57,7 @@ func init() {
 		// file upload download
 		beego.NSRouter("/nginx/:id/file/deploy", &nginx_controller.FileController{}, "post:Deploy"),
 		beego.NSRouter("/file", &nginx_controller.FileController{}),
+		beego.NSRouter(constants.DownloadFileUrl, &nginx_controller.FileController{}, "get:Download"),
 		beego.NSRouter("/logger", &nginx_controller.LoggerController{}),
 		//	角色
 		beego.NSRouter("/role/list", &user.RoleController{}, "post:List"),
@@ -84,6 +90,14 @@ func init() {
 		beego.NSRouter("/proxy/list", proxy.Instance, "post:List"),
 		beego.NSRouter("/proxy/save", proxy.Instance, "post:Save"),
 		beego.NSRouter("/proxy/remove", proxy.Instance, "post:Remove"),
+		// auth-token
+		beego.NSRouter("/auth-token/list", auth_token.AuthController, "post:List"),
+		beego.NSRouter("/auth-token/create", auth_token.AuthController, "post:Create"),
+		beego.NSRouter("/auth-token/update", auth_token.AuthController, "post:Update"),
+		beego.NSRouter("/auth-token/remove", auth_token.AuthController, "post:DeleteById"),
+		beego.NSRouter("/auth-token/detail", auth_token.AuthController, "get:GetDetail"),
+		// agent
+		beego.NSHandler(constants.AgentConnectUrl, agentServer),
 	)
 	beego.AddNamespace(ns)
 	// LDAP路由

+ 28 - 0
server/utils/ip.go

@@ -0,0 +1,28 @@
+package utils
+
+import (
+	"log"
+	"net"
+)
+
+// 获取本机网卡IP
+func GetNetIP() (ipv4 string) {
+	// 获取所有网卡
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		log.Printf("InterfaceAddrs error: %v", err)
+		return ""
+	}
+	// 取第一个非lo的网卡IP
+	for _, addr := range addrs {
+		// 这个网络地址是IP地址: ipv4, ipv6
+		if ipNet, isIpNet := addr.(*net.IPNet); isIpNet && !ipNet.IP.IsLoopback() {
+			// 跳过IPV6
+			if ipNet.IP.To4() != nil {
+				ipv4 = ipNet.IP.String() // 192.168.1.1
+				return
+			}
+		}
+	}
+	return ""
+}

+ 11 - 0
server/utils/ip_test.go

@@ -0,0 +1,11 @@
+package utils
+
+import (
+	"log"
+	"testing"
+)
+
+func TestGetNetIP(t *testing.T) {
+	ip := GetNetIP()
+	log.Printf("local ip: %s\n", ip)
+}

+ 6 - 0
server/utils/snow.go

@@ -2,7 +2,9 @@ package utils
 
 import (
 	"fmt"
+	"github.com/google/uuid"
 	"strconv"
+	"strings"
 	"sync"
 	"time"
 )
@@ -77,3 +79,7 @@ func NextId() int64 {
 func NextIdStr() string {
 	return strconv.FormatInt(NextId(), 10)
 }
+
+func GenerateUUID() string {
+	return strings.ReplaceAll(uuid.NewString(), "-", "")
+}

Some files were not shown because too many files changed in this diff