内置HTTP内网穿透

This commit is contained in:
naiba 2024-07-14 19:41:50 +08:00
parent b63f693661
commit 67b788a969
25 changed files with 384 additions and 36 deletions

View File

@ -260,8 +260,8 @@ func (cp *commonPage) home(c *gin.Context) {
} }
var upgrader = websocket.Upgrader{ var upgrader = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 10240,
WriteBufferSize: 1024, WriteBufferSize: 10240,
} }
type Data struct { type Data struct {
@ -305,8 +305,8 @@ func (cp *commonPage) ws(c *gin.Context) {
} }
func (cp *commonPage) terminal(c *gin.Context) { func (cp *commonPage) terminal(c *gin.Context) {
terminalID := c.Param("id") streamId := c.Param("id")
if _, err := rpc.NezhaHandlerSingleton.GetStream(terminalID); err != nil { if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusForbidden, Code: http.StatusForbidden,
Title: "无权访问", Title: "无权访问",
@ -316,7 +316,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
}, true) }, true)
return return
} }
defer rpc.NezhaHandlerSingleton.CloseStream(terminalID) defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil) wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
@ -344,11 +344,11 @@ func (cp *commonPage) terminal(c *gin.Context) {
} }
}() }()
if err = rpc.NezhaHandlerSingleton.UserConnected(terminalID, conn); err != nil { if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
return return
} }
rpc.NezhaHandlerSingleton.StartStream(terminalID, time.Second*10) rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
} }
type createTerminalRequest struct { type createTerminalRequest struct {
@ -380,7 +380,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return return
} }
id, err := uuid.GenerateUUID() streamId, err := uuid.GenerateUUID()
if err != nil { if err != nil {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusInternalServerError, Code: http.StatusInternalServerError,
@ -394,7 +394,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
return return
} }
rpc.NezhaHandlerSingleton.CreateStream(id) rpc.NezhaHandlerSingleton.CreateStream(streamId)
singleton.ServerLock.RLock() singleton.ServerLock.RLock()
server := singleton.ServerList[createTerminalReq.ID] server := singleton.ServerList[createTerminalReq.ID]
@ -411,7 +411,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
} }
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{ terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
StreamID: id, StreamID: streamId,
}) })
if err := server.TaskStream.Send(&proto.Task{ if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeTerminalGRPC, Type: model.TaskTypeTerminalGRPC,
@ -428,7 +428,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
} }
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{ c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
"SessionID": id, "SessionID": streamId,
"ServerName": server.Name, "ServerName": server.Name,
})) }))
} }

View File

@ -1,6 +1,7 @@
package controller package controller
import ( import (
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io/fs" "io/fs"
@ -14,16 +15,26 @@ import (
"code.cloudfoundry.org/bytefmt" "code.cloudfoundry.org/bytefmt"
"github.com/gin-contrib/pprof" "github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hashicorp/go-uuid"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/naiba/nezha/model"
"github.com/naiba/nezha/pkg/mygin" "github.com/naiba/nezha/pkg/mygin"
"github.com/naiba/nezha/pkg/utils"
"github.com/naiba/nezha/proto"
"github.com/naiba/nezha/resource" "github.com/naiba/nezha/resource"
"github.com/naiba/nezha/service/rpc"
"github.com/naiba/nezha/service/singleton" "github.com/naiba/nezha/service/singleton"
) )
func ServeWeb(port uint) *http.Server { func ServeWeb(port uint) *http.Server {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
r := gin.Default() r := gin.Default()
if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode)
pprof.Register(r)
}
r.Use(natGateway)
tmpl := template.New("").Funcs(funcMap) tmpl := template.New("").Funcs(funcMap)
var err error var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html") tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
@ -32,10 +43,6 @@ func ServeWeb(port uint) *http.Server {
} }
tmpl = loadThirdPartyTemplates(tmpl) tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl) r.SetHTMLTemplate(tmpl)
if singleton.Conf.Debug {
gin.SetMode(gin.DebugMode)
pprof.Register(r)
}
r.Use(mygin.RecordPath) r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static") staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil { if err != nil {
@ -44,7 +51,6 @@ func ServeWeb(port uint) *http.Server {
r.StaticFS("/static", http.FS(staticFs)) r.StaticFS("/static", http.FS(staticFs))
r.Static("/static-custom", "resource/static/custom") r.Static("/static-custom", "resource/static/custom")
routers(r) routers(r)
page404 := func(c *gin.Context) { page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{ mygin.ShowErrorPage(c, mygin.ErrInfo{
Code: http.StatusNotFound, Code: http.StatusNotFound,
@ -238,3 +244,64 @@ var funcMap = template.FuncMap{
return singleton.StatusCodeToString(singleton.GetStatusCode(val)) return singleton.StatusCodeToString(singleton.GetStatusCode(val))
}, },
} }
func natGateway(c *gin.Context) {
natConfig := singleton.GetNATConfigByDomain(c.Request.Host)
if natConfig == nil {
return
}
singleton.ServerLock.RLock()
server := singleton.ServerList[natConfig.ServerID]
singleton.ServerLock.RUnlock()
if server == nil || server.TaskStream == nil {
c.Writer.WriteString("server not found or not connected")
c.Abort()
return
}
streamId, err := uuid.GenerateUUID()
if err != nil {
c.Writer.WriteString(fmt.Sprintf("stream id error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.CreateStream(streamId)
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
taskData, err := json.Marshal(model.TaskNAT{
StreamID: streamId,
Host: natConfig.Host,
})
if err != nil {
c.Writer.WriteString(fmt.Sprintf("task data error: %v", err))
c.Abort()
return
}
if err := server.TaskStream.Send(&proto.Task{
Type: model.TaskTypeNAT,
Data: string(taskData),
}); err != nil {
c.Writer.WriteString(fmt.Sprintf("send task error: %v", err))
c.Abort()
return
}
w, err := utils.NewRequestWrapper(c.Request, c.Writer)
if err != nil {
c.Writer.WriteString(fmt.Sprintf("request wrapper error: %v", err))
c.Abort()
return
}
if err := rpc.NezhaHandlerSingleton.UserConnected(streamId, w); err != nil {
c.Writer.WriteString(fmt.Sprintf("user connected error: %v", err))
c.Abort()
return
}
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
c.Abort()
}

View File

@ -45,6 +45,7 @@ func (ma *memberAPI) serve() {
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup) mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
mr.POST("/batch-delete-server", ma.batchDeleteServer) mr.POST("/batch-delete-server", ma.batchDeleteServer)
mr.POST("/notification", ma.addOrEditNotification) mr.POST("/notification", ma.addOrEditNotification)
mr.POST("/nat", ma.addOrEditNAT)
mr.POST("/alert-rule", ma.addOrEditAlertRule) mr.POST("/alert-rule", ma.addOrEditAlertRule)
mr.POST("/setting", ma.updateSetting) mr.POST("/setting", ma.updateSetting)
mr.DELETE("/:model/:id", ma.delete) mr.DELETE("/:model/:id", ma.delete)
@ -209,6 +210,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
if err == nil { if err == nil {
singleton.OnDeleteNotification(id) singleton.OnDeleteNotification(id)
} }
case "nat":
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
if err == nil {
singleton.OnNATUpdate()
}
case "monitor": case "monitor":
err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error
if err == nil { if err == nil {
@ -733,6 +739,45 @@ func (ma *memberAPI) addOrEditNotification(c *gin.Context) {
}) })
} }
type natForm struct {
ID uint64
Name string
ServerID uint64
Host string
Domain string
}
func (ma *memberAPI) addOrEditNAT(c *gin.Context) {
var nf natForm
var n model.NAT
err := c.ShouldBindJSON(&nf)
if err == nil {
n.Name = nf.Name
n.ID = nf.ID
n.Domain = nf.Domain
n.Host = nf.Host
n.ServerID = nf.ServerID
}
if err == nil {
if n.ID == 0 {
err = singleton.DB.Create(&n).Error
} else {
err = singleton.DB.Save(&n).Error
}
}
if err != nil {
c.JSON(http.StatusOK, model.Response{
Code: http.StatusBadRequest,
Message: fmt.Sprintf("请求错误:%s", err),
})
return
}
singleton.OnNATUpdate()
c.JSON(http.StatusOK, model.Response{
Code: http.StatusOK,
})
}
type alertRuleForm struct { type alertRuleForm struct {
ID uint64 ID uint64
Name string Name string

View File

@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
mr.GET("/monitor", mp.monitor) mr.GET("/monitor", mp.monitor)
mr.GET("/cron", mp.cron) mr.GET("/cron", mp.cron)
mr.GET("/notification", mp.notification) mr.GET("/notification", mp.notification)
mr.GET("/nat", mp.nat)
mr.GET("/setting", mp.setting) mr.GET("/setting", mp.setting)
mr.GET("/api", mp.api) mr.GET("/api", mp.api)
} }
@ -77,6 +78,15 @@ func (mp *memberPage) notification(c *gin.Context) {
})) }))
} }
func (mp *memberPage) nat(c *gin.Context) {
var data []model.NAT
singleton.DB.Find(&data)
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
"NAT": data,
}))
}
func (mp *memberPage) setting(c *gin.Context) { func (mp *memberPage) setting(c *gin.Context) {
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{ c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}), "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),

View File

@ -21,12 +21,18 @@ const (
TaskTypeUpgrade TaskTypeUpgrade
TaskTypeKeepalive TaskTypeKeepalive
TaskTypeTerminalGRPC TaskTypeTerminalGRPC
TaskTypeNAT
) )
type TerminalTask struct { type TerminalTask struct {
StreamID string StreamID string
} }
type TaskNAT struct {
StreamID string
Host string
}
const ( const (
MonitorCoverAll = iota MonitorCoverAll = iota
MonitorCoverIgnoreAll MonitorCoverIgnoreAll

9
model/nat.go Normal file
View File

@ -0,0 +1,9 @@
package model
type NAT struct {
Common
Name string
ServerID uint64
Host string
Domain string `gorm:"unique"`
}

View File

@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true, "/monitor": true,
"/setting": true, "/setting": true,
"/notification": true, "/notification": true,
"/nat": true,
"/cron": true, "/cron": true,
"/api": true, "/api": true,
} }

View File

@ -0,0 +1,56 @@
package utils
import (
"bytes"
"io"
"net"
"net/http"
"github.com/gin-gonic/gin"
)
var _ io.ReadWriteCloser = &RequestWrapper{}
type RequestWrapper struct {
req *http.Request
reader *bytes.Buffer
writer net.Conn
}
func NewRequestWrapper(req *http.Request, writer gin.ResponseWriter) (*RequestWrapper, error) {
conn, _, err := writer.Hijack()
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(nil)
if err = req.Write(buf); err != nil {
return nil, err
}
return &RequestWrapper{
req: req,
reader: buf,
writer: conn,
}, nil
}
func (rw *RequestWrapper) Read(p []byte) (int, error) {
count, err := rw.reader.Read(p)
if err == nil {
return count, nil
}
if err != io.EOF {
return count, err
}
// request 数据读完之后等待客户端断开连接或 grpc 超时
return rw.writer.Read(p)
}
func (rw *RequestWrapper) Write(p []byte) (int, error) {
return rw.writer.Write(p)
}
func (rw *RequestWrapper) Close() error {
rw.req.Body.Close()
rw.writer.Close()
return nil
}

View File

@ -1,11 +1,14 @@
package websocketx package websocketx
import ( import (
"io"
"sync" "sync"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
var _ io.ReadWriteCloser = &Conn{}
type Conn struct { type Conn struct {
*websocket.Conn *websocket.Conn
writeLock *sync.Mutex writeLock *sync.Mutex

View File

@ -648,3 +648,6 @@ other = "Disable Switch Template in Frontend"
[ServersOnWorldMap] [ServersOnWorldMap]
other = "Servers On World Map" other = "Servers On World Map"
[NAT]
other = "NAT"

View File

@ -647,4 +647,7 @@ other = "Temperatura"
other = "Deshabilitar Cambio de Plantilla en Frontend" other = "Deshabilitar Cambio de Plantilla en Frontend"
[ServersOnWorldMap] [ServersOnWorldMap]
other = "Servidores en el mapa mundial" other = "Servidores en el mapa mundial"
[NAT]
other = "NAT"

View File

@ -648,3 +648,6 @@ other = "禁止前台切换模板"
[ServersOnWorldMap] [ServersOnWorldMap]
other = "服务器世界分布图" other = "服务器世界分布图"
[NAT]
other = "内网穿透"

View File

@ -648,3 +648,6 @@ other = "禁止前台切換主題"
[ServersOnWorldMap] [ServersOnWorldMap]
other = "伺服器世界分布圖" other = "伺服器世界分布圖"
[NAT]
other = "NAT"

View File

@ -91,6 +91,7 @@ function showFormModal(modelSelector, formID, URL, getData) {
item.name.endsWith("_id") || item.name.endsWith("_id") ||
item.name === "id" || item.name === "id" ||
item.name === "ID" || item.name === "ID" ||
item.name === "ServerID" ||
item.name === "RequestType" || item.name === "RequestType" ||
item.name === "RequestMethod" || item.name === "RequestMethod" ||
item.name === "TriggerMode" || item.name === "TriggerMode" ||
@ -255,6 +256,28 @@ function addOrEditNotification(notification) {
); );
} }
function addOrEditNAT(nat) {
const modal = $(".nat.modal");
modal.children(".header").text((nat ? LANG.Edit : LANG.Add));
modal
.find(".nezha-primary-btn.button")
.html(
nat
? LANG.Edit + '<i class="edit icon"></i>'
: LANG.Add + '<i class="add icon"></i>'
);
modal.find("input[name=ID]").val(nat ? nat.ID : null);
modal.find("input[name=ServerID]").val(nat ? nat.ServerID : null);
modal.find("input[name=Name]").val(nat ? nat.Name : null);
modal.find("input[name=Host]").val(nat ? nat.Host : null);
modal.find("input[name=Domain]").val(nat ? nat.Domain : null);
showFormModal(
".nat.modal",
"#natForm",
"/api/nat"
);
}
function connectToServer(id) { function connectToServer(id) {
post('/terminal', { Host: window.location.host, Protocol: window.location.protocol, ID: id }) post('/terminal', { Host: window.location.host, Protocol: window.location.protocol, ID: id })
} }

View File

@ -10,7 +10,7 @@
<script src="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.js"></script> <script src="https://unpkg.com/semantic-ui@2.4.0/dist/semantic.min.js"></script>
<script src="/static/semantic-ui-alerts.min.js"></script> <script src="/static/semantic-ui-alerts.min.js"></script>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script> <script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<script src="/static/main.js?v20240330"></script> <script src="/static/main.js?v20240714"></script>
<script> <script>
(function () { (function () {
updateLang({{.LANG }}); updateLang({{.LANG }});

View File

@ -9,6 +9,7 @@
<a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a> <a class='item{{if eq .MatchedPath "/monitor"}} active{{end}}' href="/monitor"><i class="rss icon"></i>{{tr "Services"}}</a>
<a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a> <a class='item{{if eq .MatchedPath "/cron"}} active{{end}}' href="/cron"><i class="clock icon"></i>{{tr "Task"}}</a>
<a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a> <a class='item{{if eq .MatchedPath "/notification"}} active{{end}}' href="/notification"><i class="bell icon"></i>{{tr "Notification"}}</a>
<a class='item{{if eq .MatchedPath "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting"> <a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
<i class="settings icon"></i>{{tr "Settings"}} <i class="settings icon"></i>{{tr "Settings"}}
</a> </a>

31
resource/template/component/nat.html vendored Normal file
View File

@ -0,0 +1,31 @@
{{define "component/nat"}}
<div class="ui tiny nat modal transition hidden">
<div class="header">Add</div>
<div class="content">
<form id="natForm" class="ui form">
<input type="hidden" name="ID">
<div class="field">
<label>{{tr "Name"}}</label>
<input type="text" name="Name">
</div>
<div class="field">
<label>Agent ID</label>
<input type="number" name="ServerID" placeholder="1">
</div>
<div class="field">
<label>内网服务</label>
<input type="text" name="Host" placeholder="192.168.1.1:80(带端口)">
</div>
<div class="field">
<label>绑定域名</label>
<input type="text" name="Domain" placeholder="router.app.yourdomain.com">
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">{{tr "Cancel"}}</div>
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
</button>
</div>
</div>
{{end}}

View File

@ -0,0 +1,54 @@
{{define "dashboard-default/nat"}}
{{template "common/header" .}}
{{template "common/menu" .}}
<div class="nb-container">
<div class="ui container">
<div class="ui grid">
<div class="right floated right aligned twelve wide column">
<button class="ui right labeled nezha-primary-btn icon button" onclick="addOrEditNAT()"><i
class="add icon"></i> Add
</button>
</div>
</div>
<table class="ui very basic table">
<thead>
<tr>
<th>ID</th>
<th>{{tr "Name"}}</th>
<th>Agent ID</th>
<th>内网服务</th>
<th>绑定域名</th>
<th>{{tr "Administration"}}</th>
</tr>
</thead>
<tbody>
{{range $item := .NAT}}
<tr>
<td>{{$item.ID}}</td>
<td>{{$item.Name}}</td>
<td>{{$item.ServerID}}</td>
<td>{{$item.Host}}</td>
<td>{{$item.Domain}}</td>
<td>
<div class="ui mini icon buttons">
<button class="ui button" onclick="addOrEditNAT({{$item}})">
<i class="edit icon"></i>
</button>
<button class="ui button"
onclick="showConfirm('确定删除NAT隧道','确认删除',deleteRequest,'/api/nat/'+{{$item.ID}})">
<i class="trash alternate outline icon"></i>
</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "component/nat"}}
{{template "common/footer" .}}
<script>
$('.checkbox').checkbox()
</script>
{{end}}

View File

@ -136,6 +136,5 @@ LOOP:
}() }()
<-endCh <-endCh
return err return err
} }

View File

@ -15,8 +15,6 @@ var (
ServerAPI = &ServerAPIService{} ServerAPI = &ServerAPIService{}
MonitorAPI = &MonitorAPIService{} MonitorAPI = &MonitorAPIService{}
once = &sync.Once{}
) )
type ServerAPIService struct{} type ServerAPIService struct{}
@ -78,7 +76,7 @@ func InitAPI() {
UserIDToApiTokenList = make(map[uint64][]string) UserIDToApiTokenList = make(map[uint64][]string)
} }
func LoadAPI() { func loadAPI() {
InitAPI() InitAPI()
var tokenList []*model.ApiToken var tokenList []*model.ApiToken
DB.Find(&tokenList) DB.Find(&tokenList)

View File

@ -24,8 +24,8 @@ func InitCronTask() {
Crons = make(map[uint64]*model.Cron) Crons = make(map[uint64]*model.Cron)
} }
// LoadCronTasks 加载计划任务 // loadCronTasks 加载计划任务
func LoadCronTasks() { func loadCronTasks() {
InitCronTask() InitCronTask()
var crons []model.Cron var crons []model.Cron
DB.Find(&crons) DB.Find(&crons)

31
service/singleton/nat.go Normal file
View File

@ -0,0 +1,31 @@
package singleton
import (
"sync"
"github.com/naiba/nezha/model"
)
var natCache = make(map[string]*model.NAT)
var natCacheRwLock = new(sync.RWMutex)
func initNAT() {
OnNATUpdate()
}
func OnNATUpdate() {
natCacheRwLock.Lock()
defer natCacheRwLock.Unlock()
var nats []*model.NAT
DB.Find(&nats)
natCache = make(map[string]*model.NAT)
for i := 0; i < len(nats); i++ {
natCache[nats[i].Domain] = nats[i]
}
}
func GetNATConfigByDomain(domain string) *model.NAT {
natCacheRwLock.RLock()
defer natCacheRwLock.RUnlock()
return natCache[domain]
}

View File

@ -24,8 +24,8 @@ func InitNotification() {
NotificationIDToTag = make(map[uint64]string) NotificationIDToTag = make(map[uint64]string)
} }
// LoadNotifications 从 DB 初始化通知方式相关参数 // loadNotifications 从 DB 初始化通知方式相关参数
func LoadNotifications() { func loadNotifications() {
InitNotification() InitNotification()
notificationsLock.Lock() notificationsLock.Lock()
defer notificationsLock.Unlock() defer notificationsLock.Unlock()

View File

@ -25,8 +25,8 @@ func InitServer() {
ServerTagToIDList = make(map[string][]uint64) ServerTagToIDList = make(map[string][]uint64)
} }
// LoadServers 加载服务器列表并根据ID排序 // loadServers 加载服务器列表并根据ID排序
func LoadServers() { func loadServers() {
InitServer() InitServer()
var servers []model.Server var servers []model.Server
DB.Find(&servers) DB.Find(&servers)

View File

@ -34,10 +34,11 @@ func InitTimezoneAndCache() {
// LoadSingleton 加载子服务并执行 // LoadSingleton 加载子服务并执行
func LoadSingleton() { func LoadSingleton() {
LoadNotifications() // 加载通知服务 loadNotifications() // 加载通知服务
LoadServers() // 加载服务器列表 loadServers() // 加载服务器列表
LoadCronTasks() // 加载定时任务 loadCronTasks() // 加载定时任务
LoadAPI() loadAPI()
initNAT()
} }
// InitConfigFromPath 从给出的文件路径中加载配置 // InitConfigFromPath 从给出的文件路径中加载配置
@ -47,11 +48,11 @@ func InitConfigFromPath(path string) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
ValidateConfig() validateConfig()
} }
// ValidateConfig 验证配置文件有效性 // validateConfig 验证配置文件有效性
func ValidateConfig() { func validateConfig() {
var err error var err error
if Conf.DDNS.Provider == "" { if Conf.DDNS.Provider == "" {
err = ValidateDDNSProvidersFromProfiles() err = ValidateDDNSProvidersFromProfiles()
@ -82,7 +83,8 @@ func InitDBFromPath(path string) {
} }
err = DB.AutoMigrate(model.Server{}, model.User{}, err = DB.AutoMigrate(model.Server{}, model.User{},
model.Notification{}, model.AlertRule{}, model.Monitor{}, model.Notification{}, model.AlertRule{}, model.Monitor{},
model.MonitorHistory{}, model.Cron{}, model.Transfer{}, model.ApiToken{}) model.MonitorHistory{}, model.Cron{}, model.Transfer{},
model.ApiToken{}, model.NAT{})
if err != nil { if err != nil {
panic(err) panic(err)
} }