├── .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 · ![GitHub Repo stars](https://img.shields.io/github/stars/natokay/go-natok-server) ![GitHub Repo stars](https://img.shields.io/github/stars/natokay/go-natok-cli) 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 | ![image-20250303220714-r1kbi0b](https://github.com/user-attachments/assets/49e963e1-0062-4e2b-89d2-8309472e9fe7) 147 | 148 | 统计概览 149 | ![image-20250303220743-etmceyf](https://github.com/user-attachments/assets/cba87be9-e6d0-4ab2-8fbe-222397c4a06a) 150 | 151 | 代理管理 152 | ![image-20250303220953-vz1hjpb](https://github.com/user-attachments/assets/bc42a243-c1fc-4fa3-adfd-23c6175f9166) 153 | ![image-20250303221323-a0q00lk](https://github.com/user-attachments/assets/ff38b0a3-d578-4342-a68c-98e4775c5021) 154 | 155 | 端口映射 156 | ![image-20250303221053-j7b3tsy](https://github.com/user-attachments/assets/4f65aea5-5f97-42dc-94a0-0e3af73d4bef) 157 | ![image-20250303221456-pkfl4wt](https://github.com/user-attachments/assets/3692fce0-6104-47ee-b2b5-fcafd78366ec) 158 | 159 | 标签名单 160 | ![image-20250303221123-zl9f76j](https://github.com/user-attachments/assets/02262934-f260-43da-8435-45fdd35c1793) 161 | ![image-20250303221545-9n2vwqs](https://github.com/user-attachments/assets/14ddd49a-fdcc-49d0-ae8e-071a9962ac4c) 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

--------------------------------------------------------------------------------