mirror of
https://github.com/veops/oneterm.git
synced 2025-10-05 23:37:03 +08:00
240 lines
5.9 KiB
Go
240 lines
5.9 KiB
Go
package connect
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"github.com/samber/lo"
|
|
"github.com/spf13/cast"
|
|
"go.uber.org/zap"
|
|
|
|
myi18n "github.com/veops/oneterm/internal/i18n"
|
|
"github.com/veops/oneterm/internal/model"
|
|
gsession "github.com/veops/oneterm/internal/session"
|
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
|
"github.com/veops/oneterm/pkg/logger"
|
|
)
|
|
|
|
var (
|
|
Upgrader = websocket.Upgrader{
|
|
HandshakeTimeout: time.Minute,
|
|
ReadBufferSize: 4096,
|
|
WriteBufferSize: 4096,
|
|
CheckOrigin: func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
}
|
|
byteClearAll = []byte("\x15\r")
|
|
byteClearCur = []byte("\b\x1b[J")
|
|
byteDel = []byte{'\x7f'}
|
|
byteR = []byte{'\r'}
|
|
byteN = []byte{'\n'}
|
|
byteT = []byte{'\t'}
|
|
byteS = []byte{' '}
|
|
byteRN = append(byteR, byteN...)
|
|
|
|
reRedis = regexp.MustCompile(`("[^"]*"|'[^']*'|\S+)`)
|
|
border = lipgloss.RoundedBorder()
|
|
)
|
|
|
|
// WriteToMonitors sends data to all monitoring sessions
|
|
func WriteToMonitors(monitors *sync.Map, out []byte) {
|
|
monitors.Range(func(k, v any) bool {
|
|
if cs, ok := v.(*gsession.SessionChans); ok {
|
|
cs.OutBuf.Write(out)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// CheckTime checks if the current time is within the allowed time range
|
|
func CheckTime(data model.AccessAuth) bool {
|
|
now := time.Now()
|
|
in := true
|
|
if (data.Start != nil && now.Before(*data.Start)) || (data.End != nil && now.After(*data.End)) {
|
|
in = false
|
|
}
|
|
if !in {
|
|
return false
|
|
}
|
|
in = false
|
|
has := false
|
|
week, hm := now.Weekday(), now.Format("15:04")
|
|
for _, r := range data.Ranges {
|
|
has = has || len(r.Times) > 0
|
|
if (r.Week+1)%7 == int(week) {
|
|
for _, str := range r.Times {
|
|
ss := strings.Split(str, "~")
|
|
in = in || (len(ss) >= 2 && hm >= ss[0] && hm <= ss[1])
|
|
}
|
|
}
|
|
}
|
|
return !has || in == data.Allow
|
|
}
|
|
|
|
// OfflineSession makes a session offline
|
|
func OfflineSession(ctx *gin.Context, sessionId string, closer string) {
|
|
logger.L().Debug("offline", zap.String("session_id", sessionId), zap.String("closer", closer))
|
|
defer gsession.GetOnlineSession().Delete(sessionId)
|
|
session := gsession.GetOnlineSessionById(sessionId)
|
|
if session == nil {
|
|
return
|
|
}
|
|
if closer != "" && session.Chans != nil {
|
|
select {
|
|
case session.Chans.CloseChan <- closer:
|
|
break
|
|
case <-time.After(time.Second):
|
|
break
|
|
}
|
|
}
|
|
session.Monitors.Range(func(key, value any) bool {
|
|
ws, ok := value.(*websocket.Conn)
|
|
if ok && ws != nil {
|
|
lang := ctx.PostForm("lang")
|
|
accept := ctx.GetHeader("Accept-Language")
|
|
localizer := i18n.NewLocalizer(myi18n.Bundle, lang, accept)
|
|
cfg := &i18n.LocalizeConfig{
|
|
TemplateData: map[string]any{"sessionId": sessionId},
|
|
DefaultMessage: myi18n.MsgSessionEnd,
|
|
}
|
|
msg, _ := localizer.Localize(cfg)
|
|
ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
|
ws.Close()
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
// HandleError handles errors from sessions
|
|
func HandleError(ctx *gin.Context, sess *gsession.Session, err error, ws *websocket.Conn, chs *gsession.SessionChans) {
|
|
defer func() {
|
|
if sess == nil || sess.Chans == nil {
|
|
return
|
|
}
|
|
ch := sess.Chans.AwayChan
|
|
if chs != nil {
|
|
ch = chs.AwayChan
|
|
}
|
|
sess.Once.Do(func() { close(ch) })
|
|
}()
|
|
|
|
if err == nil {
|
|
return
|
|
}
|
|
if sess != nil {
|
|
logger.L().Debug("", zap.String("session_id", sess.SessionId), zap.Error(err))
|
|
}
|
|
ae, ok := err.(*myErrors.ApiError)
|
|
if sess != nil && sess.IsGuacd() && ws != nil {
|
|
ws.WriteMessage(websocket.TextMessage, NewInstruction("error", lo.Ternary(ok, (ae).MessageBase64(ctx), err.Error()), cast.ToString(myErrors.ErrAdminClose)).Bytes())
|
|
} else if sess != nil {
|
|
WriteErrMsg(sess, lo.Ternary(ok, ae.MessageWithCtx(ctx), err.Error()))
|
|
}
|
|
}
|
|
|
|
// WriteErrMsg writes an error message to the session
|
|
func WriteErrMsg(sess *gsession.Session, msg string) {
|
|
chs := sess.Chans
|
|
out := []byte(fmt.Sprintf("\r\n \033[31m %s \x1b[0m", msg))
|
|
chs.OutBuf.Write(out)
|
|
Write(sess)
|
|
}
|
|
|
|
// Write writes data to the session output
|
|
func Write(sess *gsession.Session) (err error) {
|
|
chs := sess.Chans
|
|
out := chs.OutBuf.Bytes()
|
|
|
|
if sess.SessionType == model.SESSIONTYPE_WEB && sess.Ws != nil {
|
|
if len(out) > 0 || sess.IsGuacd() {
|
|
err = sess.Ws.WriteMessage(websocket.TextMessage, out)
|
|
}
|
|
} else if sess.SessionType == model.SESSIONTYPE_CLIENT && len(out) > 0 {
|
|
_, err = sess.CliRw.Write(out)
|
|
}
|
|
|
|
if sess.SshRecoder != nil && len(out) > 0 && !sess.IsGuacd() {
|
|
sess.SshRecoder.Write(out)
|
|
}
|
|
|
|
WriteToMonitors(sess.Monitors, out)
|
|
chs.OutBuf.Reset()
|
|
|
|
return
|
|
}
|
|
|
|
// Read reads data from the session input
|
|
func Read(sess *gsession.Session) error {
|
|
chs := sess.Chans
|
|
for {
|
|
select {
|
|
case <-sess.Gctx.Done():
|
|
return nil
|
|
case <-sess.Chans.AwayChan:
|
|
return nil
|
|
default:
|
|
if sess.SessionType == model.SESSIONTYPE_WEB {
|
|
t, msg, err := sess.Ws.ReadMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(msg) <= 0 {
|
|
continue
|
|
}
|
|
switch t {
|
|
case websocket.TextMessage:
|
|
chs.InChan <- msg
|
|
if (sess.IsGuacd() && len(msg) > 0 && msg[0] != '9') || (!sess.IsGuacd() && IsActive(msg)) {
|
|
sess.SetIdle()
|
|
}
|
|
}
|
|
} else if sess.SessionType == model.SESSIONTYPE_CLIENT {
|
|
p, err := sess.CliRw.Read()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chs.InChan <- p
|
|
sess.SetIdle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Instruction represents a Guacamole instruction
|
|
type Instruction struct {
|
|
opcode string
|
|
args []string
|
|
}
|
|
|
|
// NewInstruction creates a new Guacamole instruction
|
|
func NewInstruction(opcode string, args ...string) *Instruction {
|
|
return &Instruction{
|
|
opcode: opcode,
|
|
args: args,
|
|
}
|
|
}
|
|
|
|
// Bytes converts the instruction to a byte array
|
|
func (instr *Instruction) Bytes() []byte {
|
|
result := instr.opcode
|
|
for _, arg := range instr.args {
|
|
result += "," + fmt.Sprintf("%d", len(arg)) + "." + arg
|
|
}
|
|
result += ";"
|
|
return []byte(result)
|
|
}
|
|
|
|
// IsActive checks if a message indicates user activity
|
|
func IsActive(message []byte) bool {
|
|
return len(message) > 0
|
|
}
|