|
|
|
@@ -3,12 +3,8 @@ package controller
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
@@ -24,22 +20,13 @@ import (
|
|
|
|
|
"github.com/naiba/nezha/pkg/utils"
|
|
|
|
|
"github.com/naiba/nezha/pkg/websocketx"
|
|
|
|
|
"github.com/naiba/nezha/proto"
|
|
|
|
|
"github.com/naiba/nezha/service/rpc"
|
|
|
|
|
"github.com/naiba/nezha/service/singleton"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type terminalContext struct {
|
|
|
|
|
agentConn *websocketx.Conn
|
|
|
|
|
userConn *websocketx.Conn
|
|
|
|
|
serverID uint64
|
|
|
|
|
host string
|
|
|
|
|
useSSL bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type commonPage struct {
|
|
|
|
|
r *gin.Engine
|
|
|
|
|
terminals map[string]*terminalContext
|
|
|
|
|
terminalsLock *sync.Mutex
|
|
|
|
|
requestGroup singleflight.Group
|
|
|
|
|
r *gin.Engine
|
|
|
|
|
requestGroup singleflight.Group
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (cp *commonPage) serve() {
|
|
|
|
@@ -68,7 +55,6 @@ type viewPasswordForm struct {
|
|
|
|
|
func (p *commonPage) issueViewPassword(c *gin.Context) {
|
|
|
|
|
var vpf viewPasswordForm
|
|
|
|
|
err := c.ShouldBind(&vpf)
|
|
|
|
|
log.Println("bingo", vpf)
|
|
|
|
|
var hash []byte
|
|
|
|
|
if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
|
|
|
|
|
err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
|
|
|
|
@@ -283,8 +269,6 @@ type Data struct {
|
|
|
|
|
Servers []*model.Server `json:"servers,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cloudflareCookiesValidator = regexp.MustCompile("^[A-Za-z0-9-_]+$")
|
|
|
|
|
|
|
|
|
|
func (cp *commonPage) ws(c *gin.Context) {
|
|
|
|
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -322,9 +306,7 @@ func (cp *commonPage) ws(c *gin.Context) {
|
|
|
|
|
|
|
|
|
|
func (cp *commonPage) terminal(c *gin.Context) {
|
|
|
|
|
terminalID := c.Param("id")
|
|
|
|
|
cp.terminalsLock.Lock()
|
|
|
|
|
if terminalID == "" || cp.terminals[terminalID] == nil {
|
|
|
|
|
cp.terminalsLock.Unlock()
|
|
|
|
|
if _, err := rpc.NezhaHandlerSingleton.GetStream(terminalID); err != nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "无权访问",
|
|
|
|
@@ -334,104 +316,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
terminal := cp.terminals[terminalID]
|
|
|
|
|
cp.terminalsLock.Unlock()
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
// 清理 context
|
|
|
|
|
cp.terminalsLock.Lock()
|
|
|
|
|
defer cp.terminalsLock.Unlock()
|
|
|
|
|
delete(cp.terminals, terminalID)
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
var isAgent bool
|
|
|
|
|
|
|
|
|
|
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
|
|
|
|
singleton.ServerLock.RLock()
|
|
|
|
|
_, hasID := singleton.SecretToID[c.Request.Header.Get("Secret")]
|
|
|
|
|
singleton.ServerLock.RUnlock()
|
|
|
|
|
if !hasID {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "无权访问",
|
|
|
|
|
Msg: "用户未登录或非法终端",
|
|
|
|
|
Link: "/",
|
|
|
|
|
Btn: "返回首页",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if terminal.userConn == nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "无权访问",
|
|
|
|
|
Msg: "用户不在线",
|
|
|
|
|
Link: "/",
|
|
|
|
|
Btn: "返回首页",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if terminal.agentConn != nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusInternalServerError,
|
|
|
|
|
Title: "连接已存在",
|
|
|
|
|
Msg: "Websocket协议切换失败",
|
|
|
|
|
Link: "/",
|
|
|
|
|
Btn: "返回首页",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
isAgent = true
|
|
|
|
|
} else {
|
|
|
|
|
singleton.ServerLock.RLock()
|
|
|
|
|
server := singleton.ServerList[terminal.serverID]
|
|
|
|
|
singleton.ServerLock.RUnlock()
|
|
|
|
|
if server == nil || server.TaskStream == nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "请求失败",
|
|
|
|
|
Msg: "服务器不存在或处于离线状态",
|
|
|
|
|
Link: "/server",
|
|
|
|
|
Btn: "返回重试",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cloudflareCookies, _ := c.Cookie("CF_Authorization")
|
|
|
|
|
// Cloudflare Cookies 合法性验证
|
|
|
|
|
// 其应该包含.分隔的三组BASE64-URL编码
|
|
|
|
|
if cloudflareCookies != "" {
|
|
|
|
|
encodedCookies := strings.Split(cloudflareCookies, ".")
|
|
|
|
|
if len(encodedCookies) == 3 {
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
if !cloudflareCookiesValidator.MatchString(encodedCookies[i]) {
|
|
|
|
|
cloudflareCookies = ""
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
cloudflareCookies = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
|
|
|
|
Host: terminal.host,
|
|
|
|
|
UseSSL: terminal.useSSL,
|
|
|
|
|
Session: terminalID,
|
|
|
|
|
Cookie: cloudflareCookies,
|
|
|
|
|
})
|
|
|
|
|
if err := server.TaskStream.Send(&proto.Task{
|
|
|
|
|
Type: model.TaskTypeTerminal,
|
|
|
|
|
Data: string(terminalData),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "请求失败",
|
|
|
|
|
Msg: "Agent信令下发失败",
|
|
|
|
|
Link: "/server",
|
|
|
|
|
Btn: "返回重试",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
defer rpc.NezhaHandlerSingleton.CloseStream(terminalID)
|
|
|
|
|
|
|
|
|
|
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -447,36 +332,7 @@ func (cp *commonPage) terminal(c *gin.Context) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer wsConn.Close()
|
|
|
|
|
conn := &websocketx.Conn{Conn: wsConn}
|
|
|
|
|
|
|
|
|
|
log.Printf("NEZHA>> terminal connected %t %q", isAgent, c.Request.URL)
|
|
|
|
|
defer log.Printf("NEZHA>> terminal disconnected %t %q", isAgent, c.Request.URL)
|
|
|
|
|
|
|
|
|
|
if isAgent {
|
|
|
|
|
terminal.agentConn = conn
|
|
|
|
|
defer func() {
|
|
|
|
|
// Agent断开链接时断开用户连接
|
|
|
|
|
if terminal.userConn != nil {
|
|
|
|
|
terminal.userConn.Close()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
} else {
|
|
|
|
|
terminal.userConn = conn
|
|
|
|
|
defer func() {
|
|
|
|
|
// 用户断开链接时断开 Agent 连接
|
|
|
|
|
if terminal.agentConn != nil {
|
|
|
|
|
terminal.agentConn.Close()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deadlineCh := make(chan interface{})
|
|
|
|
|
go func() {
|
|
|
|
|
// 对方连接超时
|
|
|
|
|
connectDeadline := time.NewTimer(time.Second * 15)
|
|
|
|
|
<-connectDeadline.C
|
|
|
|
|
deadlineCh <- struct{}{}
|
|
|
|
|
}()
|
|
|
|
|
conn := websocketx.NewConn(wsConn)
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
// PING 保活
|
|
|
|
@@ -488,58 +344,11 @@ func (cp *commonPage) terminal(c *gin.Context) {
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
dataCh := make(chan []byte)
|
|
|
|
|
errorCh := make(chan error)
|
|
|
|
|
go func() {
|
|
|
|
|
for {
|
|
|
|
|
msgType, data, err := conn.ReadMessage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
errorCh <- err
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// 将文本消息转换为命令输入
|
|
|
|
|
if msgType == websocket.TextMessage {
|
|
|
|
|
data = append([]byte{0}, data...)
|
|
|
|
|
}
|
|
|
|
|
dataCh <- data
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
var dataBuffer [][]byte
|
|
|
|
|
var distConn *websocketx.Conn
|
|
|
|
|
checkDistConn := func() {
|
|
|
|
|
if distConn == nil {
|
|
|
|
|
if isAgent {
|
|
|
|
|
distConn = terminal.userConn
|
|
|
|
|
} else {
|
|
|
|
|
distConn = terminal.agentConn
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err = rpc.NezhaHandlerSingleton.UserConnected(terminalID, conn); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-deadlineCh:
|
|
|
|
|
checkDistConn()
|
|
|
|
|
if distConn == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
case <-errorCh:
|
|
|
|
|
return
|
|
|
|
|
case data := <-dataCh:
|
|
|
|
|
dataBuffer = append(dataBuffer, data)
|
|
|
|
|
checkDistConn()
|
|
|
|
|
if distConn != nil {
|
|
|
|
|
for i := 0; i < len(dataBuffer); i++ {
|
|
|
|
|
err = distConn.WriteMessage(websocket.BinaryMessage, dataBuffer[i])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dataBuffer = dataBuffer[:0]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rpc.NezhaHandlerSingleton.StartStream(terminalID, time.Second*10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type createTerminalRequest struct {
|
|
|
|
@@ -585,6 +394,8 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rpc.NezhaHandlerSingleton.CreateStream(id)
|
|
|
|
|
|
|
|
|
|
singleton.ServerLock.RLock()
|
|
|
|
|
server := singleton.ServerList[createTerminalReq.ID]
|
|
|
|
|
singleton.ServerLock.RUnlock()
|
|
|
|
@@ -599,13 +410,21 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cp.terminalsLock.Lock()
|
|
|
|
|
defer cp.terminalsLock.Unlock()
|
|
|
|
|
|
|
|
|
|
cp.terminals[id] = &terminalContext{
|
|
|
|
|
serverID: createTerminalReq.ID,
|
|
|
|
|
host: createTerminalReq.Host,
|
|
|
|
|
useSSL: createTerminalReq.Protocol == "https:",
|
|
|
|
|
terminalData, _ := utils.Json.Marshal(&model.TerminalTask{
|
|
|
|
|
StreamID: id,
|
|
|
|
|
})
|
|
|
|
|
if err := server.TaskStream.Send(&proto.Task{
|
|
|
|
|
Type: model.TaskTypeTerminalGRPC,
|
|
|
|
|
Data: string(terminalData),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
|
|
|
|
Code: http.StatusForbidden,
|
|
|
|
|
Title: "请求失败",
|
|
|
|
|
Msg: "Agent信令下发失败",
|
|
|
|
|
Link: "/server",
|
|
|
|
|
Btn: "返回重试",
|
|
|
|
|
}, true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
|
|
|
|
|