mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 12:48:14 -05:00
✨ 内置HTTP内网穿透
This commit is contained in:
parent
b63f693661
commit
67b788a969
@ -260,8 +260,8 @@ func (cp *commonPage) home(c *gin.Context) {
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
ReadBufferSize: 10240,
|
||||
WriteBufferSize: 10240,
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
@ -305,8 +305,8 @@ func (cp *commonPage) ws(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (cp *commonPage) terminal(c *gin.Context) {
|
||||
terminalID := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(terminalID); err != nil {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
@ -316,7 +316,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(terminalID)
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, 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
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(terminalID, time.Second*10)
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
||||
type createTerminalRequest struct {
|
||||
@ -380,7 +380,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.GenerateUUID()
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
@ -394,7 +394,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(id)
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[createTerminalReq.ID]
|
||||
@ -411,7 +411,7 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
||||
}
|
||||
|
||||
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
||||
StreamID: id,
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
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{
|
||||
"SessionID": id,
|
||||
"SessionID": streamId,
|
||||
"ServerName": server.Name,
|
||||
}))
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
@ -14,16 +15,26 @@ import (
|
||||
"code.cloudfoundry.org/bytefmt"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/naiba/nezha/model"
|
||||
"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/service/rpc"
|
||||
"github.com/naiba/nezha/service/singleton"
|
||||
)
|
||||
|
||||
func ServeWeb(port uint) *http.Server {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
if singleton.Conf.Debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
pprof.Register(r)
|
||||
}
|
||||
r.Use(natGateway)
|
||||
tmpl := template.New("").Funcs(funcMap)
|
||||
var err error
|
||||
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
||||
@ -32,10 +43,6 @@ func ServeWeb(port uint) *http.Server {
|
||||
}
|
||||
tmpl = loadThirdPartyTemplates(tmpl)
|
||||
r.SetHTMLTemplate(tmpl)
|
||||
if singleton.Conf.Debug {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
pprof.Register(r)
|
||||
}
|
||||
r.Use(mygin.RecordPath)
|
||||
staticFs, err := fs.Sub(resource.StaticFS, "static")
|
||||
if err != nil {
|
||||
@ -44,7 +51,6 @@ func ServeWeb(port uint) *http.Server {
|
||||
r.StaticFS("/static", http.FS(staticFs))
|
||||
r.Static("/static-custom", "resource/static/custom")
|
||||
routers(r)
|
||||
|
||||
page404 := func(c *gin.Context) {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusNotFound,
|
||||
@ -238,3 +244,64 @@ var funcMap = template.FuncMap{
|
||||
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()
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ func (ma *memberAPI) serve() {
|
||||
mr.POST("/batch-update-server-group", ma.batchUpdateServerGroup)
|
||||
mr.POST("/batch-delete-server", ma.batchDeleteServer)
|
||||
mr.POST("/notification", ma.addOrEditNotification)
|
||||
mr.POST("/nat", ma.addOrEditNAT)
|
||||
mr.POST("/alert-rule", ma.addOrEditAlertRule)
|
||||
mr.POST("/setting", ma.updateSetting)
|
||||
mr.DELETE("/:model/:id", ma.delete)
|
||||
@ -209,6 +210,11 @@ func (ma *memberAPI) delete(c *gin.Context) {
|
||||
if err == nil {
|
||||
singleton.OnDeleteNotification(id)
|
||||
}
|
||||
case "nat":
|
||||
err = singleton.DB.Unscoped().Delete(&model.NAT{}, "id = ?", id).Error
|
||||
if err == nil {
|
||||
singleton.OnNATUpdate()
|
||||
}
|
||||
case "monitor":
|
||||
err = singleton.DB.Unscoped().Delete(&model.Monitor{}, "id = ?", id).Error
|
||||
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 {
|
||||
ID uint64
|
||||
Name string
|
||||
|
@ -27,6 +27,7 @@ func (mp *memberPage) serve() {
|
||||
mr.GET("/monitor", mp.monitor)
|
||||
mr.GET("/cron", mp.cron)
|
||||
mr.GET("/notification", mp.notification)
|
||||
mr.GET("/nat", mp.nat)
|
||||
mr.GET("/setting", mp.setting)
|
||||
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) {
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
|
||||
|
@ -21,12 +21,18 @@ const (
|
||||
TaskTypeUpgrade
|
||||
TaskTypeKeepalive
|
||||
TaskTypeTerminalGRPC
|
||||
TaskTypeNAT
|
||||
)
|
||||
|
||||
type TerminalTask struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
type TaskNAT struct {
|
||||
StreamID string
|
||||
Host string
|
||||
}
|
||||
|
||||
const (
|
||||
MonitorCoverAll = iota
|
||||
MonitorCoverIgnoreAll
|
||||
|
9
model/nat.go
Normal file
9
model/nat.go
Normal file
@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type NAT struct {
|
||||
Common
|
||||
Name string
|
||||
ServerID uint64
|
||||
Host string
|
||||
Domain string `gorm:"unique"`
|
||||
}
|
@ -16,6 +16,7 @@ var adminPage = map[string]bool{
|
||||
"/monitor": true,
|
||||
"/setting": true,
|
||||
"/notification": true,
|
||||
"/nat": true,
|
||||
"/cron": true,
|
||||
"/api": true,
|
||||
}
|
||||
|
56
pkg/utils/request_wrapper.go
Normal file
56
pkg/utils/request_wrapper.go
Normal 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
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package websocketx
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var _ io.ReadWriteCloser = &Conn{}
|
||||
|
||||
type Conn struct {
|
||||
*websocket.Conn
|
||||
writeLock *sync.Mutex
|
||||
|
3
resource/l10n/en-US.toml
vendored
3
resource/l10n/en-US.toml
vendored
@ -648,3 +648,6 @@ other = "Disable Switch Template in Frontend"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "Servers On World Map"
|
||||
|
||||
[NAT]
|
||||
other = "NAT"
|
5
resource/l10n/es-ES.toml
vendored
5
resource/l10n/es-ES.toml
vendored
@ -647,4 +647,7 @@ other = "Temperatura"
|
||||
other = "Deshabilitar Cambio de Plantilla en Frontend"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "Servidores en el mapa mundial"
|
||||
other = "Servidores en el mapa mundial"
|
||||
|
||||
[NAT]
|
||||
other = "NAT"
|
3
resource/l10n/zh-CN.toml
vendored
3
resource/l10n/zh-CN.toml
vendored
@ -648,3 +648,6 @@ other = "禁止前台切换模板"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "服务器世界分布图"
|
||||
|
||||
[NAT]
|
||||
other = "内网穿透"
|
3
resource/l10n/zh-TW.toml
vendored
3
resource/l10n/zh-TW.toml
vendored
@ -648,3 +648,6 @@ other = "禁止前台切換主題"
|
||||
|
||||
[ServersOnWorldMap]
|
||||
other = "伺服器世界分布圖"
|
||||
|
||||
[NAT]
|
||||
other = "NAT"
|
@ -91,6 +91,7 @@ function showFormModal(modelSelector, formID, URL, getData) {
|
||||
item.name.endsWith("_id") ||
|
||||
item.name === "id" ||
|
||||
item.name === "ID" ||
|
||||
item.name === "ServerID" ||
|
||||
item.name === "RequestType" ||
|
||||
item.name === "RequestMethod" ||
|
||||
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) {
|
||||
post('/terminal', { Host: window.location.host, Protocol: window.location.protocol, ID: id })
|
||||
}
|
||||
|
2
resource/template/common/footer.html
vendored
2
resource/template/common/footer.html
vendored
@ -10,7 +10,7 @@
|
||||
<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="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>
|
||||
(function () {
|
||||
updateLang({{.LANG }});
|
||||
|
1
resource/template/common/menu.html
vendored
1
resource/template/common/menu.html
vendored
@ -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 "/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 "/nat"}} active{{end}}' href="/nat"><i class="exchange icon"></i>{{tr "NAT"}}</a>
|
||||
<a class='item{{if eq .MatchedPath "/setting"}} active{{end}}' href="/setting">
|
||||
<i class="settings icon"></i>{{tr "Settings"}}
|
||||
</a>
|
||||
|
31
resource/template/component/nat.html
vendored
Normal file
31
resource/template/component/nat.html
vendored
Normal 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}}
|
54
resource/template/dashboard-default/nat.html
vendored
Normal file
54
resource/template/dashboard-default/nat.html
vendored
Normal 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}}
|
@ -136,6 +136,5 @@ LOOP:
|
||||
}()
|
||||
|
||||
<-endCh
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -15,8 +15,6 @@ var (
|
||||
|
||||
ServerAPI = &ServerAPIService{}
|
||||
MonitorAPI = &MonitorAPIService{}
|
||||
|
||||
once = &sync.Once{}
|
||||
)
|
||||
|
||||
type ServerAPIService struct{}
|
||||
@ -78,7 +76,7 @@ func InitAPI() {
|
||||
UserIDToApiTokenList = make(map[uint64][]string)
|
||||
}
|
||||
|
||||
func LoadAPI() {
|
||||
func loadAPI() {
|
||||
InitAPI()
|
||||
var tokenList []*model.ApiToken
|
||||
DB.Find(&tokenList)
|
||||
|
@ -24,8 +24,8 @@ func InitCronTask() {
|
||||
Crons = make(map[uint64]*model.Cron)
|
||||
}
|
||||
|
||||
// LoadCronTasks 加载计划任务
|
||||
func LoadCronTasks() {
|
||||
// loadCronTasks 加载计划任务
|
||||
func loadCronTasks() {
|
||||
InitCronTask()
|
||||
var crons []model.Cron
|
||||
DB.Find(&crons)
|
||||
|
31
service/singleton/nat.go
Normal file
31
service/singleton/nat.go
Normal 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]
|
||||
}
|
@ -24,8 +24,8 @@ func InitNotification() {
|
||||
NotificationIDToTag = make(map[uint64]string)
|
||||
}
|
||||
|
||||
// LoadNotifications 从 DB 初始化通知方式相关参数
|
||||
func LoadNotifications() {
|
||||
// loadNotifications 从 DB 初始化通知方式相关参数
|
||||
func loadNotifications() {
|
||||
InitNotification()
|
||||
notificationsLock.Lock()
|
||||
defer notificationsLock.Unlock()
|
||||
|
@ -25,8 +25,8 @@ func InitServer() {
|
||||
ServerTagToIDList = make(map[string][]uint64)
|
||||
}
|
||||
|
||||
// LoadServers 加载服务器列表并根据ID排序
|
||||
func LoadServers() {
|
||||
// loadServers 加载服务器列表并根据ID排序
|
||||
func loadServers() {
|
||||
InitServer()
|
||||
var servers []model.Server
|
||||
DB.Find(&servers)
|
||||
|
@ -34,10 +34,11 @@ func InitTimezoneAndCache() {
|
||||
|
||||
// LoadSingleton 加载子服务并执行
|
||||
func LoadSingleton() {
|
||||
LoadNotifications() // 加载通知服务
|
||||
LoadServers() // 加载服务器列表
|
||||
LoadCronTasks() // 加载定时任务
|
||||
LoadAPI()
|
||||
loadNotifications() // 加载通知服务
|
||||
loadServers() // 加载服务器列表
|
||||
loadCronTasks() // 加载定时任务
|
||||
loadAPI()
|
||||
initNAT()
|
||||
}
|
||||
|
||||
// InitConfigFromPath 从给出的文件路径中加载配置
|
||||
@ -47,11 +48,11 @@ func InitConfigFromPath(path string) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ValidateConfig()
|
||||
validateConfig()
|
||||
}
|
||||
|
||||
// ValidateConfig 验证配置文件有效性
|
||||
func ValidateConfig() {
|
||||
// validateConfig 验证配置文件有效性
|
||||
func validateConfig() {
|
||||
var err error
|
||||
if Conf.DDNS.Provider == "" {
|
||||
err = ValidateDDNSProvidersFromProfiles()
|
||||
@ -82,7 +83,8 @@ func InitDBFromPath(path string) {
|
||||
}
|
||||
err = DB.AutoMigrate(model.Server{}, model.User{},
|
||||
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 {
|
||||
panic(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user