next-terminal/server/api/guacamole.go
2022-10-23 20:05:13 +08:00

342 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"fmt"
"net/http"
"next-terminal/server/common/guacamole"
"next-terminal/server/common/nt"
"path"
"strconv"
"next-terminal/server/config"
"next-terminal/server/global/session"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/service"
"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
AccessGatewayUnAvailable int = 803
AccessGatewayCreateError int = 804
AssetNotActive int = 805
NewSshClientError int = 806
)
var UpGrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"guacamole"},
}
type GuacamoleApi struct {
}
func (api GuacamoleApi) Guacamole(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
log.Warn("升级为WebSocket协议失败", log.NamedError("err", err))
return err
}
ctx := context.TODO()
width := c.QueryParam("width")
height := c.QueryParam("height")
dpi := c.QueryParam("dpi")
sessionId := c.Param("id")
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
configuration := guacamole.NewConfiguration()
propertyMap := repository.PropertyRepository.FindAllMap(ctx)
configuration.SetParameter("width", width)
configuration.SetParameter("height", height)
configuration.SetParameter("dpi", dpi)
s, err := service.SessionService.FindByIdAndDecrypt(ctx, sessionId)
if err != nil {
return err
}
api.setConfig(propertyMap, s, configuration)
if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
g, err := service.GatewayService.GetGatewayById(s.AccessGatewayId)
if err != nil {
guacamole.Disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error())
return nil
}
defer g.CloseSshTunnel(s.ID)
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, s.IP, s.Port)
if err != nil {
guacamole.Disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败"+err.Error())
return nil
}
s.IP = exposedIP
s.Port = exposedPort
}
configuration.SetParameter("hostname", s.IP)
configuration.SetParameter("port", strconv.Itoa(s.Port))
// 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置
attributes, err := repository.AssetRepository.FindAssetAttrMapByAssetId(ctx, s.AssetId)
if err != nil {
return err
}
if len(attributes) > 0 {
api.setAssetConfig(attributes, s, configuration)
}
for name := range configuration.Parameters {
// 替换数据库空格字符串占位符为真正的空格
if configuration.Parameters[name] == "-" {
configuration.Parameters[name] = ""
}
}
addr := config.GlobalCfg.Guacd.Hostname + ":" + strconv.Itoa(config.GlobalCfg.Guacd.Port)
asset := fmt.Sprintf("%s:%s", configuration.GetParameter("hostname"), configuration.GetParameter("port"))
log.Debug("新建 guacd 会话", log.String("sessionId", sessionId), log.String("addr", addr), log.String("asset", asset))
guacdTunnel, err := guacamole.NewTunnel(addr, configuration)
if err != nil {
guacamole.Disconnect(ws, NewTunnelError, err.Error())
log.Error("建立连接失败", log.String("sessionId", sessionId), log.NamedError("err", err))
return err
}
nextSession := &session.Session{
ID: sessionId,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
GuacdTunnel: guacdTunnel,
}
if configuration.Protocol == nt.SSH {
nextTerminal, err := CreateNextTerminalBySession(s)
if err != nil {
guacamole.Disconnect(ws, NewSshClientError, "建立SSH客户端失败: "+err.Error())
log.Debug("建立 ssh 客户端失败", log.String("sessionId", sessionId), log.NamedError("err", err))
return err
}
nextSession.NextTerminal = nextTerminal
}
nextSession.Observer = session.NewObserver(sessionId)
session.GlobalSessionManager.Add(nextSession)
sess := model.Session{
ConnectionId: guacdTunnel.UUID,
Width: intWidth,
Height: intHeight,
Status: nt.Connecting,
Recording: configuration.GetParameter(guacamole.RecordingPath),
}
if sess.Recording == "" {
// 未录屏时无需审计
sess.Reviewed = true
}
// 创建新会话
log.Debug("新建会话成功", log.String("sessionId", sessionId))
if err := repository.SessionRepository.UpdateById(ctx, &sess, sessionId); err != nil {
return err
}
guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
guacamoleHandler.Start()
defer guacamoleHandler.Stop()
for {
_, message, err := ws.ReadMessage()
if err != nil {
log.Debug("WebSocket已关闭", log.String("sessionId", sessionId), log.NamedError("err", err))
// guacdTunnel.Read() 会阻塞所以要先把guacdTunnel客户端关闭才能退出Guacd循环
_ = guacdTunnel.Close()
service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出")
return nil
}
_, err = guacdTunnel.WriteAndFlush(message)
if err != nil {
service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
return nil
}
}
}
func (api GuacamoleApi) setAssetConfig(attributes map[string]string, s model.Session, configuration *guacamole.Configuration) {
for key, value := range attributes {
if guacamole.DrivePath == key {
// 忽略该参数
continue
}
if guacamole.EnableDrive == key && value == "true" {
storageId := attributes[guacamole.DrivePath]
if storageId == "" || storageId == "-" {
// 默认空间ID和用户ID相同
storageId = s.Creator
}
realPath := path.Join(service.StorageService.GetBaseDrivePath(), storageId)
configuration.SetParameter(guacamole.EnableDrive, "true")
configuration.SetParameter(guacamole.DriveName, "Filesystem")
configuration.SetParameter(guacamole.DrivePath, realPath)
} else {
configuration.SetParameter(key, value)
}
}
}
func (api GuacamoleApi) GuacamoleMonitor(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
log.Warn("升级为WebSocket协议失败", log.NamedError("err", err))
return err
}
ctx := context.TODO()
sessionId := c.Param("id")
s, err := repository.SessionRepository.FindById(ctx, sessionId)
if err != nil {
return err
}
if s.Status != nt.Connected {
guacamole.Disconnect(ws, AssetNotActive, "会话离线")
return nil
}
connectionId := s.ConnectionId
configuration := guacamole.NewConfiguration()
configuration.ConnectionID = connectionId
sessionId = s.ID
configuration.SetParameter("width", strconv.Itoa(s.Width))
configuration.SetParameter("height", strconv.Itoa(s.Height))
configuration.SetParameter("dpi", "96")
addr := config.GlobalCfg.Guacd.Hostname + ":" + strconv.Itoa(config.GlobalCfg.Guacd.Port)
guacdTunnel, err := guacamole.NewTunnel(addr, configuration)
if err != nil {
guacamole.Disconnect(ws, NewTunnelError, err.Error())
return err
}
nextSession := &session.Session{
ID: sessionId,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
GuacdTunnel: guacdTunnel,
}
// 要监控会话
forObsSession := session.GlobalSessionManager.GetById(sessionId)
if forObsSession == nil {
guacamole.Disconnect(ws, NotFoundSession, "获取会话失败")
return nil
}
nextSession.ID = utils.UUID()
forObsSession.Observer.Add(nextSession)
guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel)
guacamoleHandler.Start()
defer guacamoleHandler.Stop()
for {
_, message, err := ws.ReadMessage()
if err != nil {
// guacdTunnel.Read() 会阻塞所以要先把guacdTunnel客户端关闭才能退出Guacd循环
_ = guacdTunnel.Close()
observerId := nextSession.ID
forObsSession.Observer.Del(observerId)
return nil
}
_, err = guacdTunnel.WriteAndFlush(message)
if err != nil {
service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
return nil
}
}
}
func (api GuacamoleApi) setConfig(propertyMap map[string]string, s model.Session, configuration *guacamole.Configuration) {
if propertyMap[guacamole.EnableRecording] == "true" {
configuration.SetParameter(guacamole.RecordingPath, path.Join(config.GlobalCfg.Guacd.Recording, s.ID))
configuration.SetParameter(guacamole.CreateRecordingPath, "true")
} else {
configuration.SetParameter(guacamole.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(guacamole.EnableWallpaper, propertyMap[guacamole.EnableWallpaper])
configuration.SetParameter(guacamole.EnableTheming, propertyMap[guacamole.EnableTheming])
configuration.SetParameter(guacamole.EnableFontSmoothing, propertyMap[guacamole.EnableFontSmoothing])
configuration.SetParameter(guacamole.EnableFullWindowDrag, propertyMap[guacamole.EnableFullWindowDrag])
configuration.SetParameter(guacamole.EnableDesktopComposition, propertyMap[guacamole.EnableDesktopComposition])
configuration.SetParameter(guacamole.EnableMenuAnimations, propertyMap[guacamole.EnableMenuAnimations])
configuration.SetParameter(guacamole.DisableBitmapCaching, propertyMap[guacamole.DisableBitmapCaching])
configuration.SetParameter(guacamole.DisableOffscreenCaching, propertyMap[guacamole.DisableOffscreenCaching])
configuration.SetParameter(guacamole.ColorDepth, propertyMap[guacamole.ColorDepth])
configuration.SetParameter(guacamole.ForceLossless, propertyMap[guacamole.ForceLossless])
configuration.SetParameter(guacamole.PreConnectionId, propertyMap[guacamole.PreConnectionId])
configuration.SetParameter(guacamole.PreConnectionBlob, propertyMap[guacamole.PreConnectionBlob])
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(guacamole.FontSize, propertyMap[guacamole.FontSize])
configuration.SetParameter(guacamole.FontName, propertyMap[guacamole.FontName])
configuration.SetParameter(guacamole.ColorScheme, propertyMap[guacamole.ColorScheme])
configuration.SetParameter(guacamole.Backspace, propertyMap[guacamole.Backspace])
configuration.SetParameter(guacamole.TerminalType, propertyMap[guacamole.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(guacamole.FontSize, propertyMap[guacamole.FontSize])
configuration.SetParameter(guacamole.FontName, propertyMap[guacamole.FontName])
configuration.SetParameter(guacamole.ColorScheme, propertyMap[guacamole.ColorScheme])
configuration.SetParameter(guacamole.Backspace, propertyMap[guacamole.Backspace])
configuration.SetParameter(guacamole.TerminalType, propertyMap[guacamole.TerminalType])
case "kubernetes":
configuration.SetParameter(guacamole.FontSize, propertyMap[guacamole.FontSize])
configuration.SetParameter(guacamole.FontName, propertyMap[guacamole.FontName])
configuration.SetParameter(guacamole.ColorScheme, propertyMap[guacamole.ColorScheme])
configuration.SetParameter(guacamole.Backspace, propertyMap[guacamole.Backspace])
configuration.SetParameter(guacamole.TerminalType, propertyMap[guacamole.TerminalType])
default:
}
}