Skip to content

Commit

Permalink
✨ 内置HTTP内网穿透
Browse files Browse the repository at this point in the history
  • Loading branch information
naiba committed Jul 14, 2024
1 parent b63f693 commit 67b788a
Show file tree
Hide file tree
Showing 25 changed files with 384 additions and 36 deletions.
22 changes: 11 additions & 11 deletions cmd/dashboard/controller/common_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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: "无权访问",
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -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,
}))
}
77 changes: 72 additions & 5 deletions cmd/dashboard/controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 changes: 45 additions & 0 deletions cmd/dashboard/controller/member_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions cmd/dashboard/controller/member_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"}),
Expand Down
6 changes: 6 additions & 0 deletions model/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions model/nat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package model

type NAT struct {
Common
Name string
ServerID uint64
Host string
Domain string `gorm:"unique"`
}
1 change: 1 addition & 0 deletions pkg/mygin/mygin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var adminPage = map[string]bool{
"/monitor": true,
"/setting": true,
"/notification": true,
"/nat": true,
"/cron": true,
"/api": true,
}
Expand Down
56 changes: 56 additions & 0 deletions pkg/utils/request_wrapper.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions pkg/websocketx/safe_conn.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit 67b788a

Please sign in to comment.