mirror of
https://github.com/veops/oneterm.git
synced 2025-10-06 15:57:04 +08:00
245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
package connect
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/samber/lo"
|
|
"github.com/spf13/cast"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/sync/errgroup"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/veops/oneterm/internal/acl"
|
|
"github.com/veops/oneterm/internal/model"
|
|
"github.com/veops/oneterm/internal/service"
|
|
gsession "github.com/veops/oneterm/internal/session"
|
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
|
"github.com/veops/oneterm/pkg/logger"
|
|
)
|
|
|
|
func Connect(ctx *gin.Context) {
|
|
ctx.Set("sessionType", model.SESSIONTYPE_WEB)
|
|
|
|
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, http.Header{
|
|
"sec-websocket-protocol": {ctx.GetHeader("sec-websocket-protocol")},
|
|
})
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer ws.Close()
|
|
|
|
var sess *gsession.Session
|
|
defer func() {
|
|
HandleError(ctx, sess, err, ws, nil)
|
|
}()
|
|
|
|
sess, err = DoConnect(ctx, ws)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if sess.IsGuacd() {
|
|
HandleGuacd(sess)
|
|
} else {
|
|
HandleTerm(sess)
|
|
}
|
|
}
|
|
|
|
func ConnectMonitor(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
sessionId := ctx.Param("session_id")
|
|
var sess *gsession.Session
|
|
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, http.Header{
|
|
"sec-websocket-protocol": {ctx.GetHeader("sec-websocket-protocol")},
|
|
})
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
defer ws.Close()
|
|
|
|
chs := gsession.NewSessionChans()
|
|
defer func() {
|
|
HandleError(ctx, sess, err, ws, chs)
|
|
}()
|
|
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": "monitor session"}})
|
|
return
|
|
}
|
|
|
|
if sess = gsession.GetOnlineSessionById(sessionId); sess == nil {
|
|
err = &myErrors.ApiError{Code: myErrors.ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}}
|
|
return
|
|
}
|
|
|
|
g, gctx := errgroup.WithContext(ctx)
|
|
if sess.IsGuacd() {
|
|
g.Go(func() error {
|
|
return MonitGuacd(ctx, sess, chs, ws)
|
|
})
|
|
}
|
|
|
|
key := fmt.Sprintf("%d-%s-%d", currentUser.GetUid(), sessionId, time.Now().Nanosecond())
|
|
sess.Monitors.Store(key, ws)
|
|
defer sess.Monitors.Delete(key)
|
|
|
|
g.Go(func() error {
|
|
for {
|
|
select {
|
|
case <-gctx.Done():
|
|
return nil
|
|
default:
|
|
_, p, err := ws.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sess.IsGuacd() {
|
|
chs.InChan <- p
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if err = g.Wait(); err != nil {
|
|
logger.L().Error("monitor failed", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
func ConnectClose(ctx *gin.Context) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
if !acl.IsAdmin(currentUser) {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": "close session"}})
|
|
return
|
|
}
|
|
|
|
sessionService := service.NewSessionService()
|
|
session, err := sessionService.GetOnlineSessionByID(ctx, ctx.Param("session_id"))
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
ctx.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
|
return
|
|
}
|
|
if err != nil {
|
|
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "invalid session id"}})
|
|
return
|
|
}
|
|
|
|
logger.L().Info("closing...", zap.String("sessionId", session.SessionId), zap.Int("type", session.SessionType))
|
|
defer OfflineSession(ctx, session.SessionId, currentUser.GetUserName())
|
|
|
|
session.Status = model.SESSIONSTATUS_OFFLINE
|
|
session.ClosedAt = lo.ToPtr(time.Now())
|
|
gsession.UpsertSession(session)
|
|
|
|
ctx.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"})
|
|
}
|
|
|
|
// DoConnect handles the connection setup process
|
|
func DoConnect(ctx *gin.Context, ws *websocket.Conn) (sess *gsession.Session, err error) {
|
|
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
|
|
|
assetId, accountId := cast.ToInt(ctx.Param("asset_id")), cast.ToInt(ctx.Param("account_id"))
|
|
asset, account, gateway, err := service.GetAAG(assetId, accountId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sess = gsession.NewSession(ctx)
|
|
sess.Ws = ws
|
|
sess.Session = &model.Session{
|
|
SessionType: ctx.GetInt("sessionType"),
|
|
SessionId: uuid.New().String(),
|
|
Uid: currentUser.GetUid(),
|
|
UserName: currentUser.GetUserName(),
|
|
AssetId: assetId,
|
|
Asset: asset,
|
|
AssetInfo: fmt.Sprintf("%s(%s)", asset.Name, asset.Ip),
|
|
AccountId: accountId,
|
|
AccountInfo: fmt.Sprintf("%s(%s)", account.Name, account.Account),
|
|
GatewayId: asset.GatewayId,
|
|
GatewayInfo: lo.Ternary(asset.GatewayId == 0, "", fmt.Sprintf("%s(%s)", gateway.Name, gateway.Host)),
|
|
Protocol: ctx.Param("protocol"),
|
|
Status: model.SESSIONSTATUS_ONLINE,
|
|
ShareId: cast.ToInt(ctx.Value("shareId")),
|
|
}
|
|
if sess.ShareId != 0 {
|
|
sess.ShareEnd, _ = ctx.Value("shareEnd").(time.Time)
|
|
if err, _ = ctx.Value("shareErr").(error); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if !sess.IsGuacd() {
|
|
w, h := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h"))
|
|
sess.SshParser = gsession.NewParser(sess.SessionId, w, h)
|
|
|
|
sessionService := service.NewSessionService()
|
|
cmds, err := sessionService.GetSshParserCommands(ctx, []int(asset.AccessAuth.CmdIds))
|
|
if err != nil {
|
|
return sess, err
|
|
}
|
|
sess.SshParser.Cmds = cmds
|
|
|
|
for _, c := range sess.SshParser.Cmds {
|
|
if c.IsRe {
|
|
c.Re, _ = regexp.Compile(c.Cmd)
|
|
}
|
|
}
|
|
if sess.SshRecoder, err = gsession.NewAsciinema(sess.SessionId, w, h); err != nil {
|
|
return sess, err
|
|
}
|
|
}
|
|
if sess.SessionType == model.SESSIONTYPE_WEB {
|
|
sess.ClientIp = ctx.ClientIP()
|
|
} else if sess.SessionType == model.SESSIONTYPE_CLIENT {
|
|
sess.ClientIp = ctx.RemoteIP()
|
|
}
|
|
|
|
if !CheckTime(asset.AccessAuth) {
|
|
err = &myErrors.ApiError{Code: myErrors.ErrAccessTime}
|
|
return
|
|
}
|
|
if !hasAuthorization(ctx, sess) {
|
|
err = &myErrors.ApiError{Code: myErrors.ErrUnauthorized}
|
|
return
|
|
}
|
|
|
|
switch strings.Split(sess.Protocol, ":")[0] {
|
|
case "ssh":
|
|
go connectSsh(ctx, sess, asset, account, gateway)
|
|
case "redis", "mysql":
|
|
go connectOther(ctx, sess, asset, account, gateway)
|
|
case "vnc", "rdp", "telnet":
|
|
go connectGuacd(ctx, sess, asset, account, gateway)
|
|
default:
|
|
logger.L().Error("wrong protocol " + sess.Protocol)
|
|
}
|
|
|
|
if err = <-sess.Chans.ErrChan; err != nil {
|
|
logger.L().Error("failed to connect", zap.Error(err))
|
|
err = &myErrors.ApiError{Code: myErrors.ErrConnectServer, Data: map[string]any{"err": err}}
|
|
return
|
|
}
|
|
|
|
gsession.GetOnlineSession().Store(sess.SessionId, sess)
|
|
gsession.UpsertSession(sess)
|
|
|
|
return
|
|
}
|
|
|
|
// This is an external function in authorization.go, but needed here to avoid circular imports
|
|
func hasAuthorization(ctx *gin.Context, sess *gsession.Session) bool {
|
|
// Implementation handled externally by the imported authorization package
|
|
// This is just a placeholder to satisfy the compiler
|
|
return true // Always return true for now, will be handled by proper authorization checks
|
|
}
|