├── .gitignore
├── LICENSE
├── README.md
├── bootstrap
└── app.go
├── conf.yaml
├── controller
├── auth_controller.go
├── client_controller.go
├── dashboard_controller.go
├── port_controller.go
└── tag_controller.go
├── core
├── constant.go
├── core_handler.go
├── extra_handler.go
├── natok_handler.go
└── natok_pool.go
├── docker-compose.yaml
├── dsmapper
├── client_rep.go
├── dsmapper.go
├── engine.go
├── port_rep.go
├── tag_rep.go
└── user_rep.go
├── go.mod
├── go.sum
├── grid-snake.svg
├── main.go
├── model
├── natok_client.go
├── natok_port.go
├── natok_tag.go
├── natok_user.go
└── vo
│ └── result.go
├── nac.manifest
├── nac.syso
├── service
├── client_service.go
├── dashboard_service.go
├── port_service.go
└── tag_service.go
├── support
├── app_config.go
├── auth_config.go
├── captcha.go
└── path_util.go
├── timer
└── worker.go
├── util
├── gen_bill_code.go
├── snowflake.go
└── toolkit.go
└── web
├── s-cert.key
├── s-cert.pem
├── static
├── css
│ ├── app.86591c84.css
│ ├── chunk-1d778d6e.93fae406.css
│ ├── chunk-22cea610.3c7f5ad9.css
│ ├── chunk-3ad411e6.f9c4d84f.css
│ ├── chunk-4e2a48e4.71202b84.css
│ ├── chunk-75646770.e17cce70.css
│ ├── chunk-94143924.1d17f386.css
│ ├── chunk-elementUI.c1c3b808.css
│ └── chunk-libs.3dfb7769.css
├── favicon.ico
├── fonts
│ ├── element-icons.535877f5.woff
│ └── element-icons.732389de.ttf
├── img
│ ├── 404.a57b6f31.png
│ ├── 404_cloud.0f4bc32b.png
│ ├── avatar.png
│ └── white-slash.png
└── js
│ ├── app.d1298179.js
│ ├── chunk-1d778d6e.f343b652.js
│ ├── chunk-22cea610.396870de.js
│ ├── chunk-3ad411e6.99a2a01f.js
│ ├── chunk-4e2a48e4.3809de84.js
│ ├── chunk-75646770.6e5eb569.js
│ ├── chunk-94143924.630f88be.js
│ ├── chunk-d74b6636.33b45b15.js
│ ├── chunk-elementUI.4756f768.js
│ ├── chunk-f314aac0.4f635a72.js
│ └── chunk-libs.dc64d759.js
└── view
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | # Eclipse artifacts, including WTP generated manifests #
2 | .classpath
3 | .project
4 |
5 | # IDEA artifacts and output dirs #
6 | *.ipr
7 | *.iws
8 | .idea
9 |
10 | *.log
11 | *.exe
12 | vendor
13 | natok-server
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 代码咖啡因
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NATOK ·  
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
10 | - 🌱 natok是一个将局域网内个人服务代理到公网可访问的内网穿透工具。基于tcp协议、支持udp协议, 支持任何tcp上层协议(列如: http、https、ssh、telnet、data base、remote desktop....)。
11 | - 🤔 目前市面上提供类似服务的有: 花生壳、natapp、ngrok等等。当然, 这些工具都很优秀; 但是免费提供的服务都很有限, 想要有比较好的体验都需要支付一定的套餐费用, 由于数据包会流经第三方, 因此总归有些不太友好。
12 | - ⚡ natok-server与natok-cli都基于GO语言开发, 先天并发支持; 运行时的内存开销也很低, 一般在二十M左右。
13 |
14 |
15 | 运行natok-server相关的准备
16 | - 公网ip的服务器主机,配置无特殊要求,当然带宽高点也好。
17 | - 数据库:推荐sqlite,便捷无需任何配置;支持mysql,便于数据维护。
18 |
19 | **一、natok-server使用sqlite:conf.yaml**
20 | ```yaml
21 | natok:
22 | web.port: 1000 #natok·admin管理后台web页面
23 | server:
24 | port: 1001 #natok-cli的通信;若更换需与natok-cli的端口保持一致
25 | cert-pem-path: web/s-cert.pem #TSL加密密钥;若更换需与natok-cli保持一致
26 | cert-key-path: web/s-cert.key #TSL加密证书;若更换需与natok-cli保持一致
27 | log-file-path: web/out.log #程序日志输出文件
28 | datasource:
29 | type: sqlite
30 | db-suffix: beta #库后缀,可指定
31 | table-prefix: "" #表前缀,可指定
32 | ```
33 |
34 | **二、natok-server使用mysql:conf.yaml**
35 | ```yaml
36 | natok:
37 | web.port: 1000 #natok·admin管理后台web页面
38 | server:
39 | port: 1001 #natok-cli的通信;若更换需与natok-cli的端口保持一致
40 | cert-pem-path: web/s-cert.pem #TSL加密密钥;若更换需与natok-cli保持一致
41 | cert-key-path: web/s-cert.key #TSL加密证书;若更换需与natok-cli保持一致
42 | log-file-path: web/out.log #程序日志输出文件
43 | datasource:
44 | type: mysql
45 | host: 127.0.0.1 #自己的数据库地址
46 | port: 3306 #自己的数据库端口
47 | username: natok #数据库账号
48 | password: "123456" #数据库密码
49 | db-suffix: beta #库后缀,可指定
50 | table-prefix: "" #表前缀,可指定
51 | ```
52 |
53 | - windows系统启动: 双击 natok-server.exe
54 | ```powershell
55 | # 注册服务,自动提取管理员权限:
56 | natok-server.exe install
57 | # 卸载服务,自动提取管理员权限:
58 | natok-server.exe uninstall
59 | # 启停服务,自动提取管理员权限:
60 | natok-server.exe start/stop
61 | # 启停服务,终端管理员权限
62 | net start/stop natok-server
63 | ```
64 | - Linux系统启动:
65 | ```shell
66 | # 授予natok-server可执权限
67 | chmod 755 natok-server
68 | # 启动应用
69 | nohup ./natok-server > /dev/null 2>&1 &
70 | ```
71 |
72 | ---
73 |
74 | ### natok-server开发环境搭建
75 |
76 | **Go 1.22.0 及以上(推荐)**
77 | ```shell
78 | # 配置 GOPROXY 环境变量
79 | go env -w GO111MODULE=on
80 | go env -w GOPROXY=https://goproxy.cn,direct
81 | ```
82 |
83 | ```shell
84 | # 克隆项目
85 | git clone https://github.com/natokay/go-natok-server.git
86 |
87 | # 进入项目目录
88 | cd go-natok-server
89 |
90 | # 更新/下载依赖
91 | go mod tidy
92 | go mod vendor
93 |
94 | # 设置目标可执行程序操作系统构架,包括 386,amd64,arm
95 | go env -w GOARCH=amd64
96 |
97 | # 设置可执行程序运行操作系统,支持 darwin,freebsd,linux,windows
98 | go env -w GOOS=windows
99 |
100 | # golang windows 程序获取管理员权限(UAC)
101 | # go install github.com/akavel/rsrc@latest
102 | # go env GOPATH 将里路径bin的目录配置到环境变量
103 | rsrc -manifest nac.manifest -o nac.syso
104 |
105 | # cd到main.go目录,打包命令
106 | go build
107 |
108 | # 启动程序
109 | ./natok-server.exe
110 | ```
111 |
112 | ## 版本描述
113 | **natok:1.0.0**
114 |
115 | natok-cli与natok-server网络代理通信基本功能实现。
116 |
117 | **natok:1.1.0**
118 |
119 | natok-cli与natok-server支持windows平台注册为服务运行,可支持开机自启,保证服务畅通。
120 |
121 | **natok:1.2.0**
122 |
123 | natok-cli可与多个natok-server保持连接,支持从多个不同的natok-server来访问natok-cli,以实现更快及更优的网络通信。
124 |
125 | **natok:1.3.0**
126 |
127 | natok-cli与natok-server可支持udp网络代理。
128 |
129 |
130 | **natok:1.4.0**
131 | 1. natok-server端口访问支持白名单限制,重要端口(如:linux-22,windows-3389)可限制访问的ip地址。
132 | 2. natok-server端口访问监听,可选择监听范围:global=全局,local=本地。
133 |
134 | **natok:1.5.0**
135 |
136 | natok-server数据库类型支持sqlite、mysql,推荐使用sqlite,部署更便捷。
137 |
138 | **natok:1.6.0**
139 |
140 | natok-server与natok-client内部通讯采用连接池,即从公网访问natok-server后,会将连接放入连接池中,以便后续的请求时能更快的响应。
141 |
142 |
143 | ## NATOK平台界面预览
144 |
145 | 登录页面
146 | 
147 |
148 | 统计概览
149 | 
150 |
151 | 代理管理
152 | 
153 | 
154 |
155 | 端口映射
156 | 
157 | 
158 |
159 | 标签名单
160 | 
161 | 
162 |
--------------------------------------------------------------------------------
/bootstrap/app.go:
--------------------------------------------------------------------------------
1 | package bootstrap
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "github.com/kataras/iris/v12"
7 | "github.com/kataras/iris/v12/middleware/logger"
8 | "github.com/kataras/iris/v12/middleware/recover"
9 | "github.com/kataras/iris/v12/mvc"
10 | "github.com/sirupsen/logrus"
11 | "natok-server/controller"
12 | "natok-server/core"
13 | "natok-server/dsmapper"
14 | "natok-server/service"
15 | "natok-server/support"
16 | "natok-server/timer"
17 | "net"
18 | "os"
19 | "strconv"
20 | )
21 |
22 | func StartApp() {
23 | support.InitConfig()
24 | support.InitAuth()
25 | dsmapper.InitDatabase()
26 | StartServer()
27 | StartWeb()
28 | }
29 |
30 | // StartServer 启动主服务
31 | func StartServer() {
32 | var (
33 | conf = support.AppConf.Natok.Server
34 | addr = conf.InetHost + ":" + strconv.Itoa(conf.InetPort)
35 | listener net.Listener
36 | err error
37 | )
38 |
39 | if conf.CertKeyPath == "" || conf.CertPemPath == "" {
40 | listener, err = net.Listen("tcp", addr)
41 | logrus.Info("NET Listen")
42 | } else {
43 | cert, err := tls.LoadX509KeyPair(conf.CertPemPath, conf.CertKeyPath)
44 | if err != nil {
45 | logrus.Fatal(err)
46 | }
47 | certBytes, err := os.ReadFile(conf.CertPemPath)
48 | if err != nil {
49 | panic("Unable to read cert.pem")
50 | }
51 | clientCertPool := x509.NewCertPool()
52 | ok := clientCertPool.AppendCertsFromPEM(certBytes)
53 | if !ok {
54 | panic("Failed to parse root certificate")
55 | }
56 | tlsConfig := &tls.Config{
57 | Certificates: []tls.Certificate{cert},
58 | ClientAuth: tls.RequireAndVerifyClientCert,
59 | ClientCAs: clientCertPool,
60 | }
61 | listener, err = tls.Listen("tcp", addr, tlsConfig)
62 | logrus.Info("TLS Listen")
63 | }
64 |
65 | if err != nil {
66 | logrus.Fatal(err)
67 | }
68 | logrus.Infof("Startup natok-server on %s", listener.Addr())
69 |
70 | // 监听来自Natok-cli的启动连接
71 | go core.NatokClient(listener)
72 |
73 | // 周期性任务
74 | timer.Worker()
75 | }
76 |
77 | // StartWeb 启动Web服务
78 | func StartWeb() {
79 | app := iris.New()
80 | app.Logger().SetLevel("info")
81 | app.Use(logger.New())
82 | app.Use(recover.New())
83 | app.Use(support.CorsHandler())
84 | app.Use(support.AuthorHandler())
85 |
86 | baseDirPath := support.AppConf.BaseDirPath
87 |
88 | app.Favicon(baseDirPath + "./web/static/favicon.ico")
89 | app.HandleDir("/static", baseDirPath+"./web/static")
90 | app.RegisterView(iris.HTML(baseDirPath+"./web/view", ".html"))
91 |
92 | // 跨域访问配置
93 | visitApp := app.Party("/").AllowMethods(iris.MethodOptions)
94 | // MVC控制层配置
95 | mvc.Configure(visitApp, func(app *mvc.Application) {
96 | app.Register(support.SessionsManager.Start).Handle(new(controller.AuthController))
97 | app.Register(new(service.ClientService)).Handle(new(controller.ClientController))
98 | app.Register(new(service.PortService)).Handle(new(controller.PortController))
99 | app.Register(new(service.TagService)).Handle(new(controller.TagController))
100 | app.Register(new(service.ReportService)).Handle(new(controller.ReportController))
101 | })
102 | // 路由首页
103 | visitApp.Get("/", func(ctx iris.Context) {
104 | ctx.Redirect("/index.html", iris.StatusFound)
105 | })
106 | visitApp.Get("/index.html", func(ctx iris.Context) {
107 | ctx.View("index.html")
108 | })
109 | // 启动服务,端口监听
110 | app.Run(iris.Addr(":"+strconv.Itoa(support.AppConf.Natok.WebPort)), iris.WithCharset("UTF-8"))
111 | }
112 |
--------------------------------------------------------------------------------
/conf.yaml:
--------------------------------------------------------------------------------
1 | natok:
2 | web.port: 1000
3 | log.debug: false
4 | server:
5 | host: 0.0.0.0
6 | port: 1001
7 | log-file-path: web/out.log
8 | cert-pem-path: web/s-cert.pem
9 | cert-key-path: web/s-cert.key
10 |
11 | datasource:
12 | # 数据库类型:sqlite、mysql
13 | type: sqlite
14 | host: playxy.cn
15 | port: 3306
16 | username: root
17 | password: "@ai-mysql"
18 | db-suffix: beta_test
19 | table-suffix: ""
20 |
--------------------------------------------------------------------------------
/controller/auth_controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/kataras/iris/v12"
6 | "github.com/kataras/iris/v12/mvc"
7 | "github.com/kataras/iris/v12/sessions"
8 | "github.com/mojocn/base64Captcha"
9 | "github.com/sirupsen/logrus"
10 | "natok-server/dsmapper"
11 | "natok-server/model"
12 | "natok-server/model/vo"
13 | "natok-server/support"
14 | "strings"
15 | )
16 |
17 | // AuthController struct 用户认证 - 控制层
18 | type AuthController struct {
19 | Ctx iris.Context
20 | Session *sessions.Session
21 | }
22 |
23 | func (c *AuthController) BeforeActivation(b mvc.BeforeActivation) {
24 | b.Handle("GET", "/verifyCode", "VerifyCodeHandler")
25 | b.Handle("POST", "/user/login", "Login")
26 | b.Handle("POST", "/user/logout", "Logout")
27 | b.Handle("GET", "/user/info", "UserInfo")
28 | b.Handle("POST", "/user/chgPwd", "ChangePassword")
29 | }
30 |
31 | // VerifyCodeHandler 生成图形验证码
32 | func (c *AuthController) VerifyCodeHandler() {
33 | driver := support.NewDriver().ConvertFonts()
34 | newCaptcha := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
35 | _, content, answer := newCaptcha.Driver.GenerateIdQuestionAnswer()
36 | item, _ := newCaptcha.Driver.DrawCaptcha(content)
37 | item.WriteTo(c.Ctx.ResponseWriter())
38 | c.Session.Set(support.CaptchaId, answer)
39 | return
40 | }
41 |
42 | // Login 登录:设置用户session
43 | func (c *AuthController) Login() mvc.Result {
44 | // 登录信息认证
45 | user := new(model.NatokUser)
46 | _ = c.Ctx.ReadJSON(user)
47 |
48 | // TODO 验证码非正常情况
49 | if code := c.Session.Get(support.CaptchaId); code == nil || code == "" {
50 | return vo.TipErrorMsg("当前不支持跨域;先关闭验证码再试试!")
51 | } else if len(user.Code) == 0 {
52 | return vo.TipErrorMsg("请输入验证码!")
53 | } else if strings.ToLower(code.(string)) != strings.ToLower(user.Code) {
54 | return vo.TipErrorMsg("验证码输入错误!")
55 | }
56 |
57 | if nil == dsmapper.GetUser(user) {
58 | return vo.TipErrorMsg("账号或密码错误!")
59 | }
60 |
61 | session := support.SessionsManager.Start(c.Ctx)
62 | session.Set(support.SessionKey, "OK")
63 | session.Set(support.SessionUserId, user.Id)
64 |
65 | marshal, _ := json.Marshal(user)
66 | logrus.Println(string(marshal))
67 | return vo.TipResult(map[string]string{"token": "natok-token"})
68 | }
69 |
70 | // Logout 登出:删除用户session
71 | func (c *AuthController) Logout() mvc.Result {
72 | session := support.SessionsManager.Start(c.Ctx)
73 | session.Clear()
74 | return vo.TipResult("success")
75 | }
76 |
77 | // UserInfo 用户信息
78 | func (c *AuthController) UserInfo() mvc.Result {
79 | ret := map[string]interface{}{
80 | "introduction": "Natok Manager",
81 | "avatar": "/static/img/avatar.png",
82 | "name": "Natok",
83 | "roles": []string{"admin"},
84 | }
85 | return vo.TipResult(ret)
86 | }
87 |
88 | // ChangePassword 修改密码
89 | func (c *AuthController) ChangePassword() mvc.Result {
90 | session := support.SessionsManager.Start(c.Ctx)
91 | if userId, err := session.GetInt64(support.SessionUserId); err == nil {
92 | oldPassword := c.Ctx.URLParam("oldPassword")
93 | newPassword := c.Ctx.URLParam("newPassword")
94 | if oldPassword == "" || newPassword == "" {
95 | return vo.TipErrorMsg("请输入原密码和新密码!")
96 | }
97 | if dsmapper.ChangePassword(userId, oldPassword, newPassword) {
98 | return vo.TipResult("success")
99 | } else {
100 | return vo.TipErrorMsg("原密码错误!")
101 | }
102 | } else {
103 | return vo.TipErrorMsg("请先登录!")
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/controller/client_controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/kataras/iris/v12"
5 | "github.com/kataras/iris/v12/mvc"
6 | "github.com/kataras/iris/v12/sessions"
7 | "natok-server/model/vo"
8 | "natok-server/service"
9 | )
10 |
11 | // ClientController struct 客户端 - 控制层
12 | type ClientController struct {
13 | Ctx iris.Context
14 | Session *sessions.Session
15 | Service *service.ClientService
16 | }
17 |
18 | func (c *ClientController) BeforeActivation(b mvc.BeforeActivation) {
19 | b.Handle("GET", "/client/list", "ClientList")
20 | b.Handle("GET", "/client/get", "ClientGet")
21 | b.Handle("PUT", "/client/save", "ClientSave")
22 | b.Handle("POST", "/client/switch", "ClientSwitch")
23 | b.Handle("POST", "/client/validate", "ClientValidate")
24 | b.Handle("DELETE", "/client/del", "ClientDelete")
25 | b.Handle("GET", "/client/keys", "ClientKey")
26 | }
27 |
28 | // ClientList 列表分页
29 | func (c *ClientController) ClientList() mvc.Result {
30 | wd := c.Ctx.URLParam("wd")
31 | page := c.Ctx.URLParamIntDefault("page", 1)
32 | limit := c.Ctx.URLParamIntDefault("limit", 10)
33 | ret := c.Service.ClientQuery(wd, page, limit)
34 | return vo.TipResult(ret)
35 | }
36 |
37 | // ClientGet 详情
38 | func (c *ClientController) ClientGet() mvc.Result {
39 | clientId := c.Ctx.URLParamInt64Default("clientId", 0)
40 | //非正常情况,返回错误消息
41 | if clientId <= 0 {
42 | return vo.TipErrorMsg("parameter error")
43 | }
44 | ret := c.Service.ClientGet(clientId)
45 | return vo.TipResult(ret)
46 | }
47 |
48 | // ClientSave 保存
49 | func (c *ClientController) ClientSave() mvc.Result {
50 | clientId := c.Ctx.URLParamInt64Default("clientId", 0)
51 | accessKey := c.Ctx.URLParam("accessKey")
52 | clientName := c.Ctx.URLParam("clientName")
53 | err := c.Service.ClientSave(clientId, clientName, accessKey)
54 | return vo.TipMsg(err)
55 | }
56 |
57 | // ClientDelete 删除
58 | func (c *ClientController) ClientDelete() mvc.Result {
59 | clientId := c.Ctx.URLParamInt64Default("clientId", 0)
60 | accessKey := c.Ctx.URLParam("accessKey")
61 | //非正常情况,返回错误消息
62 | if clientId <= 0 || accessKey == "" {
63 | return vo.TipErrorMsg("parameter error")
64 | }
65 | err := c.Service.ClientDelete(clientId, accessKey)
66 | return vo.TipMsg(err)
67 | }
68 |
69 | // ClientKey 获取客户端的name+accessKey的集合
70 | func (c *ClientController) ClientKey() mvc.Result {
71 | ret := c.Service.ClientKeys()
72 | return vo.TipResult(ret)
73 | }
74 |
75 | // ClientValidate 校验
76 | func (c *ClientController) ClientValidate() mvc.Result {
77 | clientId := c.Ctx.URLParamInt64Default("clientId", 0)
78 | value := c.Ctx.URLParam("value")
79 | level := c.Ctx.URLParamInt32Default("type", 0)
80 | ret := c.Service.ClientValidate(clientId, value, level)
81 | return vo.TipResult(ret)
82 | }
83 |
84 | // ClientSwitch 启用与停用
85 | func (c *ClientController) ClientSwitch() mvc.Result {
86 | clientId := c.Ctx.URLParamInt64Default("clientId", 0)
87 | enabled := c.Ctx.URLParamIntDefault("enabled", 0)
88 | accessKey := c.Ctx.URLParam("accessKey")
89 | //非正常情况,返回错误消息
90 | if clientId <= 0 || accessKey == "" || 0 > enabled || enabled > 1 {
91 | return vo.TipErrorMsg("parameter error")
92 | }
93 | err := c.Service.ClientSwitch(clientId, accessKey, int8(enabled))
94 | return vo.TipMsg(err)
95 | }
96 |
--------------------------------------------------------------------------------
/controller/dashboard_controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/kataras/iris/v12"
5 | "github.com/kataras/iris/v12/mvc"
6 | "github.com/kataras/iris/v12/sessions"
7 | "natok-server/model/vo"
8 | "natok-server/service"
9 | )
10 |
11 | // ReportController struct 报表 - 控制层
12 | type ReportController struct {
13 | Ctx iris.Context
14 | Session *sessions.Session
15 | Service *service.ReportService
16 | }
17 |
18 | func (c *ReportController) BeforeActivation(b mvc.BeforeActivation) {
19 | b.Handle("GET", "/dashboard/state", "ClientState")
20 | }
21 |
22 | // ClientState 统计客户端状态
23 | func (c *ReportController) ClientState() mvc.Result {
24 | ret := make(map[string]interface{}, 0)
25 | ret["stream"] = c.Service.StreamState()
26 | ret["client"] = c.Service.ClientState()
27 | ret["port"] = c.Service.PortState()
28 | ret["protocol"] = c.Service.ProtocolState()
29 | ret["run"] = c.Service.RunningState()
30 | return vo.TipResult(ret)
31 | }
32 |
--------------------------------------------------------------------------------
/controller/port_controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/kataras/iris/v12"
5 | "github.com/kataras/iris/v12/mvc"
6 | "github.com/kataras/iris/v12/sessions"
7 | "natok-server/model"
8 | "natok-server/model/vo"
9 | "natok-server/service"
10 | )
11 |
12 | // PortController struct 端口 - 控制层
13 | type PortController struct {
14 | Ctx iris.Context
15 | Session *sessions.Session
16 | Service *service.PortService
17 | }
18 |
19 | func (c *PortController) BeforeActivation(b mvc.BeforeActivation) {
20 | b.Handle("GET", "/port/list", "PortList")
21 | b.Handle("GET", "/port/get", "PortGet")
22 | b.Handle("PUT", "/port/save", "PortSave")
23 | b.Handle("POST", "/port/validate", "PortValidate")
24 | b.Handle("POST", "/port/switch", "PortSwitch")
25 | b.Handle("DELETE", "/port/del", "PortDelete")
26 | }
27 |
28 | // PortList 列表分页
29 | func (c *PortController) PortList() mvc.Result {
30 | wd := c.Ctx.URLParam("wd")
31 | page := c.Ctx.URLParamIntDefault("page", 1)
32 | limit := c.Ctx.URLParamIntDefault("limit", 10)
33 | ret := c.Service.QueryPort(wd, page, limit)
34 | return vo.TipResult(ret)
35 | }
36 |
37 | // PortGet 详情
38 | func (c *PortController) PortGet() mvc.Result {
39 | portId := c.Ctx.URLParamInt64Default("portId", 0)
40 | if ret, err := c.Service.GetPort(portId); err == nil {
41 | return vo.TipResult(ret)
42 | } else {
43 | return vo.TipMsg(err)
44 | }
45 | }
46 |
47 | // PortSave 保存
48 | func (c *PortController) PortSave() mvc.Result {
49 | item := new(model.NatokPort)
50 | if err := c.Ctx.ReadJSON(item); err != nil {
51 | return vo.TipMsg(err)
52 | }
53 | if item.AccessKey == "" || item.Intranet == "" || item.PortNum == 0 {
54 | return vo.TipErrorMsg("parameter error")
55 | }
56 | err := c.Service.SavePort(item)
57 | return vo.TipMsg(err)
58 | }
59 |
60 | // PortDelete 删除
61 | func (c *PortController) PortDelete() mvc.Result {
62 | portId := c.Ctx.URLParamInt64Default("portId", 0)
63 | accessKey := c.Ctx.URLParam("accessKey")
64 | //非正常情况,返回错误消息
65 | if portId <= 0 || accessKey == "" {
66 | return vo.TipErrorMsg("parameter error")
67 | }
68 | err := c.Service.DeletePort(portId, accessKey)
69 | return vo.TipMsg(err)
70 | }
71 |
72 | // PortSwitch 启用或停用
73 | func (c *PortController) PortSwitch() mvc.Result {
74 | portId := c.Ctx.URLParamInt64Default("portId", 0)
75 | enabled := c.Ctx.URLParamIntDefault("enabled", 0)
76 | accessKey := c.Ctx.URLParam("accessKey")
77 | //非正常情况,返回错误消息
78 | if portId <= 0 || accessKey == "" {
79 | return vo.TipErrorMsg("parameter error")
80 | }
81 | err := c.Service.SwitchPort(portId, accessKey, int8(enabled))
82 | return vo.TipMsg(err)
83 | }
84 |
85 | // PortValidate 校验
86 | func (c *PortController) PortValidate() mvc.Result {
87 | portId := c.Ctx.URLParamInt64Default("portId", 0)
88 | portNum := c.Ctx.URLParamIntDefault("portNum", 0)
89 | protocol := c.Ctx.URLParamDefault("protocol", "tcp")
90 | ret := c.Service.ValidatePort(portId, portNum, protocol)
91 | return vo.TipResult(ret)
92 | }
93 |
--------------------------------------------------------------------------------
/controller/tag_controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/kataras/iris/v12"
5 | "github.com/kataras/iris/v12/mvc"
6 | "github.com/kataras/iris/v12/sessions"
7 | "natok-server/model"
8 | "natok-server/model/vo"
9 | "natok-server/service"
10 | )
11 |
12 | // TagController struct - 控制层
13 | type TagController struct {
14 | Ctx iris.Context
15 | Session *sessions.Session
16 | Service *service.TagService
17 | }
18 |
19 | func (c *TagController) BeforeActivation(b mvc.BeforeActivation) {
20 | b.Handle("GET", "/tag/list", "TagList")
21 | b.Handle("GET", "/tag/get", "TagGet")
22 | b.Handle("PUT", "/tag/save", "TagSave")
23 | b.Handle("POST", "/tag/switch", "TagSwitch")
24 | b.Handle("DELETE", "/tag/del", "TagDelete")
25 | }
26 |
27 | // TagList 列表分页
28 | func (c *TagController) TagList() mvc.Result {
29 | wd := c.Ctx.URLParam("wd")
30 | page := c.Ctx.URLParamIntDefault("page", 1)
31 | limit := c.Ctx.URLParamIntDefault("limit", 10)
32 | ret := c.Service.QueryPageTag(wd, page, limit)
33 | return vo.TipResult(ret)
34 | }
35 |
36 | // TagGet 详情
37 | func (c *TagController) TagGet() mvc.Result {
38 | tagId := c.Ctx.URLParamInt64Default("tagId", 0)
39 | if ret, err := c.Service.GetTag(tagId); err == nil {
40 | return vo.TipResult(ret)
41 | } else {
42 | return vo.TipMsg(err)
43 | }
44 | }
45 |
46 | // TagSave 保存
47 | func (c *TagController) TagSave() mvc.Result {
48 | item := new(model.NatokTag)
49 | if err := c.Ctx.ReadJSON(item); err != nil {
50 | return vo.TipMsg(err)
51 | }
52 | if item.TagName == "" {
53 | return vo.TipErrorMsg("parameter error")
54 | }
55 | err := c.Service.SaveTag(item)
56 | return vo.TipMsg(err)
57 | }
58 |
59 | // TagSwitch 启用或停用
60 | func (c *TagController) TagSwitch() mvc.Result {
61 | tagId := c.Ctx.URLParamInt64Default("tagId", 0)
62 | enabled := c.Ctx.URLParamIntDefault("enabled", 0)
63 | //非正常情况,返回错误消息
64 | if tagId <= 0 {
65 | return vo.TipErrorMsg("parameter error")
66 | }
67 | err := c.Service.SwitchTag(tagId, int8(enabled))
68 | return vo.TipMsg(err)
69 | }
70 |
71 | // TagDelete 删除
72 | func (c *TagController) TagDelete() mvc.Result {
73 | tagId := c.Ctx.URLParamInt64Default("tagId", 0)
74 | //非正常情况,返回错误消息
75 | if tagId <= 0 {
76 | return vo.TipErrorMsg("parameter error")
77 | }
78 | err := c.Service.DeleteTag(tagId)
79 | return vo.TipMsg(err)
80 | }
81 |
--------------------------------------------------------------------------------
/core/constant.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "sync"
4 |
5 | // 关键字常量
6 | const (
7 | Protocol = "://"
8 | Sqlite = "sqlite"
9 | Mysql = "mysql"
10 | Empty = ""
11 | )
12 |
13 | // NATOK网络转发类型
14 | const (
15 | Tcp = "tcp"
16 | Udp = "udp"
17 | Http = "http"
18 | Https = "https"
19 | Ssh = "ssh"
20 | Ftp = "ftp"
21 | Database = "data base"
22 | Desktop = "remote desktop"
23 | )
24 |
25 | // 数据包常量
26 | const (
27 | Uint8Size = 1
28 | Uint16Size = 2
29 | Uint32Size = 4
30 | Uint64Size = 8
31 | MaxPacketSize = 4 * 1 << 20 // 最大数据包大小为最大数据包大小为 4M
32 | )
33 |
34 | // 消息类型常量
35 | const (
36 | TypeAuth = 0x01 //验证消息以检查访问密钥是否正确
37 | typeNoAvailablePort = 0x02 //访问密钥没有可用端口
38 | TypeConnectNatok = 0xa1 //连接到NATOK服务
39 | TypeConnectIntra = 0xa2 //连接到内部服务
40 | TypeDisconnect = 0x04 //断开
41 | TypeTransfer = 0x05 //数据传输
42 | TypeIsInuseKey = 0x06 //客户端秘钥已在其他客户端使用
43 | TypeHeartbeat = 0x07 //心跳
44 | TypeDisabledAccessKey = 0x08 //禁用的访问密钥
45 | TypeDisabledTrialClient = 0x09 //禁用的试用客户端
46 | TypeInvalidKey = 0x10 //无效的访问密钥
47 | )
48 |
49 | // IsEmpty 是否为空
50 | func IsEmpty(m *sync.Map) bool {
51 | ifBool := true
52 | m.Range(func(_, _ interface{}) bool {
53 | ifBool = false
54 | return false
55 | })
56 | return ifBool
57 | }
58 |
59 | // IsNotEmpty 是否不为空
60 | func IsNotEmpty(m *sync.Map) bool {
61 | return !IsEmpty(m)
62 | }
63 |
64 | // GetLen 获取长度
65 | func GetLen(m *sync.Map) int {
66 | counter := 0
67 | m.Range(func(_, _ interface{}) bool {
68 | counter++
69 | return true
70 | })
71 | return counter
72 | }
73 |
--------------------------------------------------------------------------------
/core/core_handler.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "net"
6 | "sync"
7 | "time"
8 | )
9 |
10 | var (
11 | ClientManage sync.Map //Natok客户端(AccessKey,*ClientBlocking)
12 | ConnectManage sync.Map //外部请求服务(AccessKey,*ConnectBlocking)
13 | ChanClientSate = make(chan ChanClient, 100)
14 | )
15 |
16 | // ConnectBlocking struct 连接
17 | type ConnectBlocking struct {
18 | PortSignMap sync.Map //映射签名
19 | AccessIdMap sync.Map //连接句柄
20 | }
21 |
22 | // ClientBlocking struct 客户端通道对象
23 | type ClientBlocking struct {
24 | Enabled bool //是否可用
25 | AccessKey string //客户端秘钥
26 | NatokPool *ConnectPool //客户端连接池
27 | NatokHandler *ConnectHandler //客户端连接句柄
28 | PortListener sync.Map //PortSign -> *PortMapping
29 | }
30 |
31 | // DualListener struct TCP、UDP监听器
32 | type DualListener struct {
33 | net.Listener
34 | net.PacketConn
35 | }
36 |
37 | // ChanClient struct C-S连接状态
38 | type ChanClient struct {
39 | AccessKey string
40 | State int8
41 | }
42 |
43 | // PortMapping struct 端口映射对象
44 | type PortMapping struct {
45 | Enabled bool //是否可用
46 | AccessKey string //访问秘钥
47 | PortSign string //映射签名
48 | PortScope string //监听范围
49 | PortNum int //访问端口
50 | Intranet string //转发地址
51 | Protocol string //协议类型
52 | Whitelist []string //开放名单
53 | Listener *DualListener //ServerListener
54 | ConnHandler *ConnectHandler //连接句柄
55 | }
56 |
57 | // ConnectHandler struct 通道链接载体
58 | type ConnectHandler struct {
59 | ReadTime time.Time //读取时间
60 | WriteTime time.Time //写入时间
61 | Active bool //是否活跃
62 | primary bool //核心通道
63 | ReadBuf []byte //读取的内容
64 | Conn *DualConn //连接通道
65 | MsgHandler MsgHandler //消息句柄
66 | ConnHandler *ConnectHandler //连接句柄
67 | }
68 |
69 | // DualConn struct TCP、UDP连接器
70 | type DualConn struct {
71 | net.Conn
72 | *Packet
73 | }
74 |
75 | // Packet UDP
76 | type Packet struct {
77 | net.PacketConn
78 | net.Addr
79 | }
80 |
81 | // Message 内部通信消息体
82 | type Message struct {
83 | Type byte //消息类型
84 | Serial string //消息序列
85 | Net string //网络类型
86 | Uri string //消息头
87 | Data []byte //消息体
88 | }
89 |
90 | // MsgHandler interface 消息处理接口
91 | type MsgHandler interface {
92 | Encode(interface{}) []byte //加密
93 | Decode([]byte) (interface{}, int) //解密
94 | Receive(*ConnectHandler, interface{}) //接收
95 | Close(*ConnectHandler) //关闭
96 | }
97 |
98 | // Write ConnectHandler 消息写入
99 | func (c *ConnectHandler) Write(msg interface{}) {
100 | if c.MsgHandler == nil {
101 | return
102 | }
103 | data := c.MsgHandler.Encode(msg)
104 | c.WriteTime = time.Now()
105 | // TCP
106 | if conn := c.Conn.Conn; conn != nil {
107 | if _, err := conn.Write(data); err != nil {
108 | logrus.Errorf("%v", err.Error())
109 | }
110 | }
111 | // UDP
112 | if packet := c.Conn.Packet; packet != nil {
113 | if _, err := packet.WriteTo(data, packet.Addr); err != nil {
114 | logrus.Errorf("%v", err.Error())
115 | }
116 | }
117 | }
118 |
119 | // NatokClient 客户连接监听
120 | func NatokClient(listener net.Listener) {
121 | for {
122 | accept, err := listener.Accept()
123 | if err != nil {
124 | logrus.Errorf("Natok client listen failed! %s, %v", listener.Addr(), err)
125 | continue
126 | }
127 | go func(accept net.Conn) {
128 | handler := &ConnectHandler{Conn: &DualConn{Conn: accept}}
129 | handler.Listen(&NatokConnectHandler{ConnHandler: handler})
130 | _ = accept.Close()
131 | }(accept)
132 | }
133 | }
134 |
135 | // Listen 连接请求监听
136 | func (c *ConnectHandler) Listen(msgHandler interface{}) {
137 | defer func() {
138 | c.Active = false
139 | if err := recover(); err != nil {
140 | //debug.PrintStack()
141 | c.MsgHandler.Close(c)
142 | logrus.Warn(err)
143 | }
144 | }()
145 | c.Active = true
146 | c.ReadTime = time.Now()
147 | c.MsgHandler = msgHandler.(MsgHandler)
148 |
149 | for c.Active {
150 | // 接收数据 tcp buffer size 16kb
151 | buf := make([]byte, 1024*16)
152 | if c.ReadBuf != nil && len(c.ReadBuf) > MaxPacketSize {
153 | logrus.Warn("This conn is error ! Packet max than 4M !")
154 | c.MsgHandler.Close(c)
155 | break
156 | }
157 | // 检查连接是否为 nil
158 | if c.Conn == nil || c.Conn.Conn == nil {
159 | logrus.Error("This conn is nil")
160 | c.MsgHandler.Close(c)
161 | break
162 | }
163 | // 从连接读取数据
164 | n, err := c.Conn.Conn.Read(buf)
165 | if err != nil || n == 0 {
166 | if err != nil && err.Error() != "EOF" {
167 | logrus.Errorf("%v", err.Error())
168 | }
169 | c.MsgHandler.Close(c)
170 | break
171 | }
172 |
173 | c.ReadTime = time.Now()
174 | if c.ReadBuf == nil {
175 | c.ReadBuf = buf[0:n]
176 | } else {
177 | c.ReadBuf = append(c.ReadBuf, buf[0:n]...)
178 | }
179 |
180 | for {
181 | msg, n := c.MsgHandler.Decode(c.ReadBuf)
182 | if msg == nil {
183 | break
184 | }
185 | c.MsgHandler.Receive(c, msg)
186 | c.ReadBuf = c.ReadBuf[n:]
187 | if len(c.ReadBuf) == 0 {
188 | break
189 | }
190 | }
191 | // 在包未读取完成时,需要二次读取
192 | if len(c.ReadBuf) > 0 {
193 | buf := make([]byte, len(c.ReadBuf))
194 | copy(buf, c.ReadBuf)
195 | c.ReadBuf = buf
196 | }
197 | }
198 | }
199 |
200 | // PacketRead 连接请求监听
201 | func (c *ConnectHandler) PacketRead(msgHandler MsgHandler, buf []byte, n int) {
202 | defer func() {
203 | c.Active = false
204 | if err := recover(); err != nil {
205 | //debug.PrintStack()
206 | c.MsgHandler.Close(c)
207 | logrus.Warn(err)
208 | }
209 | }()
210 | c.Active = true
211 | c.ReadTime = time.Now()
212 | c.MsgHandler = msgHandler
213 | c.ReadTime = time.Now()
214 | c.ReadBuf = buf[0:n]
215 |
216 | for {
217 | msg, n := c.MsgHandler.Decode(c.ReadBuf)
218 | if msg == nil {
219 | break
220 | }
221 | c.MsgHandler.Receive(c, msg)
222 | c.ReadBuf = c.ReadBuf[n:]
223 | if len(c.ReadBuf) == 0 {
224 | break
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/core/extra_handler.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "github.com/sirupsen/logrus"
6 | "natok-server/util"
7 | "net"
8 | "strings"
9 | "time"
10 | )
11 |
12 | // ExtraConnectHandler struct 外部端口请求服务处理
13 | type ExtraConnectHandler struct {
14 | AccessId string //接受连接的ID
15 | AccessKey string //绑定的客户端秘钥
16 | Sign string //映射签名
17 | Protocol string //传输协议
18 | Port int //绑定的端口
19 | activated bool //已经激活
20 | ChanActive chan bool //激活通道
21 | ConnHandler *ConnectHandler
22 | }
23 |
24 | // Encode 编码消息
25 | func (e *ExtraConnectHandler) Encode(msg interface{}) []byte {
26 | if msg == nil {
27 | return []byte{}
28 | }
29 | return msg.([]byte)
30 | }
31 |
32 | // Decode 解码消息
33 | func (e *ExtraConnectHandler) Decode(buf []byte) (interface{}, int) {
34 | return buf, len(buf)
35 | }
36 |
37 | // Close 关闭处理
38 | func (e *ExtraConnectHandler) Close(handler *ConnectHandler) {
39 | // 通知客户端关闭连接
40 | if natokHandler := handler.ConnHandler; natokHandler != nil {
41 | natokHandler.Write(Message{Type: TypeDisconnect, Serial: natokHandler.MsgHandler.(*NatokConnectHandler).Serial, Uri: e.AccessId})
42 | handler.ConnHandler = nil
43 | }
44 | // 关闭连接
45 | if handler.Conn != nil {
46 | if conn := handler.Conn.Conn; conn != nil {
47 | _ = conn.Close()
48 | }
49 | if packet := handler.Conn.Packet; packet != nil {
50 | packet.Addr = nil
51 | }
52 | }
53 | handler.Active = false
54 | }
55 |
56 | // Receive 请求接收
57 | func (e *ExtraConnectHandler) Receive(handler *ConnectHandler, data interface{}) {
58 | // 判断客户端是否已连接,未连接则等待连接
59 | if e.activated == false {
60 | select {
61 | case active := <-e.ChanActive:
62 | if active {
63 | e.activated = true
64 | break
65 | }
66 | case <-time.After(time.Second * 15):
67 | logrus.Errorf("Receive AccessKey %s -> %s, PortNum %d wait 15 second, connection timeout. ", e.AccessKey, e.AccessId, e.Port)
68 | e.Close(handler)
69 | return
70 | }
71 | }
72 | // 将请求转发到客户端
73 | if natokHandler := handler.ConnHandler; natokHandler != nil {
74 | msg := Message{Type: TypeTransfer, Serial: natokHandler.MsgHandler.(*NatokConnectHandler).Serial, Uri: e.AccessId, Data: data.([]byte)}
75 | natokHandler.Write(msg)
76 | }
77 | }
78 |
79 | // Activate 激活(将内部网地址发送给客户端建立连接)
80 | func (e *ExtraConnectHandler) Activate() {
81 | // 若外部端口已完成绑定,激活该请求
82 | ClientManage.Range(func(_, cm any) bool {
83 | ifBool := true
84 | client := cm.(*ClientBlocking)
85 | client.PortListener.Range(func(sign, pm any) bool {
86 | portMapping := pm.(*PortMapping)
87 | if e.Port == portMapping.PortNum {
88 | if nil != client.NatokHandler {
89 | if cn, ifCN := ConnectManage.Load(e.AccessKey); cn != nil && ifCN {
90 | blocking := cn.(*ConnectBlocking)
91 | if connect, ifConnect := blocking.AccessIdMap.Load(e.AccessId); connect != nil && ifConnect {
92 | // 已存在连接通道
93 | ifBool = false
94 | return ifBool
95 | }
96 | // 追加访问通道
97 | if signs, ifSign := blocking.PortSignMap.Load(e.Sign); signs != nil && ifSign {
98 | blocking.PortSignMap.Store(e.Sign, append(signs.([]string), e.AccessId))
99 | } else {
100 | blocking.PortSignMap.Store(e.Sign, []string{e.AccessId})
101 | }
102 | blocking.AccessIdMap.Store(e.AccessId, e)
103 | } else {
104 | // 创建端口连接通道
105 | blocking := &ConnectBlocking{}
106 | blocking.PortSignMap.Store(e.Sign, []string{e.AccessId})
107 | blocking.AccessIdMap.Store(e.AccessId, e)
108 | ConnectManage.Store(e.AccessKey, blocking)
109 | }
110 | // 激活连接通道
111 | e.Protocol = portMapping.Protocol
112 | if e.Protocol != Udp {
113 | e.Protocol = Tcp
114 | }
115 | msg := Message{Type: TypeConnectIntra, Serial: e.ConnHandler.ConnHandler.MsgHandler.(*NatokConnectHandler).Serial, Net: e.Protocol, Uri: e.AccessId, Data: []byte(portMapping.Intranet)}
116 | e.ConnHandler.ConnHandler.Write(msg)
117 | ifBool = false
118 | return ifBool
119 | }
120 | }
121 | return ifBool
122 | })
123 | return ifBool
124 | })
125 | }
126 |
127 | func getExtra(accessKey, accessId string, fn func() *ExtraConnectHandler) *ExtraConnectHandler {
128 | if cn, ifCN := ConnectManage.Load(accessKey); cn != nil && ifCN {
129 | blocking := cn.(*ConnectBlocking)
130 | if connect, ifConnect := blocking.AccessIdMap.Load(accessId); connect != nil && ifConnect {
131 | return connect.(*ExtraConnectHandler)
132 | }
133 | }
134 | return fn()
135 | }
136 |
137 | func udpListen(mapping *PortMapping) error {
138 | // 绑定服务端的端口
139 | packet, err := net.ListenPacket(Udp, util.ToAddress(mapping.PortScope, mapping.PortNum))
140 | if err != nil {
141 | logrus.Errorf("Listen udp port %d failed! %v", mapping.PortNum, err.Error())
142 | return err
143 | }
144 | logrus.Infof("Bind udp listen %s", packet.LocalAddr())
145 | mapping.Listener = &DualListener{PacketConn: packet}
146 | go func(packet net.PacketConn) {
147 | for {
148 | if mapping.Enabled == false {
149 | logrus.Infof("Port %d not enabled, exit udp listen %s", mapping.PortNum, packet.LocalAddr())
150 | _ = packet.Close()
151 | break
152 | }
153 | // 接收数据 udp max packet 64kb
154 | buf := make([]byte, 1024*64)
155 | n, addr, err := packet.ReadFrom(buf)
156 | if err != nil || n == 0 {
157 | logrus.Errorf("%v", err.Error())
158 | break
159 | }
160 | if cm, ifCM := ClientManage.Load(mapping.AccessKey); cm != nil && ifCM {
161 | // 客户端: 启用 && 在线
162 | client := cm.(*ClientBlocking)
163 | if !client.Enabled || client.NatokHandler == nil || !client.NatokHandler.Active {
164 | _ = packet.Close()
165 | break
166 | }
167 | // 限制开放名单
168 | pm, _ := client.PortListener.Load(mapping.PortSign)
169 | if nil == pm || util.NoneMatch(pm.(*PortMapping).Whitelist, func(ip string) bool {
170 | return strings.Contains(addr.String(), ip)
171 | }, false) {
172 | continue
173 | }
174 | sprintf := fmt.Sprintf("Accept connect udp://%s from %s %d", addr.String(), mapping.Protocol, mapping.PortNum)
175 | logrus.Debugf("%s", sprintf)
176 | // 将外部连接与内部连接关联
177 | extra := getExtra(mapping.AccessKey, Udp+Protocol+addr.String(), func() *ExtraConnectHandler {
178 | // 获取内部连接通道
179 | natokHandler := client.NatokPool.Get()
180 | if nil == natokHandler {
181 | logrus.Warnf("%s, Not find natokHandler!", sprintf)
182 | return nil
183 | }
184 | handler := &ConnectHandler{Conn: &DualConn{Packet: &Packet{PacketConn: packet, Addr: addr}}, ConnHandler: natokHandler}
185 | natokHandler.ConnHandler = handler
186 | return &ExtraConnectHandler{
187 | AccessId: Udp + Protocol + addr.String(),
188 | AccessKey: mapping.AccessKey,
189 | Sign: mapping.PortSign,
190 | Port: mapping.PortNum,
191 | ChanActive: make(chan bool, 1),
192 | ConnHandler: handler,
193 | }
194 | })
195 | if extra == nil {
196 | return
197 | }
198 | extra.Activate()
199 | extra.ConnHandler.PacketRead(extra, buf, n)
200 | } else {
201 | logrus.Warnf("Not find accessKey: %s", mapping.AccessKey)
202 | _ = packet.Close()
203 | }
204 | }
205 | }(packet)
206 | return nil
207 | }
208 |
209 | func tcpListen(mapping *PortMapping) error {
210 | // 绑定服务端的端口
211 | listen, err := net.Listen(Tcp, util.ToAddress(mapping.PortScope, mapping.PortNum))
212 | if err != nil {
213 | logrus.Errorf("Listen tcp port %d failed! %v", mapping.PortNum, err.Error())
214 | return err
215 | }
216 | logrus.Infof("Bind tcp listen %s", listen.Addr())
217 | mapping.Listener = &DualListener{Listener: listen}
218 | go func(listener net.Listener) {
219 | // 服务端收到请求
220 | for {
221 | if mapping.Enabled == false {
222 | logrus.Infof("Port %d not enabled, exit tcp listen %s", mapping.PortNum, listener.Addr())
223 | _ = listener.Close()
224 | break
225 | }
226 | accept, err := listener.Accept()
227 | if err != nil {
228 | logrus.Errorf("Accept failed! %v", err.Error())
229 | break
230 | }
231 | // 限制开放名单
232 | if util.NoneMatch(mapping.Whitelist, func(ip string) bool {
233 | return strings.Contains(accept.RemoteAddr().String(), ip)
234 | }, false) {
235 | _ = accept.Close()
236 | continue
237 | }
238 | if cm, ifCM := ClientManage.Load(mapping.AccessKey); cm != nil && ifCM {
239 | // 客户端: 启用 && 在线
240 | client := cm.(*ClientBlocking)
241 | if !client.Enabled || client.NatokHandler == nil || !client.NatokHandler.Active {
242 | _ = accept.Close()
243 | continue
244 | }
245 | go func(accept net.Conn) {
246 | sprintf := fmt.Sprintf("Accept connect tcp://%s from %s %d", accept.RemoteAddr(), mapping.Protocol, mapping.PortNum)
247 | logrus.Debugf(sprintf)
248 | // 将外部连接与内部连接关联
249 | extra := getExtra(mapping.AccessKey, Tcp+Protocol+accept.RemoteAddr().String(), func() *ExtraConnectHandler {
250 | // 获取内部连接通道
251 | natokHandler := client.NatokPool.Get()
252 | if natokHandler == nil {
253 | logrus.Warnf("%s, Not find natokHandler!", sprintf)
254 | _ = accept.Close()
255 | return nil
256 | }
257 | handler := &ConnectHandler{Conn: &DualConn{Conn: accept}, ConnHandler: natokHandler}
258 | natokHandler.ConnHandler = handler
259 | return &ExtraConnectHandler{
260 | ConnHandler: handler,
261 | Sign: mapping.PortSign,
262 | Port: mapping.PortNum,
263 | ChanActive: make(chan bool, 1),
264 | AccessKey: mapping.AccessKey,
265 | AccessId: Tcp + Protocol + accept.RemoteAddr().String(),
266 | }
267 | })
268 | if extra == nil {
269 | return
270 | }
271 | extra.Activate()
272 | extra.ConnHandler.Listen(extra)
273 | _ = accept.Close()
274 | }(accept)
275 | } else {
276 | logrus.Warnf("Not find accessKey: %s", mapping.AccessKey)
277 | _ = accept.Close()
278 | }
279 | }
280 | }(listen)
281 | return nil
282 | }
283 |
284 | // BindPort 绑定端口监听
285 | func BindPort(mapping *PortMapping) error {
286 | if cm, ifCM := ClientManage.Load(mapping.AccessKey); cm != nil && ifCM {
287 | mapping.Enabled = true
288 | client := cm.(*ClientBlocking)
289 | if mapping.Protocol == Udp {
290 | if err := udpListen(mapping); err != nil {
291 | return err
292 | }
293 | } else {
294 | if err := tcpListen(mapping); err != nil {
295 | return err
296 | }
297 | }
298 | client.PortListener.Store(mapping.PortSign, mapping)
299 | }
300 | return nil
301 | }
302 |
303 | // UnBindPort 解除端口绑定
304 | func UnBindPort(mapping *PortMapping) error {
305 | accessKey := mapping.AccessKey
306 | portSign := mapping.PortSign
307 | if cm, ifCM := ClientManage.Load(accessKey); cm != nil && ifCM {
308 | // 端口解绑
309 | client := cm.(*ClientBlocking)
310 | if pm, ifPM := client.PortListener.Load(portSign); pm != nil && ifPM {
311 | portMapping := pm.(*PortMapping)
312 | if nil != portMapping {
313 | portMapping.Enabled = false
314 | if portMapping.Listener != nil {
315 | // TCP
316 | if listen := portMapping.Listener.Listener; listen != nil {
317 | if err := listen.Close(); nil != err {
318 | logrus.Errorf("Unbind tcp port %s, %v", listen.Addr(), err)
319 | return err
320 | }
321 | logrus.Infof("Unbind tcp listen %s", listen.Addr())
322 | }
323 | // UDP
324 | if packet := portMapping.Listener.PacketConn; packet != nil {
325 | if err := packet.Close(); nil != err {
326 | logrus.Errorf("Unbind udp port %s, %v", packet.LocalAddr(), err)
327 | return err
328 | }
329 | logrus.Infof("Unbind udp listen %s", packet.LocalAddr())
330 | }
331 | }
332 | }
333 | }
334 | client.PortListener.Delete(portSign)
335 | }
336 | // 连接清除
337 | if cn, ifCN := ConnectManage.Load(accessKey); cn != nil && ifCN {
338 | blocking := cn.(*ConnectBlocking)
339 | if signs, ifSign := blocking.PortSignMap.Load(portSign); signs != nil && ifSign {
340 | for _, accessId := range signs.([]string) {
341 | if connect, ifConnect := blocking.AccessIdMap.Load(accessId); connect != nil && ifConnect {
342 | extra := connect.(*ExtraConnectHandler)
343 | if nil != extra && nil != extra.ConnHandler {
344 | // TCP
345 | if conn := extra.ConnHandler.Conn.Conn; conn != nil {
346 | _ = conn.Close()
347 | }
348 | // UDP
349 | if packet := extra.ConnHandler.Conn.Packet; packet != nil {
350 | packet.Addr = nil
351 | _ = packet.PacketConn.Close()
352 | }
353 | }
354 | }
355 | blocking.AccessIdMap.Delete(accessId)
356 | }
357 | }
358 | blocking.PortSignMap.Delete(portSign)
359 | }
360 | return nil
361 | }
362 |
--------------------------------------------------------------------------------
/core/natok_handler.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "encoding/binary"
5 | "github.com/sirupsen/logrus"
6 | "natok-server/support"
7 | "time"
8 | )
9 |
10 | // NatokConnectHandler struct NATOK服务端请求服务处理
11 | type NatokConnectHandler struct {
12 | Serial string //序列
13 | AccessId string //接受连接的ID
14 | AccessKey string //绑定的客户端秘钥
15 | Sign string //映射签名
16 | ConnHandler *ConnectHandler
17 | }
18 |
19 | // Close 关闭处理
20 | func (s *NatokConnectHandler) Close(handler *ConnectHandler) {
21 | if handler.primary {
22 | // 客户端离线
23 | ChanClientSate <- ChanClient{AccessKey: s.AccessKey, State: 0}
24 | // 关闭客户端连接池
25 | if cm, ifCM := ClientManage.Load(s.AccessKey); cm != nil && ifCM {
26 | client := cm.(*ClientBlocking)
27 | client.NatokPool.Shutdown()
28 | }
29 | }
30 | handler.Active = false
31 | s.disconnect(handler)
32 | }
33 |
34 | // Encode 编码消息
35 | func (s *NatokConnectHandler) Encode(inMsg interface{}) []byte {
36 | if inMsg == nil {
37 | return []byte{}
38 | }
39 | msg := inMsg.(Message)
40 | serialBytes := []byte(msg.Serial)
41 | netBytes := []byte(msg.Net)
42 | UriBytes := []byte(msg.Uri)
43 | // byte=Uint8Size,3个string=Uint8Size*3,+data
44 | dataLen := Uint8Size + Uint8Size*3 + len(serialBytes) + len(netBytes) + len(UriBytes) + len(msg.Data)
45 | data := make([]byte, Uint32Size, Uint32Size+dataLen)
46 | binary.BigEndian.PutUint32(data, uint32(dataLen))
47 |
48 | data = append(data, msg.Type)
49 | data = append(data, byte(len(serialBytes)))
50 | data = append(data, byte(len(netBytes)))
51 | data = append(data, byte(len(UriBytes)))
52 | data = append(data, serialBytes...)
53 | data = append(data, netBytes...)
54 | data = append(data, UriBytes...)
55 | data = append(data, msg.Data...)
56 | return data
57 | }
58 |
59 | // Decode 解码消息
60 | func (s *NatokConnectHandler) Decode(buf []byte) (interface{}, int) {
61 | headerBytes := buf[0:Uint32Size]
62 | headerLen := binary.BigEndian.Uint32(headerBytes)
63 | // 来自客户端的包,校验完整性。
64 | if uint32(len(buf)) < headerLen+Uint32Size {
65 | return nil, 0
66 | }
67 |
68 | head := int(Uint32Size + headerLen)
69 | body := buf[Uint32Size:head]
70 | serialLen := int(body[Uint8Size])
71 | netLen := int(body[Uint8Size*2])
72 | uriLen := int(body[Uint8Size*3])
73 | msg := Message{
74 | Type: body[0],
75 | Serial: string(body[Uint8Size*4 : Uint8Size*4+serialLen]),
76 | Net: string(body[Uint8Size*4+serialLen : Uint8Size*4+serialLen+netLen]),
77 | Uri: string(body[Uint8Size*4+serialLen+netLen : Uint8Size*4+serialLen+netLen+uriLen]),
78 | Data: body[Uint8Size*4+serialLen+netLen+uriLen:],
79 | }
80 | return msg, head
81 | }
82 |
83 | // Receive 请求接收
84 | func (s *NatokConnectHandler) Receive(handler *ConnectHandler, msgData interface{}) {
85 | msg := msgData.(Message)
86 | switch msg.Type {
87 | case TypeAuth:
88 | s.author(handler, msg)
89 | case TypeConnectNatok:
90 | s.connect(handler, msg)
91 | case TypeConnectIntra:
92 | s.intra(handler, msg)
93 | case TypeTransfer:
94 | s.transfer(handler, msg)
95 | case TypeHeartbeat:
96 | s.heartbeat(handler, msg)
97 | case TypeDisconnect:
98 | s.disconnect(handler)
99 | }
100 | }
101 |
102 | // author Natok客户端认证
103 | func (s *NatokConnectHandler) author(handler *ConnectHandler, msg Message) {
104 | s.AccessKey = msg.Uri
105 | cm, ifCM := ClientManage.Load(s.AccessKey)
106 | // 无效的访问密钥
107 | if cm == nil || !ifCM {
108 | msg.Type = TypeInvalidKey
109 | handler.Write(msg)
110 | s.Close(handler)
111 | return
112 | }
113 | // 客户端启用检查
114 | client := cm.(*ClientBlocking)
115 | if !client.Enabled {
116 | msg.Type = TypeDisabledAccessKey
117 | handler.Write(msg)
118 | logrus.Warnf("The AccessKey [%s] is disabled", s.AccessKey)
119 | }
120 | // 绑定端口检查
121 | if IsEmpty(&client.PortListener) {
122 | msg.Type = typeNoAvailablePort
123 | handler.Write(msg)
124 | logrus.Warnf("The AccessKey [%s] no port available", s.AccessKey)
125 | }
126 |
127 | // 判断是否前面已建立过连接
128 | if client.NatokHandler != nil && client.NatokHandler != handler {
129 | client.NatokHandler.primary = false
130 | msg.Type = TypeIsInuseKey
131 | _, _ = client.NatokHandler.Conn.Write(s.Encode(msg))
132 | logrus.Warnf("The accessKey [%s] use by other natok client %s -> %s", s.AccessKey, client.NatokHandler.Conn.RemoteAddr(), handler.Conn.RemoteAddr())
133 | }
134 | // 标记为主连接
135 | handler.primary = true
136 | client.NatokHandler = handler
137 | ClientManage.Store(s.AccessKey, client)
138 | // 建立连接池
139 | pool := support.AppConf.Natok.Server.ChanPool
140 | client.NatokPool = NewConnectionPool(pool.MinSize, pool.MaxSize, time.Duration(pool.IdleTimeout)*time.Second, handler)
141 | // 客户端上线
142 | ChanClientSate <- ChanClient{AccessKey: s.AccessKey, State: 1}
143 | logrus.Infof("The accessKey [%s] with ports %d in natok client %s online at %s", s.AccessKey, GetLen(&client.PortListener), handler.Conn.RemoteAddr(), time.Now().Format("2006-01-02 15:04:05"))
144 | }
145 |
146 | // connect 建立连接
147 | func (s *NatokConnectHandler) connect(handler *ConnectHandler, msg Message) {
148 | s.Serial = msg.Serial
149 | s.AccessKey = msg.Uri
150 | if s.AccessKey == "" {
151 | logrus.Warn("The AccessKey is empty")
152 | s.Close(handler)
153 | return
154 | }
155 | // 放入连接池
156 | if handler.Active {
157 | if cm, ifCM := ClientManage.Load(s.AccessKey); cm != nil && ifCM {
158 | client := cm.(*ClientBlocking)
159 | client.NatokPool.Accept(handler)
160 | return
161 | }
162 | }
163 | }
164 |
165 | // intra 建立内部连接
166 | func (s *NatokConnectHandler) intra(handler *ConnectHandler, msg Message) {
167 | if extra := handler.ConnHandler; extra != nil {
168 | extra.MsgHandler.(*ExtraConnectHandler).ChanActive <- true
169 | }
170 | }
171 |
172 | // transfer 数据传输
173 | func (s *NatokConnectHandler) transfer(handler *ConnectHandler, msg Message) {
174 | handler.ConnHandler.Write(msg.Data)
175 | }
176 |
177 | // heartbeat Natok客户端心跳
178 | func (s *NatokConnectHandler) heartbeat(handler *ConnectHandler, msg Message) {
179 | // 心跳不改变写入时间
180 | wt := handler.WriteTime
181 | handler.Write(msg)
182 | handler.WriteTime = wt
183 | }
184 |
185 | // disconnect 断开连接
186 | func (s *NatokConnectHandler) disconnect(handler *ConnectHandler) {
187 | // 关闭外部端口连接
188 | extraConnHandler := handler.ConnHandler
189 | if extraConnHandler != nil && extraConnHandler.Conn != nil {
190 | // TCP
191 | if conn := extraConnHandler.Conn.Conn; conn != nil {
192 | _ = conn.Close()
193 | }
194 | // UDP
195 | if packet := extraConnHandler.Conn.Packet; packet != nil {
196 | packet.Addr = nil
197 | }
198 | handler.ConnHandler = nil
199 | logrus.Debugf("Disconnect accessKey %s -> %s ", s.AccessKey, s.Serial)
200 | }
201 | // 放入连接池
202 | if handler.Active {
203 | if cm, ifCM := ClientManage.Load(s.AccessKey); cm != nil && ifCM {
204 | client := cm.(*ClientBlocking)
205 | client.NatokPool.Put(handler)
206 | return
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/core/natok_pool.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "natok-server/util"
6 | "sync/atomic"
7 | "time"
8 | )
9 |
10 | // ConnectPool 连接池结构体
11 | type ConnectPool struct {
12 | shutdownChan chan struct{} // 关闭检查协程的信号
13 | natokHandler *ConnectHandler // 客户端连接句柄
14 | natokChan chan *ConnectHandler // 可复用连接的通道
15 | minSize int // 最小连接数
16 | maxSize int // 最大连接数
17 | current int32 // 当前连接数
18 | idleTimeout time.Duration // 连接空闲时间
19 | }
20 |
21 | // NewConnectionPool 初始化连接池
22 | func NewConnectionPool(minSize, maxSize int, idleTimeout time.Duration, connect *ConnectHandler) *ConnectPool {
23 | p := &ConnectPool{
24 | natokChan: make(chan *ConnectHandler, maxSize),
25 | natokHandler: connect,
26 | maxSize: maxSize,
27 | minSize: minSize,
28 | current: int32(0),
29 | idleTimeout: idleTimeout,
30 | shutdownChan: make(chan struct{}),
31 | }
32 | // 初始化最小连接数
33 | go p.initiate(minSize)
34 | // 启动空闲连接清理协程
35 | go p.cleanIdle()
36 | return p
37 | }
38 |
39 | // Get 取出连接
40 | func (p *ConnectPool) Get() *ConnectHandler {
41 | // 如果连接池中的 就绪连接数 == 最小连接数*0.4,则尝试扩容
42 | if factor := int(float32(p.minSize) * 0.4); len(p.natokChan) == factor {
43 | expand := min(p.minSize-factor+len(p.natokChan), p.maxSize-int(p.current))
44 | if expand > 0 {
45 | go p.initiate(expand)
46 | }
47 | }
48 | // 连接池为空,则创建新的连接
49 | if len(p.natokChan) == 0 {
50 | go p.initiate(2)
51 | }
52 | select {
53 | // 从连接池中获取连接
54 | case handler := <-p.natokChan:
55 | p.increment()
56 | logrus.Debugf("Get from connect %s, ready chan %d, active chan %d",
57 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
58 | )
59 | return handler
60 | // 等待15秒,如果连接池为空则返回nil
61 | case <-time.After(time.Second * 15):
62 | return nil
63 | }
64 | }
65 |
66 | // Put 归还连接
67 | func (p *ConnectPool) Put(handler *ConnectHandler) {
68 | p.decrement()
69 | select {
70 | // 成功归还连接
71 | case p.natokChan <- handler:
72 | logrus.Debugf("Put connect %s, ready chan %d, active chan %d",
73 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
74 | )
75 | // 池满了,丢弃连接
76 | default:
77 | handler.MsgHandler.Close(handler)
78 | logrus.Errorf("Disconnect %s ,ready chan is full %d,active chan %d",
79 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
80 | )
81 | }
82 | }
83 |
84 | // Accept 接入连接
85 | func (p *ConnectPool) Accept(handler *ConnectHandler) {
86 | select {
87 | // 成功接入连接
88 | case p.natokChan <- handler:
89 | logrus.Debugf("Accept connect %s, ready chan %d, active chan %d",
90 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
91 | )
92 | // 池满了,丢弃连接
93 | default:
94 | handler.MsgHandler.Close(handler)
95 | logrus.Errorf("Disconnect %s ,ready chan is full %d,active chan %d",
96 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
97 | )
98 | }
99 | }
100 |
101 | // Shutdown 关闭连接池并停止清理
102 | func (p *ConnectPool) Shutdown() {
103 | close(p.shutdownChan)
104 | }
105 |
106 | // cleanIdle 清理空闲连接并保持最小存活数
107 | func (p *ConnectPool) cleanIdle() {
108 | // 每分钟检查一次连接池中的空闲连接
109 | ticker := time.NewTicker(30 * time.Second)
110 | defer ticker.Stop()
111 | for {
112 | select {
113 | case <-ticker.C:
114 | if p.minSize >= len(p.natokChan) {
115 | break
116 | }
117 | now := time.Now()
118 | // 检查空闲连接是否超过超时时间,并进行清理
119 | for count := len(p.natokChan); count > p.minSize; count-- {
120 | select {
121 | case handler := <-p.natokChan:
122 | if now.Sub(handler.WriteTime) > p.idleTimeout {
123 | // 连接空闲超过超时时间,关闭并减少池中的连接数量
124 | handler.MsgHandler.Close(handler)
125 | logrus.Debugf("Idle close connect %s, ready chan %d, active chan %d",
126 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan), p.current,
127 | )
128 | } else {
129 | // 连接仍然活跃,放回连接池
130 | p.natokChan <- handler
131 | }
132 | default:
133 | break
134 | }
135 | }
136 | case <-p.shutdownChan:
137 | close(p.natokChan)
138 | for handler := range p.natokChan {
139 | handler.MsgHandler.Close(handler)
140 | logrus.Debugf("Shutdown close connect %s, ready chan %d",
141 | handler.MsgHandler.(*NatokConnectHandler).Serial, len(p.natokChan),
142 | )
143 | }
144 | return
145 | }
146 | }
147 | }
148 |
149 | // initiate 初始化连接
150 | func (p *ConnectPool) initiate(size int) {
151 | for i := 0; i < size; i++ {
152 | msg := Message{Type: TypeConnectNatok, Serial: util.GenerateCode(Empty), Data: []byte("initiate")}
153 | p.natokHandler.Write(msg)
154 | }
155 | }
156 |
157 | // increment 增加计数
158 | func (p *ConnectPool) increment() {
159 | atomic.AddInt32(&p.current, +1)
160 | }
161 |
162 | // decrement 减少计数
163 | func (p *ConnectPool) decrement() {
164 | atomic.AddInt32(&p.current, -1)
165 | }
166 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | natok-server:
3 | container_name: natok-server
4 | network_mode: host
5 | restart: always
6 | image: debian:12
7 | command: /dist/natok-server
8 | volumes:
9 | - /data/docker/natok-server/dist:/dist
10 |
11 | # 常用命令汇总
12 | # docker-compose up -d # 启动所有服务并后台运行
13 | # docker-compose stop # 停止运行中的容器
14 | # docker-compose down # 停止容器并移除资源
15 | # docker-compose restart # 重启所有容器
16 | # docker-compose logs -f # 实时查看日志
17 | # docker-compose ps # 列出所有服务的状态
18 | # docker-compose build # 重新构建所有服务的镜像
--------------------------------------------------------------------------------
/dsmapper/client_rep.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "github.com/go-xorm/xorm"
5 | "github.com/sirupsen/logrus"
6 | "natok-server/model"
7 | "strconv"
8 | )
9 |
10 | // 表名-NatokClient
11 | func tableNameNatokClient() string {
12 | return Engine.TableName(new(model.NatokClient))
13 | }
14 |
15 | // ClientFindAll 查询客户端
16 | func (d *DsMapper) ClientFindAll() (ret []model.NatokClient) {
17 | if err := d.getSession().Where("deleted=0").Find(&ret); err != nil {
18 | logrus.Errorf("%v", err.Error())
19 | return nil
20 | }
21 | return ret
22 | }
23 |
24 | // //////////////////////////////////////////////////////////////////
25 | // ClientQueryByNameOrKey 查询客户端根据 kw
26 | func (d *DsMapper) ClientQueryByNameOrKey(kw string) *model.NatokClient {
27 | var session *xorm.Session
28 | if kw != "" {
29 | session = d.getSession().Where("deleted=0 and(client_name like CONCAT('%',?,'%') or access_key=?)", kw, kw)
30 | } else {
31 | session = d.getSession()
32 | }
33 | item := new(model.NatokClient)
34 | if ok, err := session.Get(item); !ok {
35 | if err != nil {
36 | logrus.Errorf("%v", err.Error())
37 | }
38 | return nil
39 | }
40 | return item
41 | }
42 |
43 | // ClientQuery 查询分页数据+总条数
44 | func (d *DsMapper) ClientQuery(wd string, page, limit int) (ret []model.NatokClient, total int64) {
45 | var err error
46 | session := d.getSession()
47 | for i := 0; i <= 1; i++ {
48 | if wd != "" {
49 | session.Where("deleted=0 and (client_id=? or client_name like CONCAT('%',?,'%') or access_key=?)", wd, wd, wd)
50 | } else {
51 | session.Where("deleted=0")
52 | }
53 | session.Desc("enabled", "state")
54 | session.Asc("client_id")
55 | switch i {
56 | case 0: //查询分页数据
57 | if err = session.Limit(limit, (page-1)*limit).Find(&ret); err != nil {
58 | logrus.Errorf("%v", err.Error())
59 | return nil, total
60 | }
61 | case 1: //查询总条数
62 | if total, err = session.Count(new(model.NatokClient)); err != nil {
63 | logrus.Errorf("%v", err.Error())
64 | return nil, total
65 | }
66 | }
67 | }
68 | return ret, total
69 | }
70 |
71 | // ClientGetById 获取客户端
72 | func (d *DsMapper) ClientGetById(clientId int64) *model.NatokClient {
73 | ret := new(model.NatokClient)
74 | if ok, err := d.getSession().Where("deleted=0 and client_id=?", clientId).Get(ret); !ok {
75 | if err != nil {
76 | logrus.Errorf("%v", err.Error())
77 | }
78 | return nil
79 | }
80 | return ret
81 | }
82 |
83 | // ClientStateReset 重置客户端状态
84 | func (d *DsMapper) ClientStateReset() {
85 | update, err := d.getSession().Cols("state").Where("state=?", 1).Update(new(model.NatokClient))
86 | if nil != err {
87 | logrus.Errorf("%v", err.Error())
88 | }
89 | if update > 0 {
90 | logrus.Info("Reset client state ", update, " row")
91 | }
92 | }
93 |
94 | // ClientSaveUp 插入或更新客户端
95 | func (d *DsMapper) ClientSaveUp(item *model.NatokClient) error {
96 | var err error = nil
97 | if item.ClientId <= 0 {
98 | _, err = d.getSession().Insert(item)
99 | } else {
100 | _, err = d.getSession().Cols("client_name", "access_key", "enabled", "state", "modified", "deleted").
101 | Where("client_id=?", item.ClientId).Update(item)
102 | }
103 | if err != nil {
104 | logrus.Errorf("%v", err.Error())
105 | }
106 | return err
107 | }
108 |
109 | // ClientQueryKeys 获取全部客户端密钥
110 | func (d *DsMapper) ClientQueryKeys() (ret []model.NatokClient) {
111 | if err := d.getSession().Cols("client_name", "access_key", "enabled").Where("deleted=0").Find(&ret); err != nil {
112 | logrus.Errorf("%v", err.Error())
113 | }
114 | return ret
115 | }
116 |
117 | // ClientExist 客户端,是否存在于其他
118 | func ClientExist(clientId int64, value string) bool {
119 | exist, err := Engine.Where("deleted=0 and client_id !=? and (client_name=? or access_key=?)", clientId, value, value).Exist(new(model.NatokClient))
120 | if err != nil {
121 | logrus.Errorf("%v", err.Error())
122 | }
123 | return exist
124 | }
125 |
126 | // ClientGroupByState 客户端在线状态
127 | func (d *DsMapper) ClientGroupByState() map[string]interface{} {
128 | ret := make(map[string]interface{}, 0)
129 | sql := "select state,count(state) as count from " + tableNameNatokClient() + " where deleted=0 group by state"
130 | if result, err := d.getSession().Query(sql); err != nil {
131 | logrus.Errorf("%v", err.Error())
132 | } else {
133 | for _, m := range result {
134 | count, _ := strconv.Atoi(string(m["count"]))
135 | switch string(m["state"]) {
136 | case "0":
137 | ret["离线"] = count
138 | case "1":
139 | ret["在线"] = count
140 | }
141 | }
142 | }
143 | return ret
144 | }
145 |
146 | // ClientGroupByEnabled 客户端启用
147 | func (d *DsMapper) ClientGroupByEnabled() map[string]interface{} {
148 | ret := make(map[string]interface{}, 0)
149 | sql := "select enabled,count(state) as count from " + tableNameNatokClient() + " where deleted=0 group by enabled"
150 | if result, err := d.getSession().Query(sql); err != nil {
151 | logrus.Errorf("%v", err.Error())
152 | } else {
153 | for _, m := range result {
154 | count, _ := strconv.Atoi(string(m["count"]))
155 | switch string(m["enabled"]) {
156 | case "0":
157 | ret["停用"] = count
158 | case "1":
159 | ret["启用"] = count
160 | }
161 | }
162 | }
163 | return ret
164 | }
165 |
--------------------------------------------------------------------------------
/dsmapper/dsmapper.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "github.com/go-xorm/xorm"
5 | "github.com/sirupsen/logrus"
6 | "sync/atomic"
7 | )
8 |
9 | // DsMapper struct 数据库映射操作
10 | type DsMapper struct {
11 | _session *xorm.Session //会话
12 | counter int64 //计数
13 | }
14 |
15 | // getSession 获取连接会话
16 | func (d *DsMapper) getSession() *xorm.Session {
17 | if nil == d._session {
18 | d._session = Engine.NewSession()
19 | }
20 | return d._session
21 | }
22 |
23 | // Transaction 事务保证一致性
24 | func (d *DsMapper) Transaction(exec func() error) error {
25 | if err := d.transaction(); err != nil {
26 | return err
27 | }
28 | err := exec()
29 | if nil == err {
30 | d.commit()
31 | } else {
32 | d.rollback()
33 | }
34 | return err
35 | }
36 |
37 | // transaction 开启事务
38 | func (d *DsMapper) transaction() error {
39 | if atomic.AddInt64(&d.counter, 1) == 1 {
40 | d._session = Engine.NewSession()
41 | return d._session.Begin()
42 | }
43 | return nil
44 | }
45 |
46 | // commit 提交事务
47 | func (d *DsMapper) commit() {
48 | if atomic.AddInt64(&d.counter, -1) == 0 {
49 | defer func() {
50 | d._session.Close()
51 | d._session = nil
52 | }()
53 | if err := d._session.Commit(); err != nil {
54 | logrus.Error("commit error", err)
55 | }
56 | }
57 | }
58 |
59 | // rollback 回滚事务
60 | func (d *DsMapper) rollback() {
61 | atomic.StoreInt64(&d.counter, 0)
62 | defer func() {
63 | d._session.Close()
64 | d._session = nil
65 | }()
66 | if err := d._session.Rollback(); err != nil {
67 | logrus.Error("rollback error", err)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/dsmapper/engine.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | _ "github.com/go-sql-driver/mysql"
7 | "github.com/go-xorm/xorm"
8 | _ "github.com/mattn/go-sqlite3"
9 | "github.com/sirupsen/logrus"
10 | "natok-server/core"
11 | "natok-server/model"
12 | "natok-server/support"
13 | "natok-server/util"
14 | "time"
15 | xormc "xorm.io/core"
16 | )
17 |
18 | // Engine 定义orm引擎
19 | var Engine *xorm.Engine
20 |
21 | func IsNotEmpty(str string, f func() string) string {
22 | if str != "" {
23 | return f()
24 | }
25 | return ""
26 | }
27 |
28 | // InitDatabase 数据库初始化
29 | func InitDatabase() {
30 | logrus.Info("Init database start.")
31 | var (
32 | engine *xorm.Engine
33 | err error
34 | dbProp = support.AppConf.Natok.Db
35 | dbName = "natok" + IsNotEmpty(dbProp.DbSuffix, func() string {
36 | return "_" + dbProp.DbSuffix
37 | })
38 | )
39 | if dbProp.Type == core.Mysql {
40 | dbUrl := initMysqlDb(dbProp, dbName)
41 | engine, err = xorm.NewEngine("mysql", dbUrl)
42 | } else if dbProp.Type == core.Sqlite {
43 | engine, err = xorm.NewEngine("sqlite3", fmt.Sprintf("%s./web/%s.db", support.GetCurrentAbPath(), dbName))
44 | } else {
45 | logrus.Fatalf("conf.yaml in key [natok.datasource.type] only support [%s|%s]!", core.Sqlite, core.Mysql)
46 | }
47 | if err != nil {
48 | logrus.Fatal("Database connection failed: ", err)
49 | }
50 | mapper := xormc.NewPrefixMapper(xormc.SnakeMapper{}, IsNotEmpty(dbProp.TablePrefix, func() string {
51 | return dbProp.TablePrefix + "_"
52 | }))
53 | engine.SetMapper(mapper)
54 |
55 | if err := engine.Sync(new(model.NatokUser), new(model.NatokClient), new(model.NatokPort), new(model.NatokTag)); err != nil {
56 | logrus.Fatal("Database table synchronization failed: ", err)
57 | }
58 | if count, err := engine.Count(new(model.NatokUser)); err != nil {
59 | logrus.Fatal("Query user record failed: ", err)
60 | } else if count <= 0 {
61 | password, err := util.GeneratePassword(16)
62 | if err != nil {
63 | panic(err)
64 | }
65 | logrus.Info("#####################################")
66 | logrus.Infof("Generated password: %s", password)
67 | logrus.Info("#####################################")
68 | if _, err := engine.Insert(&model.NatokUser{Username: "admin", Password: util.Md5(password)}); err != nil {
69 | logrus.Fatal("Failed to initialize system account.", err)
70 | }
71 | }
72 |
73 | engine.ShowSQL(true)
74 | engine.ShowExecTime(true)
75 | engine.SetMaxIdleConns(2)
76 | engine.SetMaxOpenConns(100)
77 | engine.SetConnMaxLifetime(time.Minute)
78 | Engine = engine
79 |
80 | logrus.Info("Init database done.")
81 | }
82 |
83 | // initMysqlDb 初始化数据库
84 | func initMysqlDb(prop support.DataSource, name string) string {
85 | dbUrl := fmt.Sprintf("%s:%s@tcp(%s:%d)/", prop.Username, prop.Password, prop.Host, prop.Port)
86 | dbParam := name + "?charset=utf8mb4&parseTime=true&loc=Local"
87 |
88 | db, err := sql.Open("mysql", dbUrl)
89 | if err != nil {
90 | logrus.Fatal("init mysql err", err)
91 | }
92 | defer db.Close()
93 |
94 | _, err = db.Exec(fmt.Sprintf("CREATE database if NOT EXISTS %s default character set utf8mb4 collate utf8mb4_0900_ai_ci;", name))
95 | if err != nil {
96 | panic(err)
97 | }
98 |
99 | _, err = db.Exec("USE " + name)
100 | if err != nil {
101 | logrus.Fatal("init mysql err", err)
102 | }
103 | defer db.Close()
104 |
105 | logrus.Infof("Loading database %s done.", name)
106 | return dbUrl + dbParam
107 | }
108 |
109 | // StateRest 状态重置
110 | func (d *DsMapper) StateRest() {
111 | // 重置客户端状态
112 | d.ClientStateReset()
113 | // 停用过期端口
114 | d.DisableExpiredPort()
115 | // 加载客户端
116 | if clients := d.ClientFindAll(); clients != nil {
117 | for _, cli := range clients {
118 | core.ClientManage.Store(cli.AccessKey, &core.ClientBlocking{
119 | AccessKey: cli.AccessKey,
120 | Enabled: cli.Enabled == 1,
121 | })
122 | }
123 | }
124 | // 加载端口
125 | if ports := d.PortFind(true); ports != nil {
126 | var portIds = make([]int64, 0)
127 | for _, port := range ports {
128 | port.WhitelistNilEmpty()
129 | if cm, ifCM := core.ClientManage.Load(port.AccessKey); cm != nil && ifCM {
130 | client := cm.(*core.ClientBlocking)
131 | if client.Enabled {
132 | client.PortListener.Store(port.PortSign, &core.PortMapping{
133 | AccessKey: port.AccessKey,
134 | PortSign: port.PortSign,
135 | PortNum: port.PortNum,
136 | Intranet: port.Intranet,
137 | Protocol: port.Protocol,
138 | Whitelist: port.Whitelist,
139 | })
140 | portIds = append(portIds, port.PortId)
141 | }
142 | }
143 | }
144 | d.PortUpApply(portIds...)
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/dsmapper/port_rep.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "fmt"
5 | "github.com/jmoiron/sqlx"
6 | "github.com/sirupsen/logrus"
7 | "natok-server/model"
8 | "natok-server/support"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // 表名-NatokPort
15 | func tableNameNatokPort() string {
16 | return Engine.TableName(new(model.NatokPort))
17 | }
18 |
19 | // DisableExpiredPort 停用已过期端口
20 | func (d *DsMapper) DisableExpiredPort() {
21 | sql, args, err := sqlx.In("update " + tableNameNatokPort() + " set enabled=0,state=1,apply=1 where expire_at <= CURRENT_TIMESTAMP")
22 | if err != nil {
23 | logrus.Errorf("%v", err.Error())
24 | return
25 | }
26 | var sar []interface{}
27 | sar = append(append(sar, sql), args...)
28 | if _, err := d.getSession().Exec(sar...); err != nil {
29 | logrus.Errorf("%v", err.Error())
30 | return
31 | }
32 | }
33 |
34 | // PortFind 查询端口
35 | func (d *DsMapper) PortFind(isAll bool) (ports []model.NatokPort) {
36 | sql := "deleted=0 and expire_at > CURRENT_TIMESTAMP"
37 | if isAll {
38 | sql += " and enabled=1"
39 | } else {
40 | sql += " and apply=0"
41 | }
42 | if err := d.getSession().Where(sql).Find(&ports); err != nil {
43 | logrus.Errorf("%v", err.Error())
44 | return nil
45 | }
46 | return ports
47 | }
48 |
49 | // PortGetExpired 获取已过期的端口
50 | func (d *DsMapper) PortGetExpired() (ports []model.NatokPort) {
51 | if err := d.getSession().Where("deleted=0 and enabled=1 and expire_at <= CURRENT_TIMESTAMP").Find(&ports); err != nil {
52 | logrus.Errorf("%v", err.Error())
53 | return nil
54 | }
55 | return ports
56 | }
57 |
58 | // PortGet 获取端口
59 | func (d *DsMapper) PortGet(key string) (ret []model.NatokPort) {
60 | if err := d.getSession().Where("deleted=0 and enabled=1 and access_key=?", key).Find(&ret); err != nil {
61 | logrus.Errorf("%v", err.Error())
62 | return nil
63 | }
64 | return ret
65 | }
66 |
67 | // PortUpApply 更新端口数据为已应用
68 | func (d *DsMapper) PortUpApply(ids ...int64) {
69 | if len(ids) == 0 {
70 | return
71 | }
72 | sql, args, err := sqlx.In("update "+tableNameNatokPort()+" set apply=1 where port_id in (?)", ids)
73 | if err != nil {
74 | logrus.Errorf("%v", err.Error())
75 | return
76 | }
77 | var sar []interface{}
78 | sar = append(append(sar, sql), args...)
79 | if _, err := d.getSession().Exec(sar...); err != nil {
80 | logrus.Errorf("%v", err.Error())
81 | return
82 | }
83 | }
84 |
85 | ////////////////////////////////////////////////////////////////////
86 |
87 | // PortQuery 查询分页数据+总条数
88 | func (d *DsMapper) PortQuery(wd string, page, limit int) ([]model.NatokPort, int64) {
89 | ret := make([]model.NatokPort, 0)
90 | session := d.getSession()
91 | if wd != "" {
92 | session.Where("deleted=0 and access_key=?", wd)
93 | } else {
94 | session.Where("deleted=0")
95 | }
96 | session.Desc("enabled")
97 | session.Asc("port_id")
98 | //查询分页数据
99 | if err := session.Limit(limit, (page-1)*limit).Find(&ret); err != nil {
100 | logrus.Errorf("%v", err.Error())
101 | return nil, 0
102 | }
103 | return ret, d.PortCountTotal(wd)
104 | }
105 |
106 | // PortQueryByTag 根据标签查询端口
107 | func (d *DsMapper) PortQueryByTag(tagIds []int64) []model.NatokPort {
108 | ret := make([]model.NatokPort, 0)
109 | session := d.getSession()
110 | // mysql数据库
111 | mySql := func() string {
112 | sql := ""
113 | for i, v := range tagIds {
114 | if i > 0 {
115 | sql += " or "
116 | }
117 | sql += fmt.Sprintf("JSON_CONTAINS(`tag`, '%d')", v)
118 | }
119 | return fmt.Sprintf(" and (%s)", sql)
120 | }
121 | // sqlite数据库
122 | sqlLite := func() string {
123 | ids := strings.ReplaceAll(fmt.Sprintf("%v", tagIds), " ", ",")
124 | sub := fmt.Sprintf("(select 1 from json_each(`tag`) where json_each.value in (%s))", ids[1:len(ids)-1])
125 | return fmt.Sprintf(" and exists %s", sub)
126 | }
127 | sub := sqlLite()
128 | if support.AppConf.Natok.Db.Type == "mysql" {
129 | sub = mySql()
130 | }
131 | if err := session.Where("deleted=0 " + sub).Find(&ret); err != nil {
132 | logrus.Errorf("%v", err.Error())
133 | }
134 | return ret
135 | }
136 |
137 | // PortGetById 获取端口
138 | func (d *DsMapper) PortGetById(portId int64) *model.NatokPort {
139 | item := new(model.NatokPort)
140 | if ok, err := d.getSession().Where("deleted=0 and port_id=?", portId).Get(item); !ok {
141 | if err != nil {
142 | logrus.Errorf("%v", err.Error())
143 | }
144 | return nil
145 | }
146 | return item
147 | }
148 |
149 | // PortSaveUp 插入或更新端口映射
150 | func (d *DsMapper) PortSaveUp(item *model.NatokPort) error {
151 | var err error = nil
152 | if item.PortId <= 0 {
153 | _, err = d.getSession().Insert(item)
154 | } else {
155 | _, err = d.getSession().
156 | Cols("port_scope", "port_num", "intranet", "protocol", "expire_at", "whitelist", "tag", "remark", "enabled", "state", "apply", "modified", "deleted").
157 | Where("port_id=?", item.PortId).Update(item)
158 | }
159 | if err != nil {
160 | logrus.Errorf("%v", err.Error())
161 | }
162 | return err
163 | }
164 |
165 | // PortUpdateAccessKey 更新AccessKey
166 | func (d *DsMapper) PortUpdateAccessKey(oldAccessKey, newAccessKey string) error {
167 | item := model.NatokPort{
168 | AccessKey: newAccessKey, Apply: 0, Modified: time.Now(),
169 | }
170 | _, err := d.getSession().Where("access_key=?", oldAccessKey).Update(&item)
171 | if err != nil {
172 | logrus.Errorf("%v", err.Error())
173 | }
174 | return err
175 | }
176 |
177 | // PortUpdateStateByAccessKey 根据AccessKey更新Sate
178 | func (d *DsMapper) PortUpdateStateByAccessKey(accessKey string, state int8) error {
179 | item := model.NatokPort{State: state, Modified: time.Now()}
180 | _, err := d.getSession().Cols("state", "modified").Where("access_key=?", accessKey).Update(&item)
181 | if err != nil {
182 | logrus.Errorf("%v", err.Error())
183 | }
184 | return err
185 | }
186 |
187 | // PortDeleteByAccessKey 删除端口映射
188 | func (d *DsMapper) PortDeleteByAccessKey(accessKey string) error {
189 | item := model.NatokPort{Deleted: 1, Enabled: 0, State: 0, Modified: time.Now()}
190 | _, err := d.getSession().Cols("state", "modified").Where("access_key=?", accessKey).Update(&item)
191 | if err != nil {
192 | logrus.Errorf("%v", err.Error())
193 | }
194 | return err
195 | }
196 |
197 | // PortExist 端口映射,是否存在于其他
198 | func (d *DsMapper) PortExist(portId int64, portNum int, protocol string) bool {
199 | exist, err := d.getSession().Where("deleted=0 and port_id!=? and port_num=? and protocol=?", portId, portNum, protocol).Exist(new(model.NatokPort))
200 | if err != nil {
201 | logrus.Errorf("%v", err.Error())
202 | }
203 | return exist
204 | }
205 |
206 | // PortGroupByProtocol 端口协议统计
207 | func (d *DsMapper) PortGroupByProtocol() map[string]interface{} {
208 | ret := make(map[string]interface{}, 0)
209 | sql := "select protocol,count(protocol) as count from " + tableNameNatokPort() + " where deleted=0 group by protocol"
210 | if result, err := d.getSession().Query(sql); err != nil {
211 | logrus.Errorf("%v", err.Error())
212 | } else {
213 | for _, m := range result {
214 | protocol := strings.ToUpper(string(m["protocol"]))
215 | count, _ := strconv.Atoi(string(m["count"]))
216 | ret[protocol] = count
217 | }
218 | }
219 | return ret
220 | }
221 |
222 | // PortExpiredCount 端口过期统计
223 | // expired true = expired count, false = not expired count
224 | func (d *DsMapper) PortExpiredCount(expired bool) int {
225 | sql := "select count(1) as count from " + tableNameNatokPort() + " where deleted=0 and expire_at %s CURRENT_TIMESTAMP"
226 | if expired {
227 | sql = fmt.Sprintf(sql, "<")
228 | } else {
229 | sql = fmt.Sprintf(sql, ">=")
230 | }
231 | if result, err := d.getSession().Query(sql); err != nil {
232 | logrus.Errorf("%v", err.Error())
233 | } else {
234 | count, _ := strconv.Atoi(string(result[0]["count"]))
235 | return count
236 | }
237 | return 0
238 | }
239 |
240 | // PortCountTotal 端口总量统计
241 | func (d *DsMapper) PortCountTotal(accessKey string) int64 {
242 | where := d.getSession().Where("deleted=0")
243 | if accessKey != "" {
244 | where.And("access_key=?", accessKey)
245 | }
246 | if total, err := where.Count(new(model.NatokPort)); err != nil {
247 | logrus.Errorf("%v", err.Error())
248 | } else {
249 | return total
250 | }
251 | return 0
252 | }
253 |
--------------------------------------------------------------------------------
/dsmapper/tag_rep.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "fmt"
5 | "github.com/sirupsen/logrus"
6 | "natok-server/model"
7 | "strings"
8 | )
9 |
10 | // 表名-NatokTag
11 | func tableNameNatokTag() string {
12 | return Engine.TableName(new(model.NatokTag))
13 | }
14 |
15 | func (d *DsMapper) TagCountTotal(wd string) int64 {
16 | session := d.getSession()
17 | if wd != "" {
18 | session.Where("deleted=0 and (`tag_name` like CONCAT('%',?,'%') or `remark` like CONCAT('%',?,'%'))", wd, wd)
19 | } else {
20 | session.Where("deleted=0")
21 | }
22 | if total, err := session.Count(new(model.NatokTag)); err != nil {
23 | logrus.Errorf("%v", err.Error())
24 | } else {
25 | return total
26 | }
27 | return 0
28 | }
29 |
30 | // TagQuery 查询分页数据+总条数
31 | func (d *DsMapper) TagQuery(wd string, page, limit int) ([]model.NatokTag, int64) {
32 | ret := make([]model.NatokTag, 0)
33 | session := d.getSession()
34 | if wd != "" {
35 | session.Where("deleted=0 and (`tag_name` like CONCAT('%',?,'%') or `remark` like CONCAT('%',?,'%'))", wd, wd)
36 | } else {
37 | session.Where("deleted=0")
38 | }
39 | session.Desc("enabled")
40 | session.Asc("tag_id")
41 | //查询分页数据
42 | if err := session.Limit(limit, (page-1)*limit).Find(&ret); err != nil {
43 | logrus.Errorf("%v", err.Error())
44 | return nil, 0
45 | }
46 | return ret, d.TagCountTotal(wd)
47 | }
48 |
49 | // TagGetByName 获取标签
50 | func (d *DsMapper) TagGetByName(tagName string) *model.NatokTag {
51 | item := new(model.NatokTag)
52 | if ok, err := d.getSession().Where("deleted=0 and tag_name=?", tagName).Get(item); !ok {
53 | if err != nil {
54 | logrus.Errorf("%v", err.Error())
55 | }
56 | return nil
57 | }
58 | return item
59 | }
60 |
61 | // TagGetById 获取标签
62 | func (d *DsMapper) TagGetById(tagId int64) *model.NatokTag {
63 | item := new(model.NatokTag)
64 | if ok, err := d.getSession().Where("deleted=0 and tag_id=?", tagId).Get(item); !ok {
65 | if err != nil {
66 | logrus.Errorf("%v", err.Error())
67 | }
68 | return nil
69 | }
70 | return item
71 | }
72 |
73 | // TagFindByIds 获取标签集合
74 | func (d *DsMapper) TagFindByIds(tagIds []int64) []model.NatokTag {
75 | item := make([]model.NatokTag, 0)
76 | ids := strings.ReplaceAll(fmt.Sprintf("%v", tagIds), " ", ",")
77 | sub := fmt.Sprintf("tag_id in(%s)", ids[1:len(ids)-1])
78 | if err := d.getSession().Where("deleted=0 and " + sub).Find(&item); err != nil {
79 | logrus.Errorf("%v", err.Error())
80 | return nil
81 | }
82 | return item
83 | }
84 |
85 | // TagSaveUp 插入或更新标签映射
86 | func (d *DsMapper) TagSaveUp(item *model.NatokTag) error {
87 | var err error = nil
88 | if item.TagId <= 0 {
89 | _, err = d.getSession().Insert(item)
90 | } else {
91 | _, err = d.getSession().
92 | Cols("tag_name", "remark", "whitelist", "enabled", "created", "modified", "deleted").
93 | Where("tag_id=?", item.TagId).Update(item)
94 | }
95 | if err != nil {
96 | logrus.Errorf("%v", err.Error())
97 | }
98 | return err
99 | }
100 |
--------------------------------------------------------------------------------
/dsmapper/user_rep.go:
--------------------------------------------------------------------------------
1 | package dsmapper
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "natok-server/model"
6 | )
7 |
8 | // GetUser 获取用户信息
9 | func GetUser(user *model.NatokUser) *model.NatokUser {
10 | if ok, err := Engine.Get(user); !ok {
11 | if err != nil {
12 | logrus.Errorf("%v", err.Error())
13 | }
14 | return nil
15 | }
16 | return user
17 | }
18 |
19 | // ChangePassword 修改密码
20 | func ChangePassword(userId int64, oldPassword, newPassword string) bool {
21 | user := GetUser(&model.NatokUser{Id: userId, Password: oldPassword})
22 | if user != nil {
23 | user.Password = newPassword
24 | if _, err := Engine.Id(userId).Cols("password").Update(user); err == nil {
25 | return true
26 | } else {
27 | logrus.Errorf("%v", err.Error())
28 | }
29 | }
30 | return false
31 | }
32 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module natok-server
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/go-sql-driver/mysql v1.5.0
7 | github.com/go-xorm/xorm v0.7.9
8 | github.com/google/uuid v1.1.2
9 | github.com/gorilla/securecookie v1.1.1
10 | github.com/iris-contrib/middleware/cors v0.0.0-20191219204441-78279b78a367
11 | github.com/jmoiron/sqlx v1.2.0
12 | github.com/kardianos/service v1.2.2
13 | github.com/kataras/iris/v12 v12.1.8
14 | github.com/mattn/go-sqlite3 v1.14.22
15 | github.com/mojocn/base64Captcha v1.3.5
16 | github.com/sirupsen/logrus v1.9.3
17 | gopkg.in/yaml.v2 v2.3.0
18 | xorm.io/core v0.7.2-0.20190928055935-90aeac8d08eb
19 | )
20 |
21 | require (
22 | github.com/BurntSushi/toml v0.3.1 // indirect
23 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
24 | github.com/CloudyKit/jet/v3 v3.0.0 // indirect
25 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 // indirect
26 | github.com/ajg/form v1.5.1 // indirect
27 | github.com/aymerick/douceur v0.2.0 // indirect
28 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible // indirect
29 | github.com/chris-ramon/douceur v0.2.0 // indirect
30 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect
31 | github.com/fatih/structs v1.1.0 // indirect
32 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect
33 | github.com/gobwas/pool v0.2.0 // indirect
34 | github.com/gobwas/ws v1.0.2 // indirect
35 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
36 | github.com/google/go-querystring v1.0.0 // indirect
37 | github.com/gorilla/css v1.0.0 // indirect
38 | github.com/gorilla/websocket v1.4.1 // indirect
39 | github.com/imkira/go-interpol v1.1.0 // indirect
40 | github.com/iris-contrib/blackfriday v2.0.0+incompatible // indirect
41 | github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
42 | github.com/iris-contrib/jade v1.1.3 // indirect
43 | github.com/iris-contrib/pongo2 v0.0.1 // indirect
44 | github.com/iris-contrib/schema v0.0.1 // indirect
45 | github.com/json-iterator/go v1.1.9 // indirect
46 | github.com/kataras/golog v0.0.10 // indirect
47 | github.com/kataras/neffos v0.0.14 // indirect
48 | github.com/kataras/pio v0.0.2 // indirect
49 | github.com/kataras/sitemap v0.0.5 // indirect
50 | github.com/klauspost/compress v1.15.11 // indirect
51 | github.com/mediocregopher/radix/v3 v3.4.2 // indirect
52 | github.com/microcosm-cc/bluemonday v1.0.3 // indirect
53 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
54 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
55 | github.com/moul/http2curl v1.0.0 // indirect
56 | github.com/nats-io/nats-server/v2 v2.9.10 // indirect
57 | github.com/nats-io/nats.go v1.19.0 // indirect
58 | github.com/nats-io/nkeys v0.3.0 // indirect
59 | github.com/nats-io/nuid v1.0.1 // indirect
60 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
61 | github.com/ryanuber/columnize v2.1.0+incompatible // indirect
62 | github.com/schollz/closestmatch v2.1.0+incompatible // indirect
63 | github.com/sergi/go-diff v1.0.0 // indirect
64 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
65 | github.com/smartystreets/goconvey v1.7.2 // indirect
66 | github.com/stretchr/testify v1.7.1 // indirect
67 | github.com/valyala/fasthttp v1.43.0 // indirect
68 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect
69 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
70 | github.com/yudai/gojsondiff v1.0.0 // indirect
71 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
72 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
73 | golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
74 | golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
75 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
76 | golang.org/x/text v0.3.7 // indirect
77 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
78 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
79 | gopkg.in/ini.v1 v1.51.1 // indirect
80 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
81 | xorm.io/builder v0.3.6 // indirect
82 | )
83 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/kardianos/service"
5 | "github.com/sirupsen/logrus"
6 | "natok-server/bootstrap"
7 | "os"
8 | )
9 |
10 | type Program struct{}
11 |
12 | func (p *Program) Start(s service.Service) error {
13 | go p.run()
14 | return nil
15 | }
16 |
17 | func (p *Program) run() {
18 | logrus.Info("Started natok-server service")
19 | bootstrap.StartApp()
20 | }
21 |
22 | func (p *Program) Stop(s service.Service) error {
23 | logrus.Info("Stop natok-server service")
24 | return nil
25 | }
26 |
27 | // 程序入口
28 | func main() {
29 | svcConfig := &service.Config{
30 | Name: "natok-server",
31 | DisplayName: "natok-server Service",
32 | Description: "Go语言实现的内网代理服务端服务",
33 | }
34 |
35 | prg := &Program{}
36 | s, err := service.New(prg, svcConfig)
37 | if err != nil {
38 | logrus.Fatal(err)
39 | }
40 |
41 | if len(os.Args) > 1 {
42 | switch os.Args[1] {
43 | case "install":
44 | if se := s.Install(); se != nil {
45 | logrus.Error("Service installation failed. ", se)
46 | } else {
47 | logrus.Info("Service installed")
48 | }
49 | case "uninstall":
50 | if se := s.Uninstall(); se != nil {
51 | logrus.Error("Service uninstall failed. ", se)
52 | } else {
53 | logrus.Info("Service uninstalled")
54 | }
55 | case "start":
56 | if se := s.Start(); se != nil {
57 | logrus.Error("Service start failed. ", se)
58 | } else {
59 | logrus.Info("Service startup completed")
60 | }
61 | case "restart":
62 | if se := s.Restart(); se != nil {
63 | logrus.Error("Service restart failed. ", se)
64 | } else {
65 | logrus.Info("Service restart completed")
66 | }
67 | case "stop":
68 | if se := s.Stop(); se != nil {
69 | logrus.Error("Service stop failed. ", se)
70 | } else {
71 | logrus.Info("Service stop completed")
72 | }
73 | default:
74 | logrus.Warn("Unknown command: ", os.Args[1])
75 | }
76 | return
77 | }
78 | if err = s.Run(); err != nil {
79 | logrus.Fatal(err)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/model/natok_client.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // NatokClient struct 客户端对象
8 | type NatokClient struct {
9 | ClientId int64 `json:"clientId" xorm:"'client_id' autoincr pk notnull"` //客户端主键
10 | ClientName string `json:"clientName" xorm:"'client_name' default ''"` //客户端名称
11 | AccessKey string `json:"accessKey" xorm:"'access_key' default ''"` //客户端秘钥
12 | JoinTime time.Time `json:"joinTime" xorm:"'join_time' default CURRENT_TIMESTAMP"` //加入时间
13 | Enabled int8 `json:"enabled" xorm:"'enabled' default '0'"` //启用状态:1-启动,0-停用
14 | State int8 `json:"state" xorm:"'state' default '0'"` //在线状态:1-在线,0-离线
15 | Modified time.Time `json:"modified" xorm:"'modified'"` //修改时间
16 | Deleted int8 `json:"Deleted" xorm:"'deleted' default '0'"` //标记移除
17 |
18 | UsePortNum int `json:"usePortNum" xorm:"-"` //使用端口数
19 | }
20 |
--------------------------------------------------------------------------------
/model/natok_port.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "math"
5 | "time"
6 | )
7 |
8 | // NatokPort struct 端口对象
9 | type NatokPort struct {
10 | PortId int64 `json:"portId" xorm:"'port_id' autoincr pk not null"` //端口主键
11 | AccessKey string `json:"accessKey" xorm:"'access_key' default ''"` //客户端秘钥
12 | PortSign string `json:"portSign" xorm:"'port_sign' default ''"` //端口签名
13 | PortScope string `json:"portScope" xorm:"'port_scope' default 'global'"` //监听范围:global=全局,local=本地
14 | PortNum int `json:"portNum" xorm:"'port_num' default '0'"` //访问端口:0.0.0.0:80,127.0.0.1:80
15 | Intranet string `json:"intranet" xorm:"'intranet' default ''"` //转发地址:127.0.0.1:80
16 | Protocol string `json:"protocol" xorm:"'protocol' default ''"` //转发类型
17 | Whitelist []string `json:"whitelist" xorm:"'whitelist' default null"` //开放名单
18 | Tag []int64 `json:"tag" xorm:"'tag' json default null"` //端口标签
19 | Remark string `json:"remark" xorm:"'remark' default ''"` //端口备注
20 | ExpireAt time.Time `json:"expireAt" xorm:"'expire_at'"` //过期时间
21 | CreateAt time.Time `json:"createAt" xorm:"'create_at' default CURRENT_TIMESTAMP"` //创建时间
22 | State int8 `json:"state" xorm:"'state' default '0'"` //客户端状态:1-启动,0-停用
23 | Enabled int8 `json:"enabled" xorm:"'enabled' default '0'"` //端口状态:1-启动,0-停用
24 | Apply int8 `json:"apply" xorm:"'apply' default '0'"` //应用:1-已应用,0-未应用
25 | Modified time.Time `json:"modified" xorm:"'modified'"` //修改时间
26 | Deleted int8 `json:"Deleted" xorm:"'deleted' default '0'"` //标记移除:1-已删除,0-未删除
27 |
28 | ValidDay float64 `json:"validDay" xorm:"-"` // 剩余有效时间
29 | }
30 |
31 | // ValidDayCalculate 计算剩余有效时间
32 | func (n *NatokPort) ValidDayCalculate() {
33 | validDay := float64(n.ExpireAt.Unix() - time.Now().Unix())
34 | if validDay > 0 && validDay < float64(86400) {
35 | // 不足1天,显示为1天内剩余的百分比
36 | validDay = math.Floor(validDay/60/60/24*100) / 100
37 | } else {
38 | // 大于1天,按天显示
39 | validDay = math.Floor(validDay / float64(86400))
40 | }
41 | n.ValidDay = validDay
42 | }
43 |
44 | // WhitelistNilEmpty 开放名单nil转[]
45 | func (n *NatokPort) WhitelistNilEmpty() {
46 | if n.Whitelist == nil {
47 | n.Whitelist = []string{}
48 | }
49 | }
50 |
51 | // TagNilEmpty 端口标签nil转[]
52 | func (n *NatokPort) TagNilEmpty() {
53 | if n.Tag == nil {
54 | n.Tag = []int64{}
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/model/natok_tag.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | // NatokTag struct 标签对象
6 | type NatokTag struct {
7 | TagId int64 `json:"tagId" xorm:"'tag_id' autoincr pk not null"` //标签主键
8 | TagName string `json:"tagName" xorm:"'tag_name' default ''"` //标签名称
9 | Remark string `json:"remark" xorm:"'remark' default ''"` //标签备注
10 | Whitelist []string `json:"whitelist" xorm:"'whitelist' default null"` //开放名单
11 | Enabled int8 `json:"enabled" xorm:"'enabled' default '0'"` //端口状态:1-启动,0-停用
12 | Created time.Time `json:"created" xorm:"'created' default CURRENT_TIMESTAMP"` //创建时间
13 | Modified time.Time `json:"modified" xorm:"'modified' default null"` //修改时间
14 | Deleted int8 `json:"deleted" xorm:"'deleted' default '0'"` //标记移除:1-已删除,0-未删除
15 | }
16 |
17 | // WhitelistNilEmpty 开放名单nil转[]
18 | func (n *NatokTag) WhitelistNilEmpty() {
19 | if n.Whitelist == nil {
20 | n.Whitelist = []string{}
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/model/natok_user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // NatokUser struct 用户对象
4 | type NatokUser struct {
5 | Id int64 `json:"id" xorm:"'id' autoincr pk notnull"` //索引
6 | Nick string `json:"nick" xorm:"'nick'"`
7 | Username string `json:"username" xorm:"'username' notnull"` //用户名最小长度不能少于3个字符,最大超度不得超过128个字符
8 | Password string `json:"password" xorm:"'password' notnull"` //密码最小长度不能少于6个字符,最大超度不得超过128个字符
9 | Email string `json:"email" xorm:"'email'"` //邮箱
10 | Phone string `json:"phone" xorm:"'phone'"` //电话
11 | Token string `json:"token" xorm:"'token'"` //访问令牌
12 |
13 | Code string `json:"code" xorm:"-"` //验证码
14 | }
15 |
--------------------------------------------------------------------------------
/model/vo/result.go:
--------------------------------------------------------------------------------
1 | package vo
2 |
3 | import (
4 | "github.com/kataras/iris/v12/mvc"
5 | "github.com/sirupsen/logrus"
6 | )
7 |
8 | // Result struct 封装请求返回值
9 | type Result struct {
10 | Code int `json:"code"`
11 | Msg string `json:"msg"`
12 | Data interface{} `json:"data"`
13 | }
14 |
15 | var (
16 | msgOk = "ok"
17 | codeOk = 20000
18 | codeFailed = 21000
19 | )
20 |
21 | func result(code int, msg string, data interface{}) *Result {
22 | return &Result{code, msg, data}
23 | }
24 |
25 | // GenSuccessData 数据封装
26 | func GenSuccessData(data interface{}) *Result {
27 | return result(codeOk, msgOk, data)
28 | }
29 |
30 | // GenSuccessMsg 消息提示
31 | func GenSuccessMsg(msg string) *Result {
32 | return result(codeOk, msg, nil)
33 | }
34 |
35 | // GenFailedMsg 失败提示
36 | func GenFailedMsg(errMsg string) *Result {
37 | return result(codeFailed, errMsg, nil)
38 | }
39 |
40 | // TipMsg 若错误,将返回消息提示
41 | func TipMsg(err error) mvc.Result {
42 | if err != nil {
43 | logrus.Errorf("%v", err.Error())
44 | return mvc.Response{Object: GenFailedMsg(err.Error())}
45 | }
46 | return mvc.Response{Object: GenSuccessMsg(msgOk)}
47 | }
48 |
49 | // TipErrorMsg 返回错误消息提示
50 | func TipErrorMsg(errMsg string) mvc.Result {
51 | return mvc.Response{Object: GenFailedMsg(errMsg)}
52 | }
53 |
54 | // TipResult 返回数据
55 | func TipResult(data interface{}) mvc.Result {
56 | return mvc.Response{Object: GenSuccessData(data)}
57 | }
58 |
--------------------------------------------------------------------------------
/nac.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | natok-server
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/nac.syso:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/nac.syso
--------------------------------------------------------------------------------
/service/client_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | "natok-server/core"
6 | "natok-server/dsmapper"
7 | "natok-server/model"
8 | "natok-server/util"
9 | "strings"
10 | "time"
11 | )
12 |
13 | // ClientService struct 客户端 - 服务层
14 | type ClientService struct {
15 | Mapper dsmapper.DsMapper
16 | }
17 |
18 | // ClientQuery 列表分页
19 | func (s *ClientService) ClientQuery(wd string, page, limit int) map[string]interface{} {
20 | items, total := s.Mapper.ClientQuery(wd, page, limit)
21 | ret := make(map[string]interface{}, 2)
22 | for i, item := range items {
23 | if cm, ifCM := core.ClientManage.Load(item.AccessKey); cm != nil && ifCM {
24 | item.UsePortNum = core.GetLen(&cm.(*core.ClientBlocking).PortListener)
25 | items[i] = item
26 | }
27 | }
28 | ret["items"] = items
29 | ret["total"] = total
30 | return ret
31 | }
32 |
33 | // ClientGet 详情
34 | func (s *ClientService) ClientGet(clientId int64) map[string]interface{} {
35 | ret := make(map[string]interface{}, 2)
36 | ret["item"] = s.Mapper.ClientGetById(clientId)
37 | return ret
38 | }
39 |
40 | // ClientSave 保存
41 | func (s *ClientService) ClientSave(id int64, name, accessKey string) (err error) {
42 | if strings.Trim(accessKey, "") == "" {
43 | accessKey = util.Md5(util.PreTs(util.UUID()))
44 | }
45 | // 事务保证一致性
46 | return s.Mapper.Transaction(func() error {
47 | if id <= 0 {
48 | //直接插入
49 | client := model.NatokClient{AccessKey: accessKey, ClientName: name}
50 | client.JoinTime = time.Now()
51 | client.Enabled = 1
52 | err = s.Mapper.ClientSaveUp(&client)
53 | if nil == err {
54 | // 启用客户端
55 | err = s.doClientSwitch(accessKey, 1)
56 | }
57 | } else {
58 | //进行更新
59 | client := s.Mapper.ClientGetById(id)
60 | if nil == client {
61 | return errors.New("not found")
62 | }
63 | if client.Enabled == 1 {
64 | return errors.New("cannot be modified")
65 | }
66 | if nil == err && client.AccessKey != accessKey {
67 | err = s.Mapper.PortUpdateAccessKey(client.AccessKey, accessKey)
68 | }
69 | if nil == err {
70 | client.ClientName = name
71 | client.AccessKey = accessKey
72 | err = s.Mapper.ClientSaveUp(client)
73 | }
74 | }
75 | return err
76 | })
77 | }
78 |
79 | // ClientKeys 获取所有客户端秘钥
80 | func (s *ClientService) ClientKeys() map[string]interface{} {
81 | ret := make(map[string]interface{}, 2)
82 | ret["items"] = s.Mapper.ClientQueryKeys()
83 | return ret
84 | }
85 |
86 | // ClientSwitch 启用或停用
87 | func (s *ClientService) ClientSwitch(clientId int64, accessKey string, enabled int8) (err error) {
88 | client := s.Mapper.ClientGetById(clientId)
89 | if nil != client && client.AccessKey == accessKey {
90 | client.Enabled = enabled
91 | client.Modified = time.Now()
92 | // 事务保证一致性
93 | return s.Mapper.Transaction(func() error {
94 | if err = s.Mapper.PortUpdateStateByAccessKey(accessKey, enabled); err != nil {
95 | return err
96 | }
97 | if err = s.Mapper.ClientSaveUp(client); err != nil {
98 | return err
99 | }
100 | if err = s.doClientSwitch(accessKey, 2-int(enabled)); err != nil {
101 | return err
102 | }
103 | return nil
104 | })
105 | }
106 | return errors.New("not found")
107 | }
108 |
109 | // ClientDelete 删除
110 | func (s *ClientService) ClientDelete(clientId int64, accessKey string) error {
111 | client := s.Mapper.ClientGetById(clientId)
112 | if nil != client && client.AccessKey == accessKey {
113 | // 只能删除已停用的
114 | if client.Enabled == 1 {
115 | return errors.New("cannot be deleted")
116 | }
117 | client.Deleted = 1
118 | client.Enabled = 0
119 | client.Modified = time.Now()
120 | // 事务保证一致性
121 | return s.Mapper.Transaction(func() error {
122 | // 标记删除客户端映射
123 | if err := s.Mapper.ClientSaveUp(client); err != nil {
124 | return err
125 | }
126 | // 标记删除端口映射
127 | if err := s.Mapper.PortDeleteByAccessKey(accessKey); err != nil {
128 | return err
129 | }
130 | // 删除客户端连接
131 | core.ClientManage.Delete(accessKey)
132 | return nil
133 | })
134 | }
135 | return errors.New("not found")
136 | }
137 |
138 | // ClientValidate 客户端,存在验证
139 | func (s *ClientService) ClientValidate(id int64, name string, level int32) map[string]interface{} {
140 | ret := make(map[string]interface{}, 2)
141 | if id >= 0 && name != "" && level >= 1 && level <= 2 {
142 | ret["state"] = dsmapper.ClientExist(id, name)
143 | } else {
144 | ret["state"] = false
145 | }
146 | return ret
147 | }
148 |
149 | // ClientSwitch 启停客户端
150 | // opt=1 启用 opt=2 停用
151 | func (s *ClientService) doClientSwitch(accessKey string, opt int) (err error) {
152 | switch opt {
153 | case 1: //启用
154 | cm, ifCM := core.ClientManage.Load(accessKey)
155 | if nil == cm || !ifCM {
156 | core.ClientManage.Store(accessKey, &core.ClientBlocking{
157 | Enabled: true, AccessKey: accessKey,
158 | })
159 | } else {
160 | cm.(*core.ClientBlocking).Enabled = true
161 | }
162 | if ports := s.Mapper.PortGet(accessKey); ports != nil {
163 | for _, port := range ports {
164 | port.WhitelistNilEmpty()
165 | mapping := &core.PortMapping{
166 | AccessKey: port.AccessKey, PortSign: port.PortSign,
167 | PortNum: port.PortNum, Intranet: port.Intranet,
168 | Protocol: port.Protocol,
169 | Whitelist: port.Whitelist,
170 | }
171 | if err = SwitchPortMapping(mapping, 1); nil != err {
172 | break
173 | }
174 | }
175 | }
176 | case 2: // 停用
177 | if cm, ifCM := core.ClientManage.Load(accessKey); cm != nil && ifCM {
178 | client := cm.(*core.ClientBlocking)
179 | client.Enabled = false
180 | client.PortListener.Range(func(_, pm any) bool {
181 | mapping := pm.(*core.PortMapping)
182 | if err = SwitchPortMapping(mapping, 2); nil != err {
183 | return false
184 | }
185 | return true
186 | })
187 | if client.NatokHandler != nil {
188 | client.NatokHandler.Write(core.Message{Type: core.TypeDisabledAccessKey, Uri: accessKey})
189 | }
190 | }
191 | }
192 | return err
193 | }
194 |
--------------------------------------------------------------------------------
/service/dashboard_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "natok-server/core"
5 | "natok-server/dsmapper"
6 | "sync/atomic"
7 | )
8 |
9 | // ReportService struct 报表 - 服务层
10 | type ReportService struct {
11 | Mapper dsmapper.DsMapper
12 | }
13 |
14 | // StreamState 统计流入流出量
15 | func (s *ReportService) StreamState() map[string]interface{} {
16 | ret := make(map[string]interface{}, 0)
17 | ret["input"] = map[string]interface{}{"name": "流入", "data": []int{100, 120, 1, 134, 105, 160, 165}}
18 | ret["output"] = map[string]interface{}{"name": "流出", "data": []int{120, 82, 91, 154, 162, 140, 145}}
19 | ret["date"] = []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
20 | return ret
21 | }
22 |
23 | // ClientState 统计客户端状态
24 | func (s *ReportService) ClientState() map[string]interface{} {
25 | ret := make(map[string]interface{}, 0)
26 | state := s.Mapper.ClientGroupByState()
27 | if state["在线"] == nil {
28 | state["在线"] = 0
29 | }
30 | if state["离线"] == nil {
31 | state["离线"] = 0
32 | }
33 | for k, v := range state {
34 | ret[k] = v
35 | }
36 | enabled := s.Mapper.ClientGroupByEnabled()
37 | if enabled["启用"] == nil {
38 | enabled["启用"] = 0
39 | }
40 | if enabled["停用"] == nil {
41 | enabled["停用"] = 0
42 | }
43 | for k, v := range enabled {
44 | ret[k] = v
45 | }
46 | return ret
47 | }
48 |
49 | // PortState 统计端口状态
50 | func (s *ReportService) PortState() map[string]interface{} {
51 | ret := make(map[string]interface{}, 0)
52 | actCount := int64(0)
53 | core.ClientManage.Range(func(_, cm any) bool {
54 | client := cm.(*core.ClientBlocking)
55 | count := core.GetLen(&client.PortListener)
56 | atomic.AddInt64(&actCount, int64(count))
57 | return true
58 | })
59 | ret["活跃"] = actCount
60 | ret["未活动"] = s.Mapper.PortCountTotal("") - actCount
61 | ret["未过期"] = s.Mapper.PortExpiredCount(false)
62 | ret["已过期"] = s.Mapper.PortExpiredCount(true)
63 | return ret
64 | }
65 |
66 | // ProtocolState 统计端口协议状态
67 | func (s *ReportService) ProtocolState() map[string]interface{} {
68 | ret := s.Mapper.PortGroupByProtocol()
69 | if nil == ret["TCP"] {
70 | ret["TCP"] = 0
71 | }
72 | if nil == ret["SSH"] {
73 | ret["SSH"] = 0
74 | }
75 | if nil == ret["HTTP"] {
76 | ret["HTTP"] = 0
77 | }
78 | if nil == ret["HTTPS"] {
79 | ret["HTTPS"] = 0
80 | }
81 | if nil == ret["DataBase"] {
82 | ret["DataBase"] = 0
83 | }
84 | if nil == ret["Telnet"] {
85 | ret["Telnet"] = 0
86 | }
87 | if nil == ret["Desktop"] {
88 | ret["Desktop"] = 0
89 | }
90 | return ret
91 | }
92 |
93 | // RunningState 统计运行状态
94 | func (s *ReportService) RunningState() []interface{} {
95 | ret := make([]any, 0)
96 | core.ClientManage.Range(func(key, cm any) bool {
97 | client := cm.(*core.ClientBlocking)
98 | if cn, ifCN := core.ConnectManage.Load(key); cn != nil && ifCN {
99 | blocking := cn.(*core.ConnectBlocking)
100 | client.PortListener.Range(func(sign, pm any) bool {
101 | mapping := pm.(*core.PortMapping)
102 | item := make(map[string]interface{}, 0)
103 | if signs, ifSign := blocking.PortSignMap.Load(sign); signs != nil && ifSign {
104 | item["cn"] = len(signs.([]string))
105 | } else {
106 | item["cn"] = 0
107 | }
108 | item["port"] = mapping.PortNum
109 | item["protocol"] = mapping.Protocol
110 | ret = append(ret, item)
111 | return true
112 | })
113 | }
114 | return true
115 | })
116 | return ret
117 | }
118 |
--------------------------------------------------------------------------------
/service/port_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | "natok-server/core"
6 | "natok-server/dsmapper"
7 | "natok-server/model"
8 | "natok-server/util"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | // PortService struct 端口 - 服务层
14 | type PortService struct {
15 | Mapper dsmapper.DsMapper
16 | }
17 |
18 | // QueryPort PortQuery 列表分页
19 | func (s *PortService) QueryPort(wd string, page, limit int) map[string]interface{} {
20 | items, total := make([]model.NatokPort, 0), int64(0)
21 | if wd != "" {
22 | if client := s.Mapper.ClientQueryByNameOrKey(wd); client != nil {
23 | items, total = s.Mapper.PortQuery(client.AccessKey, page, limit)
24 | }
25 | } else {
26 | items, total = s.Mapper.PortQuery(wd, page, limit)
27 | }
28 | for i, item := range items {
29 | item.ValidDayCalculate()
30 | item.WhitelistNilEmpty()
31 | item.TagNilEmpty()
32 | items[i] = item
33 | }
34 | ret := make(map[string]interface{}, 2)
35 | ret["items"] = items
36 | ret["total"] = total
37 | return ret
38 | }
39 |
40 | // ValidatePort 验证端口是否可用
41 | func (s *PortService) ValidatePort(portId int64, portNum int, protocol string) map[string]interface{} {
42 | ret := make(map[string]interface{}, 2)
43 | ret["state"] = true
44 | // 端口号验证
45 | if portNum > 0 && portNum < 65535 {
46 | if able := util.PortAvailable(portNum, protocol); able {
47 | if !s.Mapper.PortExist(portId, portNum, protocol) {
48 | ret["state"] = false
49 | return ret
50 | }
51 | }
52 | }
53 | return ret
54 | }
55 |
56 | // GetPort PortGet 详情
57 | func (s *PortService) GetPort(portId int64) (map[string]interface{}, error) {
58 | ret := make(map[string]interface{}, 2)
59 | if item := s.Mapper.PortGetById(portId); item != nil {
60 | item.ValidDayCalculate()
61 | item.WhitelistNilEmpty()
62 | item.TagNilEmpty()
63 | ret["item"] = item
64 | return ret, nil
65 | }
66 | return nil, errors.New("not found")
67 | }
68 |
69 | // SavePort 保存端口映射
70 | func (s *PortService) SavePort(item *model.NatokPort) (err error) {
71 | item.WhitelistNilEmpty()
72 | item.TagNilEmpty()
73 | // 过期时间顺延一天
74 | if item.ValidDay <= 0 {
75 | item.ExpireAt = util.NowDayStart().Add(8 * 24 * time.Hour)
76 | } else {
77 | item.ExpireAt = util.NowDayStart().Add(time.Duration(item.ValidDay+1) * 24 * time.Hour)
78 | }
79 | // 事务保证一致性
80 | return s.Mapper.Transaction(func() error {
81 | if item.PortId <= 0 {
82 | //直接插入
83 | item.CreateAt = time.Now()
84 | item.PortSign = util.Md5(util.PreTs(util.UUID() + item.AccessKey + strconv.Itoa(item.PortNum)))
85 | item.State = 1
86 | item.Apply = 0
87 | item.Enabled = 1
88 | if err = s.Mapper.PortSaveUp(item); err != nil {
89 | return err
90 | }
91 | if err = s.SwitchPort(item.PortId, item.AccessKey, 1); err != nil {
92 | return err
93 | }
94 | } else {
95 | port := s.Mapper.PortGetById(item.PortId)
96 | if nil == port {
97 | return errors.New("not found")
98 | }
99 | if port.Enabled == 1 {
100 | return errors.New("cannot be modified")
101 | }
102 | port.PortScope = item.PortScope
103 | port.PortNum = item.PortNum
104 | port.Intranet = item.Intranet
105 | port.Protocol = item.Protocol
106 | port.ExpireAt = item.ExpireAt
107 | port.Remark = item.Remark
108 | port.Whitelist = item.Whitelist
109 | port.Tag = item.Tag
110 | port.Modified = time.Now()
111 | if err = s.Mapper.PortSaveUp(port); err != nil {
112 | return err
113 | }
114 | }
115 | // 更新开放名单
116 | if err = PortWhitelist(item, s.Mapper); err != nil {
117 | return err
118 | }
119 | return nil
120 | })
121 | }
122 |
123 | // SwitchPort 启用或停用
124 | func (s *PortService) SwitchPort(portId int64, accessKey string, enabled int8) (err error) {
125 | port := s.Mapper.PortGetById(portId)
126 | if nil != port && port.AccessKey == accessKey {
127 | if port.State <= 0 {
128 | return errors.New("client not enabled")
129 | }
130 | port.Enabled = enabled
131 | port.Modified = time.Now()
132 | port.WhitelistNilEmpty()
133 | port.TagNilEmpty()
134 | // 事务保证一致性
135 | return s.Mapper.Transaction(func() error {
136 | if err = s.Mapper.PortSaveUp(port); err != nil {
137 | return err
138 | }
139 | // 端口映射:绑定与解绑
140 | if err = SwitchPortMapping(&core.PortMapping{
141 | AccessKey: port.AccessKey, PortSign: port.PortSign,
142 | PortScope: port.PortScope,
143 | PortNum: port.PortNum, Intranet: port.Intranet,
144 | Protocol: port.Protocol,
145 | Whitelist: port.Whitelist,
146 | }, int(2-enabled)); err != nil {
147 | return err
148 | }
149 | // 更新开放名单
150 | if err = PortWhitelist(port, s.Mapper); err != nil {
151 | return err
152 | }
153 | return nil
154 | })
155 | }
156 | return errors.New("not found")
157 | }
158 |
159 | // DeletePort 删除
160 | func (s *PortService) DeletePort(portId int64, accessKey string) (err error) {
161 | port := s.Mapper.PortGetById(portId)
162 | if nil == port || port.AccessKey != accessKey {
163 | return errors.New("not found")
164 | }
165 | port.Deleted = 1
166 | port.Modified = time.Now()
167 | port.WhitelistNilEmpty()
168 | port.TagNilEmpty()
169 | // 事务保证一致性
170 | return s.Mapper.Transaction(func() error {
171 | if err = s.Mapper.PortSaveUp(port); err != nil {
172 | return err
173 | }
174 | // 端口映射:解绑
175 | if err = SwitchPortMapping(&core.PortMapping{
176 | AccessKey: port.AccessKey, PortSign: port.PortSign,
177 | PortScope: port.PortScope,
178 | PortNum: port.PortNum, Intranet: port.Intranet,
179 | Protocol: port.Protocol,
180 | Whitelist: port.Whitelist,
181 | }, 2); err != nil {
182 | return err
183 | }
184 | return nil
185 | })
186 | }
187 |
188 | // SwitchPortMapping 启停端口映射
189 | // opt=1 启用 opt=2 停用
190 | func SwitchPortMapping(mapping *core.PortMapping, opt int) error {
191 | var err error
192 | switch opt {
193 | case 1:
194 | err = core.BindPort(mapping)
195 | case 2:
196 | err = core.UnBindPort(mapping)
197 | }
198 | return err
199 | }
200 |
--------------------------------------------------------------------------------
/service/tag_service.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "errors"
5 | "natok-server/core"
6 | "natok-server/dsmapper"
7 | "natok-server/model"
8 | "natok-server/util"
9 | "time"
10 | )
11 |
12 | // TagService struct 标签 - 服务层
13 | type TagService struct {
14 | Mapper dsmapper.DsMapper
15 | }
16 |
17 | // QueryPageTag 列表分页
18 | func (s *TagService) QueryPageTag(wd string, page, limit int) map[string]interface{} {
19 | items, total := make([]model.NatokTag, 0), int64(0)
20 | items, total = s.Mapper.TagQuery(wd, page, limit)
21 | for i, item := range items {
22 | item.WhitelistNilEmpty()
23 | items[i] = item
24 | }
25 | ret := make(map[string]interface{}, 2)
26 | ret["items"] = items
27 | ret["total"] = total
28 | return ret
29 | }
30 |
31 | // GetTag 获取
32 | func (s *TagService) GetTag(tagId int64) (map[string]interface{}, error) {
33 | ret := make(map[string]interface{}, 2)
34 | if item := s.Mapper.TagGetById(tagId); item != nil {
35 | item.WhitelistNilEmpty()
36 | ret["item"] = item
37 | return ret, nil
38 | }
39 | return nil, errors.New("not found")
40 | }
41 |
42 | // SaveTag 保存
43 | func (s *TagService) SaveTag(item *model.NatokTag) (err error) {
44 | if item != nil {
45 | // 检查名称是否已存在
46 | if byName := s.Mapper.TagGetByName(item.TagName); byName != nil {
47 | if item.TagId <= 0 || item.TagId != byName.TagId {
48 | return errors.New("tag name already exists")
49 | }
50 | }
51 | // 添加标签映射
52 | if item.TagId <= 0 {
53 | item.Created = time.Now()
54 | item.Enabled = 1
55 | item.Deleted = 0
56 | } else {
57 | item.Modified = time.Now()
58 | }
59 | // 事务保证一致性
60 | return s.Mapper.Transaction(func() error {
61 | if err = s.Mapper.TagSaveUp(item); err != nil {
62 | return err
63 | }
64 | if err = s.refresh(item.TagId); err != nil {
65 | return err
66 | }
67 | return nil
68 | })
69 | }
70 | return err
71 | }
72 |
73 | // DeleteTag 标记删除标签映射
74 | func (s *TagService) DeleteTag(tagId int64) (err error) {
75 | tag := s.Mapper.TagGetById(tagId)
76 | if nil != tag {
77 | tag.Deleted = 1
78 | tag.Enabled = 0
79 | tag.Modified = time.Now()
80 | // 事务保证一致性
81 | return s.Mapper.Transaction(func() error {
82 | if err = s.Mapper.TagSaveUp(tag); err != nil {
83 | return err
84 | }
85 | if err = s.refresh(tagId); err != nil {
86 | return err
87 | }
88 | return nil
89 | })
90 | }
91 | return err
92 | }
93 |
94 | // SwitchTag 启用或停用
95 | func (s *TagService) SwitchTag(tagId int64, enabled int8) (err error) {
96 | tag := s.Mapper.TagGetById(tagId)
97 | if nil == tag {
98 | return errors.New("not found")
99 | }
100 | tag.Enabled = enabled
101 | tag.Modified = time.Now()
102 | // 事务保证一致性
103 | return s.Mapper.Transaction(func() error {
104 | if err = s.Mapper.TagSaveUp(tag); err != nil {
105 | return err
106 | }
107 | if err = s.refresh(tagId); err != nil {
108 | return err
109 | }
110 | return nil
111 | })
112 | }
113 |
114 | // refresh 刷新端口映射的开放名单
115 | func (s *TagService) refresh(tagId int64) (err error) {
116 | // 标签映射:更新端口映射;获取出包含该标签的端口映射,刷新端口的白名单
117 | if ports := s.Mapper.PortQueryByTag([]int64{tagId}); len(ports) > 0 {
118 | for _, port := range ports {
119 | // 更新开放名单
120 | if err = PortWhitelist(&port, s.Mapper); err != nil {
121 | return err
122 | }
123 | }
124 | }
125 | return err
126 | }
127 |
128 | // PortWhitelist 端口映射:更新开放名单
129 | func PortWhitelist(port *model.NatokPort, mapper dsmapper.DsMapper) (err error) {
130 | port.TagNilEmpty()
131 | port.WhitelistNilEmpty()
132 | tagIds := make([]int64, 0)
133 | for _, tag := range port.Tag {
134 | tagIds = append(tagIds, tag)
135 | }
136 | port.Whitelist = make([]string, 0)
137 | port.Tag = make([]int64, 0)
138 | // 标签开放名单
139 | if len(tagIds) > 0 {
140 | tags := mapper.TagFindByIds(tagIds)
141 | for _, tag := range tags {
142 | tag.WhitelistNilEmpty()
143 | port.Tag = append(port.Tag, tag.TagId)
144 | if tag.Enabled == 1 && len(tag.Whitelist) > 0 {
145 | port.Whitelist = append(port.Whitelist, tag.Whitelist...)
146 | }
147 | }
148 | }
149 | // 去重
150 | port.Whitelist = util.Distinct(port.Whitelist)
151 | // 端口映射:更新
152 | if err = mapper.PortSaveUp(port); err != nil {
153 | return err
154 | }
155 | // 客户端下的端口映射:更新
156 | if cm, ifCM := core.ClientManage.Load(port.AccessKey); ifCM {
157 | if pm, ifPM := cm.(*core.ClientBlocking).PortListener.Load(port.PortSign); ifPM {
158 | pm.(*core.PortMapping).Whitelist = port.Whitelist
159 | }
160 | }
161 | return err
162 | }
163 |
--------------------------------------------------------------------------------
/support/app_config.go:
--------------------------------------------------------------------------------
1 | package support
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "gopkg.in/yaml.v2"
6 | "io"
7 | "os"
8 | "regexp"
9 | )
10 |
11 | var AppConf *AppConfig
12 |
13 | // AppConfig 应用配置
14 | type AppConfig struct {
15 | Natok Natok `yaml:"natok"`
16 | BaseDirPath string
17 | ProxyContent string
18 | }
19 | type Natok struct {
20 | Server Server `yaml:"server"`
21 | WebPort int `yaml:"web.port"`
22 | Debug bool `yaml:"log.debug"`
23 | Db DataSource `yaml:"datasource"`
24 | }
25 |
26 | // Server NATOK服务配置
27 | type Server struct {
28 | InetHost string `yaml:"host"` //服务器地址
29 | InetPort int `yaml:"port"` //服务器端口
30 | LogFilePath string `yaml:"log-file-path"` //日志路径
31 | CertPemPath string `yaml:"cert-pem-path"` //密钥路径
32 | CertKeyPath string `yaml:"cert-key-path"` //证书路径
33 | ChanPool ChanPool `yaml:"chan-pool"` //连接池配置
34 | }
35 |
36 | // ChanPool 通道连接池配置
37 | type ChanPool struct {
38 | MaxSize int `yaml:"max-size"` //最大连接数
39 | MinSize int `yaml:"min-size"` //最小连接数
40 | IdleTimeout int64 `yaml:"idle-timeout"` //连接空闲时间(秒)
41 | }
42 |
43 | // DataSource 源数据库配置
44 | type DataSource struct {
45 | Type string `yaml:"type"` //数据类型:sqlite、mysql
46 | Host string `yaml:"host"` //主机
47 | Port int `yaml:"port"` //端口
48 | Username string `yaml:"username"` //用户名
49 | Password string `yaml:"password"` //密码
50 | DbSuffix string `yaml:"db-suffix"` //库后缀
51 | TablePrefix string `yaml:"table-prefix"` //表前缀
52 | }
53 |
54 | // SetDefaults 设置默认值
55 | func (c *ChanPool) SetDefaults() {
56 | if c.MaxSize == 0 {
57 | c.MaxSize = 200
58 | }
59 | if c.MinSize == 0 {
60 | c.MinSize = 10
61 | }
62 | if c.IdleTimeout == 0 {
63 | c.IdleTimeout = 600
64 | }
65 | }
66 |
67 | // InitConfig 初始化配置
68 | func InitConfig() {
69 | if AppConf != nil {
70 | return
71 | }
72 | baseDirPath := GetCurrentAbPath()
73 | // 读取文件内容
74 | file, err := os.ReadFile(baseDirPath + "conf.yaml")
75 | if err != nil {
76 | panic(err)
77 | }
78 | // 利用json转换为AppConfig
79 | appConfig := new(AppConfig)
80 | err = yaml.Unmarshal(file, appConfig)
81 | if err != nil {
82 | panic(err)
83 | }
84 | // 设置默认值
85 | appConfig.Natok.Server.ChanPool.SetDefaults()
86 |
87 | // 配置当前路径
88 | appConfig.BaseDirPath = baseDirPath
89 | server := &appConfig.Natok.Server
90 | compile, err := regexp.Compile("^/|^\\\\|^[a-zA-Z]:")
91 | // 密钥文件
92 | if server.CertKeyPath != "" && !compile.MatchString(server.CertKeyPath) {
93 | logrus.Infof("%s -> %s", server.CertKeyPath, baseDirPath+server.CertKeyPath)
94 | server.CertKeyPath = baseDirPath + server.CertKeyPath
95 | }
96 | // 证书文件
97 | if server.CertPemPath != "" && !compile.MatchString(server.CertPemPath) {
98 | logrus.Infof("%s -> %s", server.CertPemPath, baseDirPath+server.CertPemPath)
99 | server.CertPemPath = baseDirPath + server.CertPemPath
100 | }
101 | // 日志记录
102 | logrus.SetLevel(logrus.InfoLevel)
103 | logrus.SetFormatter(&logrus.TextFormatter{
104 | FullTimestamp: true,
105 | ForceColors: true,
106 | TimestampFormat: "2006-01-02 15:04:05.000",
107 | })
108 | // 在输出日志中添加文件名和方法信息
109 | if appConfig.Natok.Debug {
110 | logrus.SetReportCaller(true)
111 | logrus.SetLevel(logrus.DebugLevel)
112 | }
113 | // 日志记录输出文件
114 | if server.LogFilePath != "" && !compile.MatchString(server.LogFilePath) {
115 | logrus.Infof("%s -> %s", server.LogFilePath, baseDirPath+server.LogFilePath)
116 | server.LogFilePath = baseDirPath + server.LogFilePath
117 | logFile, err := os.OpenFile(server.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
118 | if err != nil {
119 | logrus.Fatal(err)
120 | } else {
121 | // 组合一下即可,os.Stdout代表标准输出流
122 | multiWriter := io.MultiWriter(logFile, os.Stdout)
123 | logrus.SetOutput(multiWriter)
124 | }
125 | }
126 |
127 | AppConf = appConfig
128 | }
129 |
--------------------------------------------------------------------------------
/support/auth_config.go:
--------------------------------------------------------------------------------
1 | package support
2 |
3 | import (
4 | "github.com/gorilla/securecookie"
5 | "github.com/iris-contrib/middleware/cors"
6 | "github.com/kataras/iris/v12"
7 | "github.com/kataras/iris/v12/sessions"
8 | "github.com/sirupsen/logrus"
9 | "strings"
10 | "time"
11 | )
12 |
13 | var (
14 | CookieName = "NATOK"
15 | SessionKey = "NATOK-AUTHENTICATION"
16 | SessionUserId = "NATOK-USER_ID"
17 | SessionsManager *sessions.Sessions
18 | )
19 |
20 | // TimeCounter 时间内计数器
21 | type TimeCounter struct {
22 | time time.Time
23 | counter int64
24 | }
25 |
26 | func InitAuth() {
27 | //附加session管理器
28 | // AES仅支持16,24或32字节的密钥大小。
29 | // 您需要准确提供该字节数,或者从您键入的内容中获取密钥。
30 | hashKey := []byte("the-big-and-secret-fash-key-here")
31 | blockKey := []byte("lot-secret-of-characters-big-too")
32 | secureCookie := securecookie.New(hashKey, blockKey)
33 | SessionsManager = sessions.New(sessions.Config{
34 | Cookie: CookieName,
35 | Encode: secureCookie.Encode,
36 | Decode: secureCookie.Decode,
37 | Expires: 24 * time.Hour,
38 | })
39 | }
40 |
41 | var (
42 | basics = []string{"/static", "/js", "/css", "/favicon.ico", "/captcha"}
43 | passUri = []string{"/user/login", "/index.html"}
44 | )
45 |
46 | // AuthorHandler 认证拦截器
47 | func AuthorHandler() func(iris.Context) {
48 | return func(ctx iris.Context) {
49 | path := ctx.Path()
50 | for _, pass := range append(basics, passUri...) {
51 | if strings.HasPrefix(path, pass) {
52 | ctx.Next()
53 | return
54 | }
55 | }
56 | if nil != SessionsManager.Start(ctx).Get(SessionKey) {
57 | ctx.Next()
58 | return
59 | }
60 | ctx.Next()
61 | logrus.Warnf("Intercept: %s", path)
62 | //ctx.Redirect(loginWeb, iris.StatusFound)
63 | }
64 | }
65 |
66 | // CorsHandler 开启可跨域
67 | func CorsHandler() func(iris.Context) {
68 | return cors.New(cors.Options{
69 | AllowCredentials: true,
70 | AllowedOrigins: []string{"*"},
71 | AllowedHeaders: []string{"*"},
72 | AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/support/captcha.go:
--------------------------------------------------------------------------------
1 | package support
2 |
3 | import (
4 | "github.com/mojocn/base64Captcha"
5 | "math/rand"
6 | )
7 |
8 | // 验证码信息常量
9 | const (
10 | CaptchaId = "NATOK-CAPTCHA"
11 | CaptchaWidth = 120
12 | CaptchaHeight = 44
13 | CaptchaLength = 5
14 | CaptchaContent = "1234567890abcdefghijklmnopqrstwvxyzABCDEFGHIJKLMNOPQRSTWVXYZ"
15 | CaptchaFont = "wqy-microhei.ttc"
16 | )
17 |
18 | // NewDriver 创建配置好的验证码驱动对象
19 | func NewDriver() *base64Captcha.DriverString {
20 | driver := new(base64Captcha.DriverString)
21 | driver.Height = CaptchaHeight
22 | driver.Width = CaptchaWidth
23 | driver.NoiseCount = 4 + rand.Intn(5)
24 | driver.ShowLineOptions = base64Captcha.OptionShowHollowLine
25 | driver.Length = CaptchaLength
26 | driver.Source = CaptchaContent
27 | driver.Fonts = []string{CaptchaFont}
28 | return driver
29 | }
30 |
--------------------------------------------------------------------------------
/support/path_util.go:
--------------------------------------------------------------------------------
1 | package support
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | // GetCurrentAbPath 最终方案-全兼容
12 | func GetCurrentAbPath() string {
13 | dir := getCurrentAbPathByExecutable()
14 | if strings.Contains(dir, getTmpDir()) {
15 | dir = getCurrentAbPathByCaller()
16 | }
17 | return dir + "/"
18 | }
19 |
20 | // 获取系统临时目录,兼容go run
21 | func getTmpDir() string {
22 | dir := os.Getenv("TEMP")
23 | if dir == "" {
24 | dir = os.Getenv("TMP")
25 | }
26 | res, _ := filepath.EvalSymlinks(dir)
27 | return res
28 | }
29 |
30 | // 获取当前执行文件绝对路径
31 | func getCurrentAbPathByExecutable() string {
32 | exePath, err := os.Executable()
33 | if err != nil {
34 | logrus.Fatal(err)
35 | }
36 | res, _ := filepath.EvalSymlinks(filepath.Dir(exePath))
37 | return res
38 | }
39 |
40 | // 获取当前执行文件绝对路径(go run)
41 | func getCurrentAbPathByCaller() string {
42 | var abPath string
43 | if _, filename, _, ok := runtime.Caller(0); ok {
44 | if lst := strings.LastIndex(filename, "/support"); lst != -1 {
45 | abPath = filename[0:lst]
46 | }
47 | }
48 | return abPath
49 | }
50 |
--------------------------------------------------------------------------------
/timer/worker.go:
--------------------------------------------------------------------------------
1 | package timer
2 |
3 | import (
4 | "github.com/sirupsen/logrus"
5 | "natok-server/core"
6 | "natok-server/dsmapper"
7 | "natok-server/support"
8 | "natok-server/util"
9 | "time"
10 | )
11 |
12 | func Worker() {
13 | Initialization()
14 | }
15 | func Initialization() {
16 | dsMapper := new(dsmapper.DsMapper)
17 | //载入数据,绑定端口
18 | dsMapper.StateRest()
19 | core.ClientManage.Range(func(key, value interface{}) bool {
20 | client := value.(*core.ClientBlocking)
21 | client.PortListener.Range(func(_, pm any) bool {
22 | mapping := pm.(*core.PortMapping)
23 | if err := core.BindPort(mapping); err != nil {
24 | logrus.Errorf("%v", err.Error())
25 | }
26 | return true
27 | })
28 | return true
29 | })
30 | //客户端状态实时更新
31 | go func() {
32 | for {
33 | select {
34 | case chanClient := <-core.ChanClientSate:
35 | if dbClient := dsMapper.ClientQueryByNameOrKey(chanClient.AccessKey); dbClient != nil {
36 | dbClient.State = chanClient.State
37 | dbClient.Modified = time.Now()
38 | _ = dsMapper.ClientSaveUp(dbClient)
39 | }
40 | logrus.Infof("Client %s is onlie", chanClient.AccessKey)
41 | }
42 | }
43 | }()
44 | // 自动停用已过期端口
45 | go func() {
46 | for {
47 | select {
48 | case <-time.After(time.Minute * 15):
49 | if ports := dsMapper.PortGetExpired(); len(ports) > 0 {
50 | for _, port := range ports {
51 | if cm, ifCM := core.ClientManage.Load(port.AccessKey); cm != nil && ifCM {
52 | client := cm.(*core.ClientBlocking)
53 | if pm, ifPM := client.PortListener.Load(port.PortSign); pm != nil && ifPM {
54 | mapping := pm.(*core.PortMapping)
55 | _ = core.UnBindPort(mapping)
56 | }
57 | }
58 | }
59 | dsMapper.DisableExpiredPort()
60 | }
61 | }
62 | }
63 | }()
64 | // 定时清理空闲连接
65 | go func() {
66 | for {
67 | select {
68 | case <-time.After(time.Second * 5):
69 | now := time.Now()
70 | idleTimeout := time.Duration(support.AppConf.Natok.Server.ChanPool.IdleTimeout) * time.Second
71 | core.ConnectManage.Range(func(accessKey, cn any) bool {
72 | blocking := cn.(*core.ConnectBlocking)
73 | blocking.PortSignMap.Range(func(sign, ids any) bool {
74 | portSign, accessIds := sign.(string), ids.([]string)
75 | for _, accessId := range accessIds {
76 | if connect, ifConnect := blocking.AccessIdMap.Load(accessId); ifConnect && connect != nil {
77 | extra := connect.(*core.ExtraConnectHandler)
78 | // 非活跃状态
79 | if extra.ConnHandler.Active == false {
80 | // UDP无连接的协议,不需要频繁清理
81 | if extra.Protocol == core.Udp && now.Sub(extra.ConnHandler.WriteTime) > idleTimeout {
82 | blocking.AccessIdMap.Delete(accessId)
83 | blocking.PortSignMap.Store(portSign, util.RemoveIf(accessIds, accessId))
84 | extra.ConnHandler.MsgHandler.Close(extra.ConnHandler)
85 | }
86 | if extra.Protocol == core.Tcp {
87 | blocking.AccessIdMap.Delete(accessId)
88 | blocking.PortSignMap.Store(portSign, util.RemoveIf(accessIds, accessId))
89 | }
90 | logrus.Debugf("Clean idle connect %s", accessId)
91 | }
92 | }
93 | }
94 | return true
95 | })
96 | return true
97 | })
98 | }
99 | }
100 | }()
101 | }
102 |
--------------------------------------------------------------------------------
/util/gen_bill_code.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "time"
7 | )
8 |
9 | // Encoder 编码生成器
10 | type Encoder struct {
11 | prefix string
12 | serialNumber int
13 | systemSerial string
14 | mu sync.Mutex
15 | }
16 |
17 | // Generate 生成编码
18 | func (e *Encoder) Generate() string {
19 | e.mu.Lock()
20 | defer e.mu.Unlock()
21 | if e.serialNumber >= 9999 {
22 | time.Sleep(10 * time.Millisecond)
23 | }
24 | currentSerial := time.Now().Format("20060102150405")
25 | if currentSerial != e.systemSerial {
26 | e.systemSerial = currentSerial
27 | e.serialNumber = 1
28 | } else {
29 | e.serialNumber++
30 | }
31 | return fmt.Sprintf("%s%s%04d", e.prefix, e.systemSerial, e.serialNumber)
32 | }
33 |
34 | // NewEncoder 创建一个新的编码生成器
35 | func NewEncoder(prefix string) *Encoder {
36 | return &Encoder{
37 | prefix: prefix,
38 | serialNumber: 0,
39 | systemSerial: "",
40 | }
41 | }
42 |
43 | var (
44 | encoderMap sync.Map
45 | mu sync.Mutex
46 | )
47 |
48 | // GenerateCode 生成编码
49 | func GenerateCode(prefix string) string {
50 | if encoder, ok := encoderMap.Load(prefix); encoder != nil && ok {
51 | return encoder.(*Encoder).Generate()
52 | }
53 | mu.Lock()
54 | defer mu.Unlock()
55 | encoder := NewEncoder(prefix)
56 | encoderMap.Store(prefix, encoder)
57 | return encoder.Generate()
58 | }
59 |
--------------------------------------------------------------------------------
/util/snowflake.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strconv"
7 | "sync"
8 | "time"
9 | )
10 |
11 | var (
12 | // 开始计时时间
13 | epoch = func() int64 { return time.Now().UnixNano() / int64(time.Millisecond) }
14 | // 时间戳最大值
15 | // timestampMax int64 = -1 ^ (-1 << 41)
16 | snowflakeWorker *IdWorker
17 | workerBits = uint(5)
18 | dataCenterBits = uint(5)
19 | sequenceBits = uint(12)
20 | maxWorkerId int64 = -1 ^ (-1 << workerBits)
21 | maxDataCenterId int64 = -1 ^ (-1 << dataCenterBits)
22 | workerIdShift = sequenceBits
23 | dataCenterIdShift = sequenceBits + workerBits
24 | timestampLeftShift = sequenceBits + workerBits + dataCenterBits
25 | sequenceMask int64 = -1 ^ (-1 << sequenceBits)
26 | )
27 |
28 | type IdWorker struct {
29 | mutex sync.Mutex
30 | workerId int64
31 | dataCenterId int64
32 | epoch int64
33 | sequence int64
34 | lastTimestamp int64
35 | }
36 |
37 | // DefSnowflakeId 默认获取雪花ID
38 | func DefSnowflakeId() (error, int64) {
39 | return SnowflakeId(0, 0)
40 | }
41 |
42 | // DefStrSnowflakeId 默认获取雪花ID
43 | func DefStrSnowflakeId() (error, string) {
44 | return StrSnowflakeId(0, 0)
45 | }
46 |
47 | // SnowflakeId 获取雪花ID
48 | func SnowflakeId(workerId, dataCenterId int64) (error, int64) {
49 | var err error
50 | if nil == snowflakeWorker {
51 | err, snowflakeWorker = NewIdWorker(workerId, dataCenterId, epoch())
52 | }
53 | if err == nil {
54 | return snowflakeWorker.NextId()
55 | }
56 | return err, 0
57 | }
58 |
59 | // StrSnowflakeId 获取雪花ID
60 | func StrSnowflakeId(workerId, dataCenterId int64) (error, string) {
61 | if err, id := SnowflakeId(workerId, dataCenterId); err == nil {
62 | return nil, strconv.FormatInt(id, 10)
63 | } else {
64 | return err, ""
65 | }
66 | }
67 |
68 | // NewIdWorker new worker
69 | func NewIdWorker(workerId, dataCenterId, epoch int64) (error, *IdWorker) {
70 | if workerId > maxWorkerId || workerId < 0 {
71 | return errors.New(fmt.Sprintf("worker Id can't be greater than %d or less than 0", maxWorkerId)), nil
72 | }
73 | if dataCenterId > maxDataCenterId || dataCenterId < 0 {
74 | return errors.New(fmt.Sprintf("datacenter Id can't be greater than %d or less than 0", maxDataCenterId)), nil
75 | }
76 | id := &IdWorker{}
77 | id.workerId = workerId
78 | id.dataCenterId = dataCenterId
79 | id.sequence = 0
80 | id.lastTimestamp = -1
81 | id.epoch = epoch
82 | id.mutex = sync.Mutex{}
83 | return nil, id
84 | }
85 |
86 | // NextId 生成一个ID
87 | func (i *IdWorker) NextId() (error, int64) {
88 | // 加锁, 防止数据被更改
89 | i.mutex.Lock()
90 | defer i.mutex.Unlock()
91 | timestamp := i.GenTime()
92 | // 如果时间出现回拨, 直接抛弃
93 | if timestamp < i.lastTimestamp {
94 | // "clock is moving backwards. Rejecting requests until %d.", lastTimestamp
95 | err := errors.New(fmt.Sprintf("Clock moved backwards. Refusing to generate id for %d milliseconds", i.lastTimestamp-timestamp))
96 | return err, 0
97 | }
98 | // 同一毫秒内出现多个请求, sequence +1
99 | // 最大值是4095, 超过4095则调用tilNextMillis进行等待
100 | if i.lastTimestamp == timestamp {
101 | i.sequence = (i.sequence + 1) & sequenceMask
102 | if i.sequence == 0 {
103 | timestamp = i.tilNextMillis(i.lastTimestamp)
104 | }
105 | } else {
106 | i.sequence = 0
107 | }
108 | i.lastTimestamp = timestamp
109 | return nil, ((timestamp - i.epoch) << timestampLeftShift) |
110 | (i.dataCenterId << dataCenterIdShift) |
111 | (i.workerId << workerIdShift) |
112 | i.sequence
113 | }
114 |
115 | // tilNextMillis 等待下一毫秒
116 | func (i *IdWorker) tilNextMillis(lastTimestamp int64) int64 {
117 | timestamp := i.GenTime()
118 | for timestamp <= lastTimestamp {
119 | timestamp = i.GenTime()
120 | }
121 | return timestamp
122 | }
123 |
124 | // GenTime 获取当前时间, 单位是毫秒
125 | func (i *IdWorker) GenTime() int64 {
126 | return time.Now().UnixNano() / int64(time.Millisecond)
127 | }
128 |
129 | // GetWorkerId 获得workerID
130 | func (i *IdWorker) GetWorkerId() int64 {
131 | return i.workerId
132 | }
133 |
134 | // GetDataCenterID 获得datacenterID
135 | func (i *IdWorker) GetDataCenterID() int64 {
136 | return i.dataCenterId
137 | }
138 |
--------------------------------------------------------------------------------
/util/toolkit.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/md5"
5 | "crypto/rand"
6 | "encoding/hex"
7 | "errors"
8 | "github.com/google/uuid"
9 | "github.com/sirupsen/logrus"
10 | "net"
11 | "strconv"
12 | "strings"
13 | "time"
14 | )
15 |
16 | // PortAvailable 端口是否可用
17 | func PortAvailable(port int, protocol string) bool {
18 | if strings.EqualFold("udp", protocol) {
19 | packet, err := net.ListenPacket("udp", ":"+strconv.Itoa(port))
20 | if err != nil {
21 | return false
22 | }
23 | defer func(packet net.PacketConn) {
24 | _ = packet.Close()
25 | }(packet)
26 | } else {
27 | listen, err := net.Listen("tcp", ":"+strconv.Itoa(port))
28 | if err != nil {
29 | return false
30 | }
31 | defer func(listen net.Listener) {
32 | _ = listen.Close()
33 | }(listen)
34 | }
35 | return true
36 | }
37 |
38 | // ToAddress scope=local时,返回本地,默认为全局
39 | func ToAddress(scope string, port int) string {
40 | ip := "" // 默认为开放
41 | if scope == "local" {
42 | ip = "127.0.0.1"
43 | }
44 | return ip + ":" + strconv.Itoa(port)
45 | }
46 |
47 | // Md5 字符串md5加密处理
48 | func Md5(str string) string {
49 | h := md5.New()
50 | h.Write([]byte(str))
51 | return hex.EncodeToString(h.Sum(nil))
52 | }
53 |
54 | // UUID V4 基于随机数
55 | func UUID() string {
56 | return uuid.New().String()
57 | }
58 |
59 | // loc 时区Asia/Shanghai
60 | var loc, _ = time.LoadLocation("Local")
61 |
62 | // PreTs 前缀添加时间戳(纳秒)
63 | func PreTs(s string) string {
64 | return time.Now().Format("20060102150405.000000000") + "." + s
65 | }
66 |
67 | // NowDayStart 当天的开始时间
68 | func NowDayStart() time.Time {
69 | nowtDayStart, _ := time.ParseInLocation("2006-01-02", time.Now().Format("2006-01-02"), loc)
70 | return nowtDayStart
71 | }
72 |
73 | // DayStart 将目标时间转为当天的开始时间
74 | func DayStart(target time.Time) time.Time {
75 | parse, err := time.ParseInLocation("2006-01-02", target.Format("2006-01-02"), loc)
76 | if err != nil {
77 | logrus.Errorf("%v", err.Error())
78 | }
79 | return parse
80 | }
81 |
82 | // Contains 检查特定字符串是否在切片中
83 | func Contains(slice []string, element string) bool {
84 | for _, value := range slice {
85 | if value == element {
86 | return true
87 | }
88 | }
89 | return false
90 | }
91 |
92 | // RemoveIf 从切片中移除特定字符串
93 | func RemoveIf(slice []string, element string) []string {
94 | var result []string
95 | for _, s := range slice {
96 | if s != element {
97 | result = append(result, s)
98 | }
99 | }
100 | return result
101 | }
102 |
103 | func NoneMatch(slice []string, predicate func(string) bool, def bool) bool {
104 | if slice != nil && len(slice) != 0 {
105 | for _, v := range slice {
106 | if predicate(v) {
107 | return false
108 | }
109 | }
110 | return true
111 | }
112 | return def
113 | }
114 |
115 | // Distinct 去重
116 | func Distinct(s []string) []string {
117 | keys := make(map[string]bool)
118 | list := make([]string, 0)
119 | for _, entry := range s {
120 | if _, value := keys[entry]; !value {
121 | keys[entry] = true
122 | list = append(list, entry)
123 | }
124 | }
125 | return list
126 | }
127 |
128 | // GeneratePassword 生成指定长度的密码
129 | func GeneratePassword(passLen int) (string, error) {
130 | // 检查密码长度是否小于6
131 | if passLen < 6 {
132 | return "", errors.New("password length must be not be less than 6")
133 | }
134 | // 定义包含3类字符的字符集(62个字符)
135 | charsetBytes := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
136 | charsetLen := byte(len(charsetBytes))
137 | // 计算最大有效值以确保均匀分布
138 | maxValue := byte(255 - (256 % uint(charsetLen)))
139 | password := make([]byte, passLen)
140 | for i := 0; i < passLen; {
141 | buf := make([]byte, 1)
142 | if _, err := rand.Read(buf); err != nil {
143 | return "", err
144 | }
145 | // 过滤超出范围的字节值
146 | if buf[0] <= maxValue {
147 | password[i] = charsetBytes[buf[0]%charsetLen]
148 | i++
149 | }
150 | }
151 | return string(password), nil
152 | }
153 |
--------------------------------------------------------------------------------
/web/s-cert.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAv2q/KDfpOc10RfS7L/0zf/ytJl5gf+aaEhV82Y3Knjn5Q6Xc
3 | t4ldBQ3fP+ziVglPuX/dcO/CUoCwvbLYenDUG0odP1ruMVZjleLk0OlUjQMnxrAO
4 | UYlFjNkrX5ryIVuYrPYgxn3283LiXWuHoUD9jbYyf0FR9w6qK9LFHDEMIvdgyYyT
5 | kc6sEaK5BfTR8Tbx2NP6Ej5ZFJF1/8NsTzxzBPPLD6VETBHNI/slfa0xoby8aoen
6 | d06PSoZZGonh6I/syNAu9xNdM9rB88Sx1ysUQgl/uACue6HsovG0kgeCMSMmsPrM
7 | YL7tS01ZJ33sARppjtI42XxpHrH152Do6zt9KwIDAQABAoIBAEFjAoeHidjf8O8Q
8 | qXy8HoKC2tb3eDlYmZrB0lMyl1szbI2KM/pSJv9Z/MAGeE5xgdVY81jn3dZ29Wjn
9 | lgFFV3828wS4WBNscjo6NnWSrvo4cLbzXwDFRofVi3ZuJHX2pxG2Rf3n+5qvzNmi
10 | qMMRw0tMSLWlp40gakrsBb8alg2/IevSSxKjRMt5HsihNbbLydR2qSGNBcmsDKQB
11 | uLzwmpgPeyhUdvBeX+99yqwJ6AJ4GpDyi1dGgbsiK/Z5F0T8AwD0ic+LCkuWFCL0
12 | K2HBx/HsfrTpj5dGcDTxxFI1JdvJWcioPYvUlDcHg98i/zwng6eCx/d3Q+jzK5w3
13 | 6/NYwrECgYEA5i4FO8Va73hAACI005wEqjJRsapI5tI4ckFLtiwFnU385GdeNJzv
14 | CGtBMRCFpEAdVKmqs+EVHKntPg4+LzfYKnsyBA9qnns7kHjP2Efqfq/ixAOZMaeX
15 | ++pPgQJ2bEszjqaXakXdykoDbr21GAM1fPBoslY9OE51p1tOGGCKMZMCgYEA1OOX
16 | Rf6r75GfQd0JnvCys5iNZwnhKvbR7UvKjRDv09fZ6/Q5HmFZldmmxq0SGWmgQzSz
17 | UvVg5i8pc4DghMzJvDtCFSGgPTLn2xNTvECB8Jwm7o1rJw39/CnamMkadLg9Cb2i
18 | g8IMn9Mml+xx1hI9hsbL38vEKpoFNKFqvO1kpQkCgYEArfeCRRZ4EB2WYYN44aY9
19 | cFTvoZPN3YZs2w22p0zGQYm75PSrIqCpmHdXojmWh/ldMau6NJGdXzie8hPZs95F
20 | JnZN6vur3XPOJPbqP9C6zl0oynTdx8We/OquhBbUYizEHsCSF+QOKOGfjoca47cp
21 | KfCZcI/1XSUPjxlXAN2WFLkCgYASnPuC8StTPOYxugO3U9AsB7CFS8XWHdJo7vF8
22 | t/hgC0VQbf/4egZ9JZSBVmx4sFWEyrzLCg040vLK2H/I3KbewEec1V3PO/4tl1kA
23 | 4pr50I1O2ip+Naj5PSeRqDOZ9OnRSjVFU9gKuUlsiw3A68NZX1Q/8u7p0qGV4m8U
24 | qaTdEQKBgQCRNs3pSvr5MBst/TpLddrBBwRxBdZHZkgCecoKC9OtWG3llcKCO939
25 | LuDR8TcTGx6cga9CJsdxF7uxViRl1ggtRe8qZUwnTWLFdobPP6Zo4VpjJhtPANwl
26 | PIm4B0GReEUSmAcOCAERMZh4n/3BYUNRu/EMgEB3XEyt28JstoNDSQ==
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/web/s-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDWTCCAkGgAwIBAgIJAK2vWakY/TwYMA0GCSqGSIb3DQEBCwUAMEIxCzAJBgNV
3 | BAYTAkNOMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
4 | Q29tcGFueSBMdGQwIBcNMjAwOTExMDgyODQ4WhgPMjEyMDA4MTgwODI4NDhaMEIx
5 | CzAJBgNVBAYTAkNOMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl
6 | ZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
7 | AQC/ar8oN+k5zXRF9Lsv/TN//K0mXmB/5poSFXzZjcqeOflDpdy3iV0FDd8/7OJW
8 | CU+5f91w78JSgLC9sth6cNQbSh0/Wu4xVmOV4uTQ6VSNAyfGsA5RiUWM2StfmvIh
9 | W5is9iDGffbzcuJda4ehQP2NtjJ/QVH3Dqor0sUcMQwi92DJjJORzqwRorkF9NHx
10 | NvHY0/oSPlkUkXX/w2xPPHME88sPpURMEc0j+yV9rTGhvLxqh6d3To9KhlkaieHo
11 | j+zI0C73E10z2sHzxLHXKxRCCX+4AK57oeyi8bSSB4IxIyaw+sxgvu1LTVknfewB
12 | GmmO0jjZfGkesfXnYOjrO30rAgMBAAGjUDBOMB0GA1UdDgQWBBR31XafwrSG1VCh
13 | SIaZ6E5lHVnB9DAfBgNVHSMEGDAWgBR31XafwrSG1VChSIaZ6E5lHVnB9DAMBgNV
14 | HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC7Ai/2bklqngszXsQTH0B3loLn
15 | DUjE8yiL/1aryat09fB0HL/LfuKESkNnNLSWqH3mKzXgOcnx5zR7T+i/83kHgtkj
16 | kDn0saaFdVPudTYiJuYhsnVKnaQwGeGfPz3deiLT8qnNgBraw2148+OhyXi4v9y9
17 | CPrGQq58SaULPxr0SJCpcSsUaNRiBYKHHYfLrBv4nbJ3VOwRhpEI8pAQ7v2UlbCN
18 | 6wW115ZkeeZiIk5NMl4garHXBM+1vm0FqLUNwRIfTK2eX4n9NnZXepgDJFJXvL6x
19 | 8e/uTN4rKAHIBBxRZ0lUxd1C0I0dTNJBrc7aCVnahlaSOc1szZvPXM/JWX1f
20 | -----END CERTIFICATE-----
21 |
--------------------------------------------------------------------------------
/web/static/css/app.86591c84.css:
--------------------------------------------------------------------------------
1 | .fade-enter-active,.fade-leave-active{-webkit-transition:opacity .28s;transition:opacity .28s}.fade-enter,.fade-leave-active{opacity:0}.fade-transform-enter-active,.fade-transform-leave-active{-webkit-transition:all .5s;transition:all .5s}.fade-transform-enter{opacity:0;-webkit-transform:translateX(-30px);transform:translateX(-30px)}.fade-transform-leave-to{opacity:0;-webkit-transform:translateX(30px);transform:translateX(30px)}.breadcrumb-enter-active,.breadcrumb-leave-active{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-enter,.breadcrumb-leave-active{opacity:0;-webkit-transform:translateX(20px);transform:translateX(20px)}.breadcrumb-move{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-leave-active{position:absolute}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:400!important}.el-upload input[type=file]{display:none!important}.el-upload__input{display:none}.el-dialog{-webkit-transform:none;transform:none;left:0;position:relative;margin:0 auto}.upload-container .el-upload{width:100%}.upload-container .el-upload .el-upload-dragger{width:100%;height:200px}.el-dropdown-menu a{display:block}.el-range-separator{-webkit-box-sizing:content-box;box-sizing:content-box}#app .main-container{min-height:100%;-webkit-transition:margin-left .28s;transition:margin-left .28s;margin-left:210px;position:relative}#app .sidebar-container{-webkit-transition:width .28s;transition:width .28s;width:210px!important;background-color:#304156;height:100%;position:fixed;font-size:0;top:0;bottom:0;left:0;z-index:1001;overflow:hidden}#app .sidebar-container .horizontal-collapse-transition{-webkit-transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out;transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out}#app .sidebar-container .scrollbar-wrapper{overflow-x:hidden!important}#app .sidebar-container .el-scrollbar__bar.is-vertical{right:0}#app .sidebar-container .el-scrollbar{height:100%}#app .sidebar-container.has-logo .el-scrollbar{height:calc(100% - 50px)}#app .sidebar-container .is-horizontal{display:none}#app .sidebar-container a{display:inline-block;width:100%;overflow:hidden}#app .sidebar-container .svg-icon{margin-right:16px}#app .sidebar-container .sub-el-icon{margin-right:12px;margin-left:-2px}#app .sidebar-container .el-menu{border:none;height:100%;width:100%!important}#app .sidebar-container .el-submenu__title:hover,#app .sidebar-container .submenu-title-noDropdown:hover{background-color:#263445!important}#app .sidebar-container .is-active>.el-submenu__title{color:#f4f4f5!important}#app .sidebar-container .el-submenu .el-menu-item,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title{min-width:210px!important;background-color:#1f2d3d!important}#app .sidebar-container .el-submenu .el-menu-item:hover,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#001528!important}#app .hideSidebar .sidebar-container{width:54px!important}#app .hideSidebar .main-container{margin-left:54px}#app .hideSidebar .submenu-title-noDropdown{padding:0!important;position:relative}#app .hideSidebar .submenu-title-noDropdown .el-tooltip{padding:0!important}#app .hideSidebar .submenu-title-noDropdown .el-tooltip .svg-icon{margin-left:20px}#app .hideSidebar .submenu-title-noDropdown .el-tooltip .sub-el-icon{margin-left:19px}#app .hideSidebar .el-submenu{overflow:hidden}#app .hideSidebar .el-submenu>.el-submenu__title{padding:0!important}#app .hideSidebar .el-submenu>.el-submenu__title .svg-icon{margin-left:20px}#app .hideSidebar .el-submenu>.el-submenu__title .sub-el-icon{margin-left:19px}#app .hideSidebar .el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}#app .hideSidebar .el-menu--collapse .el-submenu>.el-submenu__title>span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}#app .el-menu--collapse .el-menu .el-submenu{min-width:210px!important}#app .mobile .main-container{margin-left:0}#app .mobile .sidebar-container{-webkit-transition:-webkit-transform .28s;transition:-webkit-transform .28s;transition:transform .28s;transition:transform .28s,-webkit-transform .28s;width:210px!important}#app .mobile.hideSidebar .sidebar-container{pointer-events:none;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:translate3d(-210px,0,0);transform:translate3d(-210px,0,0)}#app .withoutAnimation .main-container,#app .withoutAnimation .sidebar-container{-webkit-transition:none;transition:none}.el-menu--vertical>.el-menu .svg-icon{margin-right:16px}.el-menu--vertical>.el-menu .sub-el-icon{margin-right:12px;margin-left:-2px}.el-menu--vertical .el-menu-item:hover,.el-menu--vertical .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#263445!important}.el-menu--vertical>.el-menu--popup{max-height:100vh;overflow-y:auto}.el-menu--vertical>.el-menu--popup::-webkit-scrollbar-track-piece{background:#d3dce6}.el-menu--vertical>.el-menu--popup::-webkit-scrollbar{width:6px}.el-menu--vertical>.el-menu--popup::-webkit-scrollbar-thumb{background:#99a9bf;border-radius:20px}body{height:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif}label{font-weight:700}html{-webkit-box-sizing:border-box;box-sizing:border-box}#app,html{height:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}a:active,a:focus{outline:none}a,a:focus,a:hover{cursor:pointer;color:inherit;text-decoration:none}div:focus{outline:none}.clearfix:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}.app-container{padding:20px}.el-dialog{min-width:640px;max-width:680px;border-radius:8px!important;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.el-dialog .el-form-item__content{margin-right:20px!important}.nat-font-red{color:red!important}.nat-font-gray{color:grey!important}.nat-font-green{color:green!important}.nat-font-warn{color:#e6a23c!important}.app-breadcrumb.el-breadcrumb[data-v-64b20714]{display:inline-block;font-size:14px;line-height:50px;margin-left:8px}.app-breadcrumb.el-breadcrumb .no-redirect[data-v-64b20714]{color:#97a8be;cursor:text}.hamburger[data-v-49e15297]{display:inline-block;vertical-align:middle;width:20px;height:20px}.hamburger.is-active[data-v-49e15297]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.navbar[data-v-b011da3c]{height:50px;overflow:hidden;position:relative;background:#fff;-webkit-box-shadow:0 1px 4px rgba(0,21,41,.08);box-shadow:0 1px 4px rgba(0,21,41,.08)}.navbar .hamburger-container[data-v-b011da3c]{line-height:46px;height:100%;float:left;cursor:pointer;-webkit-transition:background .3s;transition:background .3s;-webkit-tap-highlight-color:transparent}.navbar .hamburger-container[data-v-b011da3c]:hover{background:rgba(0,0,0,.025)}.navbar .breadcrumb-container[data-v-b011da3c]{float:left}.navbar .right-menu[data-v-b011da3c]{float:right;height:100%;line-height:50px}.navbar .right-menu[data-v-b011da3c]:focus{outline:none}.navbar .right-menu .right-menu-item[data-v-b011da3c]{display:inline-block;padding:0 8px;height:100%;font-size:18px;color:#5a5e66;vertical-align:text-bottom}.navbar .right-menu .right-menu-item.hover-effect[data-v-b011da3c]{cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .right-menu .right-menu-item.hover-effect[data-v-b011da3c]:hover{background:rgba(0,0,0,.025)}.navbar .right-menu .avatar-container[data-v-b011da3c]{margin-right:30px}.navbar .right-menu .avatar-container .avatar-wrapper[data-v-b011da3c]{margin-top:5px;position:relative}.navbar .right-menu .avatar-container .avatar-wrapper .user-avatar[data-v-b011da3c]{cursor:pointer;width:36px;height:36px;border-radius:18px}.navbar .right-menu .avatar-container .avatar-wrapper .el-icon-caret-bottom[data-v-b011da3c]{cursor:pointer;position:absolute;right:-20px;top:25px;font-size:12px}.sidebarLogoFade-enter-active[data-v-9e4b43fc]{-webkit-transition:opacity 1.5s;transition:opacity 1.5s}.sidebarLogoFade-enter[data-v-9e4b43fc],.sidebarLogoFade-leave-to[data-v-9e4b43fc]{opacity:0}.sidebar-logo-container[data-v-9e4b43fc]{position:relative;width:100%;height:50px;line-height:50px;background:#2b2f3a;text-align:center;overflow:hidden}.sidebar-logo-container .sidebar-logo-link[data-v-9e4b43fc]{height:100%;width:100%}.sidebar-logo-container .sidebar-logo-link .sidebar-logo[data-v-9e4b43fc]{width:32px;height:32px;vertical-align:middle;margin-right:12px}.sidebar-logo-container .sidebar-logo-link .sidebar-title[data-v-9e4b43fc]{display:inline-block;margin:0;color:#fff;font-weight:600;line-height:50px;font-size:14px;font-family:Avenir,Helvetica Neue,Arial,Helvetica,sans-serif;vertical-align:middle}.sidebar-logo-container.collapse .sidebar-logo[data-v-9e4b43fc]{margin-right:0}.sub-el-icon[data-v-18eeea00]{color:currentColor;width:1em;height:1em}.app-main[data-v-64cf4d83]{min-height:calc(100vh - 50px);width:100%;position:relative;overflow:hidden}.fixed-header+.app-main[data-v-64cf4d83]{padding-top:50px}.el-popup-parent--hidden .fixed-header{padding-right:15px}[data-v-4f739cf0]:export{menuText:#bfcbd9;menuActiveText:#409eff;subMenuActiveText:#f4f4f5;menuBg:#304156;menuHover:#263445;subMenuBg:#1f2d3d;subMenuHover:#001528;sideBarWidth:210px}.app-wrapper[data-v-4f739cf0]{position:relative;height:100%;width:100%}.app-wrapper[data-v-4f739cf0]:after{content:"";display:table;clear:both}.app-wrapper.mobile.openSidebar[data-v-4f739cf0]{position:fixed;top:0}.drawer-bg[data-v-4f739cf0]{background:#000;opacity:.3;width:100%;top:0;height:100%;position:absolute;z-index:999}.fixed-header[data-v-4f739cf0]{position:fixed;top:0;right:0;z-index:9;width:calc(100% - 210px);-webkit-transition:width .28s;transition:width .28s}.hideSidebar .fixed-header[data-v-4f739cf0]{width:calc(100% - 54px)}.mobile .fixed-header[data-v-4f739cf0]{width:100%}.svg-icon[data-v-f9f7fefc]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}.svg-external-icon[data-v-f9f7fefc]{background-color:currentColor;-webkit-mask-size:cover!important;mask-size:cover!important;display:inline-block}
--------------------------------------------------------------------------------
/web/static/css/chunk-1d778d6e.93fae406.css:
--------------------------------------------------------------------------------
1 | .dashboard-container-container[data-v-76457b1c]{margin:30px}.dashboard-container-text[data-v-76457b1c]{font-size:30px;line-height:46px}.dashboard-container[data-v-76457b1c]{padding:32px;background-color:#f0f2f5;position:relative}.chart-wrapper[data-v-76457b1c]{background:#fff;padding:16px 16px 0;margin-bottom:32px}@media(max-width:1024px){.chart-wrapper[data-v-76457b1c]{padding:8px}}
--------------------------------------------------------------------------------
/web/static/css/chunk-22cea610.3c7f5ad9.css:
--------------------------------------------------------------------------------
1 | .wscn-http404-container[data-v-c095f994]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-c095f994]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-c095f994]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-c095f994]{width:100%}.wscn-http404 .pic-404__child[data-v-c095f994]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-c095f994]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-c095f994;animation-name:cloudLeft-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-c095f994]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-c095f994;animation-name:cloudMid-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-c095f994]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-c095f994;animation-name:cloudRight-data-v-c095f994;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-c095f994{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-c095f994{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-c095f994{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-c095f994{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-c095f994{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-c095f994{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-c095f994]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-c095f994]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-c095f994],.wscn-http404 .bullshit__oops[data-v-c095f994]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-c095f994;animation-name:slideUp-data-v-c095f994;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-c095f994]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-c095f994]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-c095f994],.wscn-http404 .bullshit__return-home[data-v-c095f994]{opacity:0;-webkit-animation-name:slideUp-data-v-c095f994;animation-name:slideUp-data-v-c095f994;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-c095f994]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-c095f994{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-c095f994{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}
--------------------------------------------------------------------------------
/web/static/css/chunk-3ad411e6.f9c4d84f.css:
--------------------------------------------------------------------------------
1 | .waves-ripple{position:absolute;border-radius:100%;background-color:rgba(0,0,0,.15);background-clip:padding-box;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transform:scale(0);transform:scale(0);opacity:1}.waves-ripple.z-active{opacity:0;-webkit-transform:scale(2);transform:scale(2);-webkit-transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out,-webkit-transform .6s ease-out}.pagination-container[data-v-6af373ef]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-6af373ef]{display:none}.filter-container[data-v-21b0d7c5]{padding-bottom:10px}.filter-item[data-v-21b0d7c5]{display:inline-block;vertical-align:middle;margin-bottom:10px}.el-tag+.el-tag[data-v-21b0d7c5]{margin-left:10px}.button-new-tag[data-v-21b0d7c5]{//margin-left:10px;height:32px;line-height:30px;padding-top:0;padding-bottom:0}.input-new-tag[data-v-21b0d7c5]{width:90px;//margin-left:10px;vertical-align:bottom}
--------------------------------------------------------------------------------
/web/static/css/chunk-4e2a48e4.71202b84.css:
--------------------------------------------------------------------------------
1 | @supports(-webkit-mask:none) and (not (cater-color:#283443)){.login-container .el-input input{color:#283443}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#283443;height:47px;caret-color:#283443}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #fff inset!important;box-shadow:inset 0 0 0 1000px #fff!important;-webkit-text-fill-color:#283443!important}.login-container .el-form-item{border:1px solid #dcdcdc;background:#fff;border-radius:5px;color:#000}.login-container[data-v-4839dfc7]{min-height:100%;width:100%;background:#2d3a4b url(/static/img/white-slash.png) repeat;overflow:hidden;padding-top:12%}.login-container .login-form[data-v-4839dfc7]{background:hsla(0,0%,100%,.7);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);position:relative;width:520px;max-width:100%;margin:0 auto;padding:80px 35px 0;border-radius:12px;overflow:hidden}.login-container .tips[data-v-4839dfc7]{font-size:14px;color:#a9a9a9;margin-bottom:10px}.login-container .tips span[data-v-4839dfc7]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-4839dfc7]{padding:6px 5px 6px 15px;color:#000;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-4839dfc7]{position:relative}.login-container .title-container .title[data-v-4839dfc7]{font-size:26px;color:#000;margin:0 auto 40px auto;text-align:center;font-weight:700}.login-container .show-pwd[data-v-4839dfc7]{position:absolute;right:10px;top:7px;font-size:16px;color:#000;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .vertify_code[data-v-4839dfc7]{width:60%}.login-container .vertify_img[data-v-4839dfc7]{position:absolute;right:2px;bottom:2px;width:128px}
--------------------------------------------------------------------------------
/web/static/css/chunk-75646770.e17cce70.css:
--------------------------------------------------------------------------------
1 | .waves-ripple{position:absolute;border-radius:100%;background-color:rgba(0,0,0,.15);background-clip:padding-box;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transform:scale(0);transform:scale(0);opacity:1}.waves-ripple.z-active{opacity:0;-webkit-transform:scale(2);transform:scale(2);-webkit-transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out,-webkit-transform .6s ease-out}.pagination-container[data-v-6af373ef]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-6af373ef]{display:none}.filter-container[data-v-d89c3f8c]{padding-bottom:10px}.filter-item[data-v-d89c3f8c]{display:inline-block;vertical-align:middle;margin-bottom:10px}.el-tag+.el-tag[data-v-d89c3f8c]{margin-left:10px}.button-new-tag[data-v-d89c3f8c]{//margin-left:10px;height:32px;line-height:30px;padding-top:0;padding-bottom:0}.input-new-tag[data-v-d89c3f8c]{width:126px;vertical-align:bottom}
--------------------------------------------------------------------------------
/web/static/css/chunk-94143924.1d17f386.css:
--------------------------------------------------------------------------------
1 | .waves-ripple{position:absolute;border-radius:100%;background-color:rgba(0,0,0,.15);background-clip:padding-box;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transform:scale(0);transform:scale(0);opacity:1}.waves-ripple.z-active{opacity:0;-webkit-transform:scale(2);transform:scale(2);-webkit-transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,-webkit-transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out;transition:opacity 1.2s ease-out,transform .6s ease-out,-webkit-transform .6s ease-out}.pagination-container[data-v-6af373ef]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-6af373ef]{display:none}.filter-container[data-v-0d55bb74]{padding-bottom:10px}.filter-item[data-v-0d55bb74]{display:inline-block;vertical-align:middle;margin-bottom:10px}
--------------------------------------------------------------------------------
/web/static/css/chunk-libs.3dfb7769.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;-webkit-box-shadow:0 0 10px #29d,0 0 5px #29d;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translateY(-4px);transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;-webkit-box-sizing:border-box;box-sizing:border-box;border:2px solid transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}
--------------------------------------------------------------------------------
/web/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/favicon.ico
--------------------------------------------------------------------------------
/web/static/fonts/element-icons.535877f5.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/fonts/element-icons.535877f5.woff
--------------------------------------------------------------------------------
/web/static/fonts/element-icons.732389de.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/fonts/element-icons.732389de.ttf
--------------------------------------------------------------------------------
/web/static/img/404.a57b6f31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/img/404.a57b6f31.png
--------------------------------------------------------------------------------
/web/static/img/404_cloud.0f4bc32b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/img/404_cloud.0f4bc32b.png
--------------------------------------------------------------------------------
/web/static/img/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/img/avatar.png
--------------------------------------------------------------------------------
/web/static/img/white-slash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natokay/go-natok-server/ca7b34212c0f0e5c5ad0ccf1f99f40d6d546b2e5/web/static/img/white-slash.png
--------------------------------------------------------------------------------
/web/static/js/chunk-1d778d6e.f343b652.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-1d778d6e"],{"437e":function(t,e,a){},"737d":function(t,e,a){"use strict";a("437e")},9406:function(t,e,a){"use strict";a.r(e);var i=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"dashboard-container"},[a("el-row",{staticStyle:{background:"#fff",padding:"16px 16px 0","margin-bottom":"32px"}},[a("line-chart",{attrs:{"chart-data":t.lineChartStreamData}})],1),a("el-row",{attrs:{gutter:16}},[a("el-col",{attrs:{xs:24,sm:24,lg:8}},[a("div",{staticClass:"chart-wrapper"},[a("pie-chart",{attrs:{"chart-data":t.pieChartClientData}})],1)]),a("el-col",{attrs:{xs:24,sm:24,lg:8}},[a("div",{staticClass:"chart-wrapper"},[a("pie-chart",{attrs:{"chart-data":t.pieChartPortData}})],1)]),a("el-col",{attrs:{xs:24,sm:24,lg:8}},[a("div",{staticClass:"chart-wrapper"},[a("pie-chart",{attrs:{"chart-data":t.pieChartProtocolData}})],1)])],1)],1)},n=[],r=a("5530"),s=a("2f62"),o=a("b775");function c(t){return Object(o["a"])({url:"/dashboard/state",method:"get",data:t})}var h=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{class:t.className,style:{height:t.height,width:t.width}})},d=[];a("b0c0"),a("53ca"),a("ac1f"),a("00b4"),a("5319"),a("4d63"),a("2c3e"),a("25f0"),a("d3b7"),a("4d90"),a("a15b"),a("d81d"),a("b64b"),a("159b"),a("fb6a"),a("a630"),a("3ca3"),a("6062"),a("ddb0"),a("466d");function l(t,e,a){var i,n,r,s,o,c=function c(){var h=+new Date-s;h0?i=setTimeout(c,e-h):(i=null,a||(o=t.apply(r,n),i||(r=n=null)))};return function(){for(var n=arguments.length,h=new Array(n),d=0;d0&&void 0!==arguments[0]?arguments[0]:{},e=t.input,a=t.output,i=t.date;this.chart.setOption({grid:{left:10,right:10,bottom:20,top:30,containLabel:!0},tooltip:{trigger:"axis",axisPointer:{type:"cross"},padding:[5,10]},xAxis:{data:i,boundaryGap:!1,axisTick:{show:!1}},yAxis:{axisTick:{show:!1}},legend:{},series:[{itemStyle:{normal:{color:"#FF005A",lineStyle:{color:"#FF005A",width:2}}},name:e.name,data:e.data,smooth:!0,type:"line",animationDuration:200,animationEasing:"cubicInOut"},{itemStyle:{normal:{color:"#3888fa",lineStyle:{color:"#3888fa",width:2},areaStyle:{color:"#f3f8ff"}}},name:a.name,data:a.data,smooth:!0,type:"line",animationDuration:200,animationEasing:"quadraticOut"}]})}}},f=m,b=a("2877"),v=Object(b["a"])(f,h,d,!1,null,null,null),y=v.exports,g=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{class:t.className,style:{height:t.height,width:t.width}})},$=[],_=a("313e");a("817d");var w={mixins:[u],props:{className:{type:String,default:"chart"},width:{type:String,default:"100%"},height:{type:String,default:"300px"},chartData:{type:Object,required:!0}},data:function(){return{chart:null}},watch:{chartData:{deep:!0,handler:function(){this.initChart()}}},mounted:function(){var t=this;this.$nextTick((function(){t.initChart()}))},beforeDestroy:function(){this.chart&&(this.chart.dispose(),this.chart=null)},methods:{initChart:function(){this.chart=_.init(this.$el,"macarons"),this.setOptions(this.chartData)},setOptions:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=t.name,a=t.data;this.chart.setOption({tooltip:{trigger:"item",formatter:"{a}
{b} : {c} ({d}%)"},title:{text:e,x:"left",y:"top",textAlign:"left"},legend:{left:"center",bottom:"0",itemWidth:21,itemHeight:12,textStyle:{color:"rgba(25,25,112,.9)",fontSize:"12"}},series:[{name:e,type:"pie",roseType:"radius",radius:["40%","60%"],center:["50%","52%"],data:a,animationEasing:"cubicInOut",animationDuration:200}]})}}},D=w,C=Object(b["a"])(D,g,$,!1,null,null,null),E=C.exports,x={name:"Dashboard",components:{LineChart:y,PieChart:E},data:function(){return{lineChartStreamData:{date:[],input:{name:"流入流量",data:[]},output:{name:"流出流量",data:[]}},pieChartClientData:{name:"客户端状态",data:[]},pieChartPortData:{name:"端口映射",data:[]},pieChartProtocolData:{name:"协议类型",data:[]},lineChartRunData:{name:"连接池",data:[]}}},created:function(){this.loadReportData()},mounted:function(){this.$nextTick((function(){}))},methods:{loadReportData:function(){var t=this;c().then((function(e){var a=e.data;for(var i in t.lineChartStreamData=a.stream,a.client)t.pieChartClientData.data.push({name:i,value:a.client[i]});for(var n in a.port)t.pieChartPortData.data.push({name:n,value:a.port[n]});for(var r in a.protocol)t.pieChartProtocolData.data.push({name:r,value:a.protocol[r]});for(var s in a.run)t.lineChartRunData.data.push({name:s,value:a.run[s]})}))}},computed:Object(r["a"])({},Object(s["b"])(["name"]))},z=x,S=(a("737d"),Object(b["a"])(z,i,n,!1,null,"76457b1c",null));e["default"]=S.exports}}]);
--------------------------------------------------------------------------------
/web/static/js/chunk-22cea610.396870de.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-22cea610"],{"26fc":function(t,s,a){t.exports=a.p+"static/img/404_cloud.0f4bc32b.png"},"8cdb":function(t,s,a){"use strict";a.r(s);var e=function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"wscn-http404-container"},[a("div",{staticClass:"wscn-http404"},[t._m(0),a("div",{staticClass:"bullshit"},[a("div",{staticClass:"bullshit__oops"},[t._v("OOPS!")]),t._m(1),a("div",{staticClass:"bullshit__headline"},[t._v(t._s(t.message))]),a("div",{staticClass:"bullshit__info"},[t._v("Please check that the URL you entered is correct, or click the button below to return to the homepage.")]),a("a",{staticClass:"bullshit__return-home",attrs:{href:""}},[t._v("Back to home")])])])])},c=[function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"pic-404"},[e("img",{staticClass:"pic-404__parent",attrs:{src:a("a36b"),alt:"404"}}),e("img",{staticClass:"pic-404__child left",attrs:{src:a("26fc"),alt:"404"}}),e("img",{staticClass:"pic-404__child mid",attrs:{src:a("26fc"),alt:"404"}}),e("img",{staticClass:"pic-404__child right",attrs:{src:a("26fc"),alt:"404"}})])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"bullshit__info"},[t._v("All rights reserved "),a("a",{staticStyle:{color:"#20a0ff"},attrs:{href:"https://wallstreetcn.com",target:"_blank"}},[t._v("wallstreetcn")])])}],i={name:"Page404",computed:{message:function(){return"The webmaster said that you can not enter this page..."}}},l=i,n=(a("dd53"),a("2877")),r=Object(n["a"])(l,e,c,!1,null,"c095f994",null);s["default"]=r.exports},a36b:function(t,s,a){t.exports=a.p+"static/img/404.a57b6f31.png"},b0a8:function(t,s,a){},dd53:function(t,s,a){"use strict";a("b0a8")}}]);
--------------------------------------------------------------------------------
/web/static/js/chunk-4e2a48e4.3809de84.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4e2a48e4"],{"2c5e":function(e,t,s){"use strict";s("321e")},"2d22":function(e,t,s){},"321e":function(e,t,s){},"87b5":function(e,t,s){"use strict";s("2d22")},"9ed6":function(e,t,s){"use strict";s.r(t);var o=function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("div",{staticClass:"login-container"},[s("el-form",{ref:"loginForm",staticClass:"login-form",attrs:{model:e.loginForm,rules:e.loginRules,"auto-complete":"on","label-position":"left"}},[s("div",{staticClass:"title-container"},[s("h3",{staticClass:"title"},[e._v("NATOK·ADMIN")])]),s("el-form-item",{attrs:{prop:"username"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"user"}})],1),s("el-input",{ref:"username",attrs:{placeholder:"请输入账号",name:"username",type:"text",tabindex:"1","auto-complete":"on"},model:{value:e.loginForm.username,callback:function(t){e.$set(e.loginForm,"username",t)},expression:"loginForm.username"}})],1),s("el-form-item",{attrs:{prop:"password"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"password"}})],1),s("el-input",{key:e.passwordType,ref:"password",attrs:{type:e.passwordType,placeholder:"请输入密码",name:"password",tabindex:"2","auto-complete":"on"},nativeOn:{keyup:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleLogin(t)}},model:{value:e.loginForm.password,callback:function(t){e.$set(e.loginForm,"password",t)},expression:"loginForm.password"}}),s("span",{staticClass:"show-pwd",on:{click:e.showPwd}},[s("svg-icon",{attrs:{"icon-class":"password"===e.passwordType?"eye":"eye-open"}})],1)],1),s("el-form-item",{attrs:{prop:"code"}},[s("span",{staticClass:"svg-container"},[s("i",{staticClass:"el-icon-mobile-phone"})]),s("el-input",{staticClass:"vertify_code",attrs:{type:"text",placeholder:"点击图片更换验证码","auto-complete":"false"},model:{value:e.loginForm.code,callback:function(t){e.$set(e.loginForm,"code",t)},expression:"loginForm.code"}}),s("img",{staticClass:"vertify_img",staticStyle:{cursor:"pointer"},attrs:{src:e.imgUrl},on:{click:e.resetImg}})],1),s("el-button",{staticStyle:{width:"100%","margin-bottom":"30px"},attrs:{loading:e.loading,type:"primary"},nativeOn:{click:function(t){return t.preventDefault(),e.handleLogin(t)}}},[e._v("登录")]),s("div",{staticClass:"tips"},[s("span",{staticStyle:{"margin-right":"20px"}},[e._v("若有疑问请联系管理员")])])],1)],1)},r=[],n=s("61f7"),i={name:"Login",data:function(){var e=function(e,t,s){Object(n["d"])(t)?s():s(new Error("请输入正确的账号"))},t=function(e,t,s){t.length<6?s(new Error("密码不能少于6位")):s()};return{loginForm:{username:"",password:"",code:""},loginRules:{username:[{required:!0,trigger:"blur",validator:e}],password:[{required:!0,trigger:"blur",validator:t}],code:[{required:!0,message:"请输入验证码",trigger:"blur"}]},imgUrl:"/verifyCode",loading:!1,passwordType:"password",redirect:void 0}},watch:{$route:{handler:function(e){this.redirect=e.query&&e.query.redirect},immediate:!0}},methods:{showPwd:function(){var e=this;"password"===this.passwordType?this.passwordType="":this.passwordType="password",this.$nextTick((function(){e.$refs.password.focus()}))},resetImg:function(){this.imgUrl="/verifyCode?t="+(new Date).getTime()},handleLogin:function(){var e=this;this.$refs.loginForm.validate((function(t){if(!t)return console.log("error submit!!"),!1;e.loading=!0,e.$store.dispatch("user/login",e.loginForm).then((function(){e.$router.push({path:e.redirect||"/"}),e.loading=!1})).catch((function(){e.loading=!1}))}))}}},a=i,l=(s("2c5e"),s("87b5"),s("2877")),c=Object(l["a"])(a,o,r,!1,null,"4839dfc7",null);t["default"]=c.exports}}]);
--------------------------------------------------------------------------------
/web/static/js/chunk-75646770.6e5eb569.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-75646770"],{"2cbf":function(e,t,n){"use strict";n("73e0")},"2f00":function(e,t,n){},"333d":function(e,t,n){"use strict";var i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"pagination-container",class:{hidden:e.hidden}},[n("el-pagination",e._b({attrs:{background:e.background,"current-page":e.currentPage,"page-size":e.pageSize,layout:e.layout,"page-sizes":e.pageSizes,total:e.total},on:{"update:currentPage":function(t){e.currentPage=t},"update:current-page":function(t){e.currentPage=t},"update:pageSize":function(t){e.pageSize=t},"update:page-size":function(t){e.pageSize=t},"size-change":e.handleSizeChange,"current-change":e.handleCurrentChange}},"el-pagination",e.$attrs,!1))],1)},a=[];n("a9e3");Math.easeInOutQuad=function(e,t,n,i){return e/=i/2,e<1?n/2*e*e+t:(e--,-n/2*(e*(e-2)-1)+t)};var o=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}();function l(e){document.documentElement.scrollTop=e,document.body.parentNode.scrollTop=e,document.body.scrollTop=e}function r(){return document.documentElement.scrollTop||document.body.parentNode.scrollTop||document.body.scrollTop}function s(e,t,n){var i=r(),a=e-i,s=20,u=0;t="undefined"===typeof t?500:t;var d=function e(){u+=s;var r=Math.easeInOutQuad(u,i,a,t);l(r),u0,expression:"total>0"}],attrs:{total:e.total,page:e.listQuery.page,limit:e.listQuery.limit},on:{"update:page":function(t){return e.$set(e.listQuery,"page",t)},"update:limit":function(t){return e.$set(e.listQuery,"limit",t)},pagination:e.getList}}),n("el-dialog",{attrs:{title:e.dialogMode.title,visible:e.dialogMode.visible,"close-on-click-modal":!1},on:{"update:visible":function(t){return e.$set(e.dialogMode,"visible",t)},close:e.handleCancel}},[n("el-form",{ref:"dialogForm",attrs:{model:e.dialogForm,rules:e.formRules}},[n("el-form-item",{attrs:{label:"标签名称",prop:"tagName","label-width":e.dialogFormLabelWidth}},[n("el-input",{attrs:{placeholder:"标签名称,必填"},model:{value:e.dialogForm.tagName,callback:function(t){e.$set(e.dialogForm,"tagName","string"===typeof t?t.trim():t)},expression:"dialogForm.tagName"}})],1),n("el-form-item",{attrs:{label:"标签备注",prop:"remark","label-width":e.dialogFormLabelWidth}},[n("el-input",{attrs:{type:"textarea",rows:2,placeholder:"请输入内容"},model:{value:e.dialogForm.remark,callback:function(t){e.$set(e.dialogForm,"remark","string"===typeof t?t.trim():t)},expression:"dialogForm.remark"}})],1),n("el-form-item",{attrs:{label:"开放名单",prop:"whitelist","label-width":e.dialogFormLabelWidth}},[n("el-row",{attrs:{gutter:0}},e._l(e.dialogForm.whitelist,(function(t){return n("el-col",{key:"tag-key="+t,attrs:{span:7}},[n("el-tag",{attrs:{"disable-transitions":!1,type:"primary",closable:""},on:{close:function(n){return e.handleClose(t)}}},[e._v(" "+e._s(t)+" ")])],1)})),1),e.dialogFormInputVisible?n("el-input",{ref:"saveTagInput",staticClass:"input-new-tag",attrs:{size:"small"},on:{blur:e.handleInputConfirm},nativeOn:{keyup:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleInputConfirm(t)}},model:{value:e.dialogFormInputValue,callback:function(t){e.dialogFormInputValue=t},expression:"dialogFormInputValue"}}):n("el-button",{staticClass:"button-new-tag",attrs:{plain:"",size:"small",type:"primary",icon:"el-icon-edit"},on:{click:e.showInput}},[e._v("添加 ")])],1)],1),n("div",{ref:"dialogForm",staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[n("el-button",{attrs:{disabled:e.dialogMode.loading},on:{click:e.handleCancel}},[e._v("取消")]),n("el-button",{attrs:{type:"primary",loading:e.dialogMode.loading},on:{click:e.handleSave}},[e._v("确定")])],1)],1)],1)},a=[],o=(n("d3b7"),n("a434"),n("caad"),n("2532"),n("6724")),l=n("d28d"),r=n("333d"),s={components:{Pagination:r["a"]},directives:{waves:o["a"]},data:function(){return{listLoading:!0,total:1,listQuery:{page:1,limit:10,wd:void 0},options:[],tags:[],list:[{tagId:0,tagName:"",remark:"",whitelist:[],created:"",modified:"",enabled:1}],dialogFormLabelWidth:"120px",dialogMode:{option:"add",title:"",visible:!1,loading:!1},dialogForm:{tagId:0,tagName:"",remark:"",whitelist:[],enabled:1},dialogFormInputVisible:!1,dialogFormInputValue:"",formRules:{tagName:[{required:!0,trigger:"blur",message:"请输入标签名称"}]}}},created:function(){this.getList()},methods:{toNum:function(e){return e?e.length:0},handleFilter:function(){this.getList()},getList:function(){var e=this;this.listLoading=!0,Object(l["b"])(this.listQuery).then((function(t){e.list=t.data.items,e.total=t.data.total})).catch((function(e){console.log(e)})).finally((function(){e.listLoading=!1}))},handleOpen:function(e){var t=this;e?(t.dialogMode.option="edit",Object(l["c"])({tagId:e.tagId}).then((function(e){t.dialogForm=e.data.item,t.dialogMode.title="编辑标签名单 · "+t.dialogForm.tagName,t.dialogMode.visible=!0})).catch((function(e){t.$message.error(e)}))):(t.dialogMode.option="add",t.dialogMode.title="添加标签名单",t.dialogMode.visible=!0)},handleClose:function(e){this.dialogForm.whitelist.splice(this.dialogForm.whitelist.indexOf(e),1),this.dialogFormInputVisible=!0,this.dialogFormInputVisible=!1},showInput:function(){var e=this;this.dialogFormInputVisible=!0,this.$nextTick((function(t){e.$refs.saveTagInput.$refs.input.focus()}))},handleInputConfirm:function(){this.dialogFormInputValue&&(this.dialogForm.whitelist.includes(this.dialogFormInputValue)||this.dialogForm.whitelist.push(this.dialogFormInputValue)),this.dialogFormInputVisible=!1,this.dialogFormInputValue=""},handleSave:function(){var e=this,t=this;this.$refs.dialogForm.validate((function(n){n&&(t.dialogMode.loading=!0,Object(l["d"])(e.dialogForm).then((function(e){t.$notify({type:"success",title:"完成",message:e.msg}),t.handleCancel(),t.getList()})).catch((function(e){t.$notify.error({title:"错误",message:e}),console.log(e)})).finally((function(){t.dialogMode.loading=!1})))}))},handleCancel:function(){this.dialogForm={tagId:0,whitelist:[]},this.dialogMode.visible=!1,this.$refs.dialogForm.resetFields()},handSwitch:function(e){var t=this,n=e["enabled"],i=1===n?"停用":"启用";this.$confirm("此操作将"+i+"该端口映射, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){n=1===n?0:1,Object(l["e"])({tagId:e["tagId"],enabled:n}).then((function(){e["enabled"]=n,t.$message({type:"success",message:"处理成功!"})}))})).catch((function(){t.$message({type:"info",message:"操作已取消"})}))},handleDelete:function(e){var t=this;this.$confirm("此操作将删除该端口映射, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){Object(l["a"])({accessKey:e["accessKey"],tagId:e["tagId"]}).then((function(){t.$message({type:"success",message:"处理成功!"}),t.getList()}))})).catch((function(){t.$message({type:"info",message:"操作已取消"})}))},showState:function(e){var t="";return t=0===e["enabled"]?'已停用':'已启用',t}}},u=s,d=(n("49b0"),n("453b"),n("2877")),c=Object(d["a"])(u,i,a,!1,null,"d89c3f8c",null);t["default"]=c.exports},6724:function(e,t,n){"use strict";n("8d41");var i="@@wavesContext";function a(e,t){function n(n){var i=Object.assign({},t.value),a=Object.assign({ele:e,type:"hit",color:"rgba(0, 0, 0, 0.15)"},i),o=a.ele;if(o){o.style.position="relative",o.style.overflow="hidden";var l=o.getBoundingClientRect(),r=o.querySelector(".waves-ripple");switch(r?r.className="waves-ripple":(r=document.createElement("span"),r.className="waves-ripple",r.style.height=r.style.width=Math.max(l.width,l.height)+"px",o.appendChild(r)),a.type){case"center":r.style.top=l.height/2-r.offsetHeight/2+"px",r.style.left=l.width/2-r.offsetWidth/2+"px";break;default:r.style.top=(n.pageY-l.top-r.offsetHeight/2-document.documentElement.scrollTop||document.body.scrollTop)+"px",r.style.left=(n.pageX-l.left-r.offsetWidth/2-document.documentElement.scrollLeft||document.body.scrollLeft)+"px"}return r.style.backgroundColor=a.color,r.className="waves-ripple z-active",!1}}return e[i]?e[i].removeHandle=n:e[i]={removeHandle:n},n}var o={bind:function(e,t){e.addEventListener("click",a(e,t),!1)},update:function(e,t){e.removeEventListener("click",e[i].removeHandle,!1),e.addEventListener("click",a(e,t),!1)},unbind:function(e){e.removeEventListener("click",e[i].removeHandle,!1),e[i]=null,delete e[i]}},l=function(e){e.directive("waves",o)};window.Vue&&(window.waves=o,Vue.use(l)),o.install=l;t["a"]=o},"73e0":function(e,t,n){},8758:function(e,t,n){},"8d41":function(e,t,n){},a434:function(e,t,n){"use strict";var i=n("23e7"),a=n("23cb"),o=n("a691"),l=n("50c4"),r=n("7b0b"),s=n("65f0"),u=n("8418"),d=n("1dde"),c=n("ae40"),g=d("splice"),m=c("splice",{ACCESSORS:!0,0:0,1:2}),p=Math.max,f=Math.min,h=9007199254740991,b="Maximum allowed length exceeded";i({target:"Array",proto:!0,forced:!g||!m},{splice:function(e,t){var n,i,d,c,g,m,v=r(this),y=l(v.length),w=a(e,y),k=arguments.length;if(0===k?n=i=0:1===k?(n=0,i=y-w):(n=k-2,i=f(p(o(t),0),y-w)),y+n-i>h)throw TypeError(b);for(d=s(v,i),c=0;cy-i+n;c--)delete v[c-1]}else if(n>i)for(c=y-i;c>w;c--)g=c+i-1,m=c+n-1,g in v?v[m]=v[g]:delete v[m];for(c=0;c0,expression:"total>0"}],attrs:{total:e.total,page:e.listQuery.page,limit:e.listQuery.limit},on:{"update:page":function(t){return e.$set(e.listQuery,"page",t)},"update:limit":function(t){return e.$set(e.listQuery,"limit",t)},pagination:e.getList}}),n("el-dialog",{attrs:{title:e.dialogMode.title,width:"36%",visible:e.dialogMode.visible,"close-on-click-modal":!1},on:{"update:visible":function(t){return e.$set(e.dialogMode,"visible",t)},close:e.handleCancel}},[n("el-form",{ref:"dialogForm",attrs:{model:e.dialogForm,rules:e.formRules}},[n("el-form-item",{attrs:{label:"客户端名称",prop:"clientName","label-width":e.dialogFormLabelWidth}},[n("el-input",{attrs:{placeholder:"客户端名称,必填,建议使用邮箱"},model:{value:e.dialogForm.clientName,callback:function(t){e.$set(e.dialogForm,"clientName",t)},expression:"dialogForm.clientName"}})],1),n("el-form-item",{attrs:{label:"客户端秘钥",prop:"accessKey","label-width":e.dialogFormLabelWidth}},[n("el-input",{attrs:{placeholder:"客户端秘钥,可选,默认系统会自动生成"},model:{value:e.dialogForm.accessKey,callback:function(t){e.$set(e.dialogForm,"accessKey",t)},expression:"dialogForm.accessKey"}})],1)],1),n("div",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[n("el-button",{attrs:{disabled:e.dialogMode.loading},on:{click:e.handleCancel}},[e._v("取消")]),n("el-button",{attrs:{type:"primary",loading:e.dialogMode.loading},on:{click:e.handleSave}},[e._v("确定")])],1)],1)],1)},s=[],i=(n("d3b7"),n("6724")),l=n("61f7"),o=n("b39f"),c=n("333d"),r=n("c1df"),d=n.n(r),u={components:{Pagination:c["a"]},directives:{waves:i["a"]},data:function(){var e=this,t=function(t,n,a){if(n.length<2||n.length>24)return a(new Error("长度在 2 到 24 个字符"));Object(l["b"])(e.dialogForm.clientId,n,1).then((function(e){return e?a(new Error("名称["+n+"]已被占用")):a()}))},n=function(t,n,a){var s=n.length;return 0===s?a():s<16||s>32?a(new Error("长度在 16 到 32 个字符")):void Object(l["b"])(e.dialogForm.clientId,n,2).then((function(e){return e?a(new Error("此秘钥已被使用")):a()}))};return{listLoading:!0,total:1,listQuery:{page:1,limit:10,wd:void 0},list:[{clientId:0,accessKey:"",enabled:0,usePortNum:0,joinTime:"0",clientName:"",state:0,apply:0,modified:"0"}],dialogFormLabelWidth:"120px",dialogMode:{option:"add",title:"",visible:!1,loading:!1},dialogForm:{clientId:0,clientName:"",accessKey:""},formRules:{clientName:[{required:!0,trigger:"blur",validator:t}],accessKey:[{required:!1,trigger:"blur",validator:n}]}}},created:function(){this.getList()},methods:{handleFilter:function(){this.getList()},getList:function(){var e=this;this.listLoading=!0,Object(o["c"])(this.listQuery).then((function(t){e.list=t.data.items,e.total=t.data.total})).catch((function(e){console.log(e)})).finally((function(){e.listLoading=!1}))},handleOpen:function(e){var t=this;e<=0?(t.dialogMode.option="add",t.dialogMode.title="新增客户端",t.dialogMode.visible=!0):(t.dialogMode.option="edit",Object(o["d"])({clientId:e}).then((function(e){t.dialogForm=e.data.item,t.dialogMode.title="编辑 · "+t.dialogForm.clientName,t.dialogMode.visible=!0})).catch((function(e){t.$message.error(e)})))},handleSave:function(){var e=this;this.$refs.dialogForm.validate((function(t){t&&(e.dialogMode.loading=!0,Object(o["e"])(e.dialogForm).then((function(t){e.$notify({type:"success",title:"完成",message:t.msg}),e.handleCancel(),e.getList()})).catch((function(t){e.$notify.error({title:"错误",message:t}),console.log(t)})).finally((function(){e.dialogMode.loading=!1})))}))},handleCancel:function(){this.dialogForm={clientId:0,clientName:"",accessKey:""},this.dialogMode.visible=!1,this.$refs.dialogForm.resetFields()},handSwitch:function(e){var t=this,n=e["enabled"],a=1===n?"停用":"启用";this.$confirm("此操作将"+a+"该客户端, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){n=1===n?0:1,Object(o["f"])({accessKey:e["accessKey"],clientId:e["clientId"],enabled:n}).then((function(){t.$message({type:"success",message:"处理成功!"}),e["enabled"]=n}))})).catch((function(){t.$message({type:"info",message:"操作已取消"})}))},handleDelete:function(e){var t=this;this.$confirm("此操作将删除该端口映射, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){Object(o["a"])({accessKey:e["accessKey"],clientId:e["clientId"]}).then((function(){t.$message({type:"success",message:"处理成功!"}),t.getList()}))})).catch((function(){t.$message({type:"info",message:"操作已取消"})}))},dateFormat:function(e){return void 0===e?"":d()(new Date(e)).format("YYYY-MM-DD HH:mm:ss")},showSate:function(e){var t=e["state"];return 0===t&&(t='离线'),1===t&&(t='在线'),t}}},f=u,m=(n("9f23"),n("2877")),b=Object(m["a"])(f,a,s,!1,null,"0d55bb74",null);t["default"]=b.exports},"2cbf":function(e,t,n){"use strict";n("73e0")},"333d":function(e,t,n){"use strict";var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"pagination-container",class:{hidden:e.hidden}},[n("el-pagination",e._b({attrs:{background:e.background,"current-page":e.currentPage,"page-size":e.pageSize,layout:e.layout,"page-sizes":e.pageSizes,total:e.total},on:{"update:currentPage":function(t){e.currentPage=t},"update:current-page":function(t){e.currentPage=t},"update:pageSize":function(t){e.pageSize=t},"update:page-size":function(t){e.pageSize=t},"size-change":e.handleSizeChange,"current-change":e.handleCurrentChange}},"el-pagination",e.$attrs,!1))],1)},s=[];n("a9e3");Math.easeInOutQuad=function(e,t,n,a){return e/=a/2,e<1?n/2*e*e+t:(e--,-n/2*(e*(e-2)-1)+t)};var i=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}();function l(e){document.documentElement.scrollTop=e,document.body.parentNode.scrollTop=e,document.body.scrollTop=e}function o(){return document.documentElement.scrollTop||document.body.parentNode.scrollTop||document.body.scrollTop}function c(e,t,n){var a=o(),s=e-a,c=20,r=0;t="undefined"===typeof t?500:t;var d=function e(){r+=c;var o=Math.easeInOutQuad(r,a,s,t);l(o),rNATOK·ADMIN
--------------------------------------------------------------------------------