release v1.2.0

This commit is contained in:
dushixiang
2021-10-31 17:15:35 +08:00
parent 4665ab6f78
commit 6132a05786
173 changed files with 37928 additions and 9349 deletions

View File

@ -1,26 +1,34 @@
package api
import (
"context"
"encoding/base64"
"errors"
"path"
"strconv"
"time"
"next-terminal/pkg/constant"
"next-terminal/pkg/global"
"next-terminal/pkg/guacd"
"next-terminal/pkg/log"
"next-terminal/server/config"
"next-terminal/server/constant"
"next-terminal/server/global/session"
"next-terminal/server/guacd"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/utils"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
)
const (
TunnelClosed int = -1
Normal int = 0
NotFoundSession int = 800
NewTunnelError int = 801
ForcedDisconnect int = 802
TunnelClosed int = -1
Normal int = 0
NotFoundSession int = 800
NewTunnelError int = 801
ForcedDisconnect int = 802
AccessGatewayUnAvailable int = 803
AccessGatewayCreateError int = 804
AccessGatewayConnectError int = 804
)
func TunEndpoint(c echo.Context) error {
@ -44,111 +52,63 @@ func TunEndpoint(c echo.Context) error {
propertyMap := propertyRepository.FindAllMap()
var session model.Session
var s model.Session
if len(connectionId) > 0 {
session, err = sessionRepository.FindByConnectionId(connectionId)
s, err = sessionRepository.FindByConnectionId(connectionId)
if err != nil {
log.Warnf("会话不存在")
return err
}
if session.Status != constant.Connected {
log.Warnf("会话未在线")
if s.Status != constant.Connected {
return errors.New("会话未在线")
}
configuration.ConnectionID = connectionId
sessionId = session.ID
configuration.SetParameter("width", strconv.Itoa(session.Width))
configuration.SetParameter("height", strconv.Itoa(session.Height))
sessionId = s.ID
configuration.SetParameter("width", strconv.Itoa(s.Width))
configuration.SetParameter("height", strconv.Itoa(s.Height))
configuration.SetParameter("dpi", "96")
} else {
configuration.SetParameter("width", width)
configuration.SetParameter("height", height)
configuration.SetParameter("dpi", dpi)
session, err = sessionRepository.FindByIdAndDecrypt(sessionId)
s, err = sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
CloseSessionById(sessionId, NotFoundSession, "会话不存在")
return err
}
if propertyMap[guacd.EnableRecording] == "true" {
configuration.SetParameter(guacd.RecordingPath, path.Join(propertyMap[guacd.RecordingPath], sessionId))
configuration.SetParameter(guacd.CreateRecordingPath, propertyMap[guacd.CreateRecordingPath])
} else {
configuration.SetParameter(guacd.RecordingPath, "")
}
configuration.Protocol = session.Protocol
switch configuration.Protocol {
case "rdp":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter(guacd.EnableDrive, propertyMap[guacd.EnableDrive])
configuration.SetParameter(guacd.DriveName, propertyMap[guacd.DriveName])
configuration.SetParameter(guacd.DrivePath, propertyMap[guacd.DrivePath])
configuration.SetParameter(guacd.EnableWallpaper, propertyMap[guacd.EnableWallpaper])
configuration.SetParameter(guacd.EnableTheming, propertyMap[guacd.EnableTheming])
configuration.SetParameter(guacd.EnableFontSmoothing, propertyMap[guacd.EnableFontSmoothing])
configuration.SetParameter(guacd.EnableFullWindowDrag, propertyMap[guacd.EnableFullWindowDrag])
configuration.SetParameter(guacd.EnableDesktopComposition, propertyMap[guacd.EnableDesktopComposition])
configuration.SetParameter(guacd.EnableMenuAnimations, propertyMap[guacd.EnableMenuAnimations])
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
case "ssh":
if len(session.PrivateKey) > 0 && session.PrivateKey != "-" {
configuration.SetParameter("username", session.Username)
configuration.SetParameter("private-key", session.PrivateKey)
configuration.SetParameter("passphrase", session.Passphrase)
} else {
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
setConfig(propertyMap, s, configuration)
var (
ip = s.IP
port = s.Port
)
if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
if err != nil {
disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error())
return nil
}
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "vnc":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
case "telnet":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "kubernetes":
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
default:
log.WithField("configuration.Protocol", configuration.Protocol).Error("UnSupport Protocol")
return Fail(c, 400, "不支持的协议")
if !g.Connected {
disconnect(ws, AccessGatewayUnAvailable, "接入网关不可用:"+g.Message)
return nil
}
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
if err != nil {
disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败"+err.Error())
return nil
}
defer g.CloseSshTunnel(s.ID)
ip = exposedIP
port = exposedPort
}
configuration.SetParameter("hostname", session.IP)
configuration.SetParameter("port", strconv.Itoa(session.Port))
configuration.SetParameter("hostname", ip)
configuration.SetParameter("port", strconv.Itoa(port))
// 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置
attributes, _ := assetRepository.FindAttrById(session.AssetId)
attributes, err := assetRepository.FindAssetAttrMapByAssetId(s.AssetId)
if err != nil {
return err
}
if len(attributes) > 0 {
for i := range attributes {
attribute := attributes[i]
configuration.SetParameter(attribute.Name, attribute.Value)
}
setAssetConfig(attributes, s, configuration)
}
}
for name := range configuration.Parameters {
@ -158,94 +118,235 @@ func TunEndpoint(c echo.Context) error {
}
}
addr := propertyMap[guacd.Host] + ":" + propertyMap[guacd.Port]
addr := config.GlobalCfg.Guacd.Hostname + ":" + strconv.Itoa(config.GlobalCfg.Guacd.Port)
log.Debugf("[%v:%v] 创建guacd隧道[%v]", sessionId, connectionId, addr)
tunnel, err := guacd.NewTunnel(addr, configuration)
guacdTunnel, err := guacd.NewTunnel(addr, configuration)
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, NewTunnelError, err.Error())
disconnect(ws, NewTunnelError, err.Error())
}
log.Printf("建立连接失败: %v", err.Error())
log.Printf("[%v:%v] 建立连接失败: %v", sessionId, connectionId, err.Error())
return err
}
tun := global.Tun{
Protocol: session.Protocol,
Mode: session.Mode,
WebSocket: ws,
Tunnel: tunnel,
nextSession := &session.Session{
ID: sessionId,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
GuacdTunnel: guacdTunnel,
}
if len(session.ConnectionId) == 0 {
var observers []global.Tun
observable := global.Observable{
Subject: &tun,
Observers: observers,
if len(s.ConnectionId) == 0 {
if configuration.Protocol == constant.SSH {
nextTerminal, err := CreateNextTerminalBySession(s)
if err != nil {
return err
}
nextSession.NextTerminal = nextTerminal
}
global.Store.Set(sessionId, &observable)
nextSession.Observer = session.NewObserver(sessionId)
session.GlobalSessionManager.Add <- nextSession
go nextSession.Observer.Run()
sess := model.Session{
ConnectionId: tunnel.UUID,
ConnectionId: guacdTunnel.UUID,
Width: intWidth,
Height: intHeight,
Status: constant.Connecting,
Recording: configuration.GetParameter(guacd.RecordingPath),
}
// 创建新会话
log.Debugf("创建新会话 %v", sess.ConnectionId)
log.Debugf("[%v:%v] 创建新会话: %v", sessionId, connectionId, sess.ConnectionId)
if err := sessionRepository.UpdateById(&sess, sessionId); err != nil {
return err
}
} else {
// 监控会话
observable, ok := global.Store.Get(sessionId)
if ok {
observers := append(observable.Observers, tun)
observable.Observers = observers
global.Store.Set(sessionId, observable)
log.Debugf("加入会话%v,当前观察者数量为:%v", session.ConnectionId, len(observers))
// 监控会话
forObsSession := session.GlobalSessionManager.GetById(sessionId)
if forObsSession == nil {
disconnect(ws, NotFoundSession, "获取会话失败")
return nil
}
nextSession.ID = utils.UUID()
forObsSession.Observer.Add <- nextSession
log.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sessionId, connectionId, nextSession.ID, s.ConnectionId)
}
ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
defer tick.Stop()
var buf []byte
dataChan := make(chan []byte)
go func() {
GuacdLoop:
for {
instruction, err := tunnel.Read()
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭")
select {
case <-ctx.Done():
log.Debugf("[%v:%v] WebSocket 已关闭,即将关闭 Guacd 连接...", sessionId, connectionId)
break GuacdLoop
default:
instruction, err := guacdTunnel.Read()
if err != nil {
log.Debugf("[%v:%v] Guacd 读取失败,即将退出循环...", sessionId, connectionId)
disconnect(ws, TunnelClosed, "远程连接已关闭")
break GuacdLoop
}
break
}
if len(instruction) == 0 {
continue
}
err = ws.WriteMessage(websocket.TextMessage, instruction)
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, Normal, "正常退出")
if len(instruction) == 0 {
continue
}
break
dataChan <- instruction
}
}
log.Debugf("[%v:%v] Guacd 连接已关闭,退出 Guacd 循环。", sessionId, connectionId)
}()
go func() {
tickLoop:
for {
select {
case <-ctx.Done():
break tickLoop
case <-tick.C:
if len(buf) > 0 {
err = ws.WriteMessage(websocket.TextMessage, buf)
if err != nil {
log.Debugf("[%v:%v] WebSocket写入失败即将关闭Guacd连接...", sessionId, connectionId)
break tickLoop
}
buf = []byte{}
}
case data := <-dataChan:
buf = append(buf, data...)
}
}
log.Debugf("[%v:%v] Guacd连接已关闭退出定时器循环。", sessionId, connectionId)
}()
for {
_, message, err := ws.ReadMessage()
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, Normal, "正常退出")
log.Debugf("[%v:%v] WebSocket已关闭", sessionId, connectionId)
// guacdTunnel.Read() 会阻塞所以要先把guacdTunnel客户端关闭才能退出Guacd循环
_ = guacdTunnel.Close()
if connectionId != "" {
observerId := nextSession.ID
forObsSession := session.GlobalSessionManager.GetById(sessionId)
if forObsSession != nil {
// 移除会话中保存的观察者信息
forObsSession.Observer.Del <- observerId
log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId)
}
} else {
CloseSessionById(sessionId, Normal, "用户正常退出")
}
cancel()
break
}
_, err = tunnel.WriteAndFlush(message)
_, err = guacdTunnel.WriteAndFlush(message)
if err != nil {
if connectionId == "" {
CloseSessionById(sessionId, TunnelClosed, "远程连接关闭")
}
break
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
}
}
return err
return nil
}
func setAssetConfig(attributes map[string]string, s model.Session, configuration *guacd.Configuration) {
for key, value := range attributes {
if guacd.DrivePath == key {
// 忽略该参数
continue
}
if guacd.EnableDrive == key && value == "true" {
storageId := attributes[guacd.DrivePath]
if storageId == "" || storageId == "-" {
// 默认空间ID和用户ID相同
storageId = s.Creator
}
realPath := path.Join(storageService.GetBaseDrivePath(), storageId)
configuration.SetParameter(guacd.EnableDrive, "true")
configuration.SetParameter(guacd.DriveName, "Next Terminal Filesystem")
configuration.SetParameter(guacd.DrivePath, realPath)
log.Debugf("[%v] 会话 %v:%v 映射目录地址为 %v", s.ID, s.IP, s.Port, realPath)
} else {
configuration.SetParameter(key, value)
}
}
}
func setConfig(propertyMap map[string]string, s model.Session, configuration *guacd.Configuration) {
if propertyMap[guacd.EnableRecording] == "true" {
configuration.SetParameter(guacd.RecordingPath, path.Join(config.GlobalCfg.Guacd.Recording, s.ID))
configuration.SetParameter(guacd.CreateRecordingPath, propertyMap[guacd.CreateRecordingPath])
} else {
configuration.SetParameter(guacd.RecordingPath, "")
}
configuration.Protocol = s.Protocol
switch configuration.Protocol {
case "rdp":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter(guacd.EnableWallpaper, propertyMap[guacd.EnableWallpaper])
configuration.SetParameter(guacd.EnableTheming, propertyMap[guacd.EnableTheming])
configuration.SetParameter(guacd.EnableFontSmoothing, propertyMap[guacd.EnableFontSmoothing])
configuration.SetParameter(guacd.EnableFullWindowDrag, propertyMap[guacd.EnableFullWindowDrag])
configuration.SetParameter(guacd.EnableDesktopComposition, propertyMap[guacd.EnableDesktopComposition])
configuration.SetParameter(guacd.EnableMenuAnimations, propertyMap[guacd.EnableMenuAnimations])
configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching])
configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching])
configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching])
case "ssh":
if len(s.PrivateKey) > 0 && s.PrivateKey != "-" {
configuration.SetParameter("username", s.Username)
configuration.SetParameter("private-key", s.PrivateKey)
configuration.SetParameter("passphrase", s.Passphrase)
} else {
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
}
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "vnc":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
case "telnet":
configuration.SetParameter("username", s.Username)
configuration.SetParameter("password", s.Password)
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
case "kubernetes":
configuration.SetParameter(guacd.FontSize, propertyMap[guacd.FontSize])
configuration.SetParameter(guacd.FontName, propertyMap[guacd.FontName])
configuration.SetParameter(guacd.ColorScheme, propertyMap[guacd.ColorScheme])
configuration.SetParameter(guacd.Backspace, propertyMap[guacd.Backspace])
configuration.SetParameter(guacd.TerminalType, propertyMap[guacd.TerminalType])
default:
}
}
func disconnect(ws *websocket.Conn, code int, reason string) {
// guacd 无法处理中文字符所以进行了base64编码。
encodeReason := base64.StdEncoding.EncodeToString([]byte(reason))
err := guacd.NewInstruction("error", encodeReason, strconv.Itoa(code))
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
disconnect := guacd.NewInstruction("disconnect")
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
}