mirror of
https://github.com/veops/oneterm.git
synced 2025-10-05 07:16:57 +08:00
238 lines
5.9 KiB
Go
238 lines
5.9 KiB
Go
package protocols
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gliderlabs/ssh"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/samber/lo"
|
|
"github.com/spf13/cast"
|
|
"go.uber.org/zap"
|
|
gossh "golang.org/x/crypto/ssh"
|
|
|
|
"github.com/veops/oneterm/internal/model"
|
|
"github.com/veops/oneterm/internal/service"
|
|
gsession "github.com/veops/oneterm/internal/session"
|
|
"github.com/veops/oneterm/internal/tunneling"
|
|
myErrors "github.com/veops/oneterm/pkg/errors"
|
|
"github.com/veops/oneterm/pkg/logger"
|
|
)
|
|
|
|
// ConnectSsh connects to SSH server
|
|
func ConnectSsh(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, account *model.Account, gateway *model.Gateway) (err error) {
|
|
w, h := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h"))
|
|
chs := sess.Chans
|
|
defer func() {
|
|
if err != nil {
|
|
chs.ErrChan <- err
|
|
}
|
|
}()
|
|
|
|
ip, port, err := tunneling.Proxy(false, sess.SessionId, "ssh", asset, gateway)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
auth, err := service.GetAuth(account)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sshCli, err := gossh.Dial("tcp", fmt.Sprintf("%s:%d", ip, port), &gossh.ClientConfig{
|
|
User: account.Account,
|
|
Auth: []gossh.AuthMethod{auth},
|
|
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
|
Timeout: time.Second,
|
|
})
|
|
if err != nil {
|
|
logger.L().Error("ssh dial failed", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
sshSess, err := sshCli.NewSession()
|
|
if err != nil {
|
|
logger.L().Error("ssh session create failed", zap.Error(err))
|
|
return
|
|
}
|
|
defer sshSess.Close()
|
|
|
|
sshSess.Stdin = chs.Rin
|
|
sshSess.Stdout = chs.Wout
|
|
sshSess.Stderr = chs.Wout
|
|
|
|
modes := gossh.TerminalModes{
|
|
gossh.ECHO: 1,
|
|
gossh.TTY_OP_ISPEED: 14400,
|
|
gossh.TTY_OP_OSPEED: 14400,
|
|
}
|
|
if err = sshSess.RequestPty("xterm", h, w, modes); err != nil {
|
|
logger.L().Error("ssh request pty failed", zap.Error(err))
|
|
return
|
|
}
|
|
if err = sshSess.Shell(); err != nil {
|
|
logger.L().Error("ssh start shell failed", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
sess.G.Go(func() error {
|
|
err = sshSess.Wait()
|
|
return fmt.Errorf("ssh session wait end %w", err)
|
|
})
|
|
|
|
chs.ErrChan <- err
|
|
|
|
sess.G.Go(func() error {
|
|
buf := bufio.NewReader(chs.Rout)
|
|
for {
|
|
select {
|
|
case <-sess.Gctx.Done():
|
|
return nil
|
|
default:
|
|
rn, size, err := buf.ReadRune()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if size <= 0 || rn == utf8.RuneError {
|
|
continue
|
|
}
|
|
p := make([]byte, utf8.RuneLen(rn))
|
|
utf8.EncodeRune(p, rn)
|
|
chs.OutChan <- p
|
|
}
|
|
}
|
|
})
|
|
sess.G.Go(func() error {
|
|
defer sshSess.Close()
|
|
defer sess.Chans.Rout.Close()
|
|
defer sess.Chans.Win.Close()
|
|
for {
|
|
select {
|
|
case <-sess.Gctx.Done():
|
|
return nil
|
|
case <-chs.AwayChan:
|
|
return fmt.Errorf("away")
|
|
case window := <-chs.WindowChan:
|
|
if err := sshSess.WindowChange(window.Height, window.Width); err != nil {
|
|
logger.L().Warn("reset window size failed", zap.Error(err))
|
|
continue
|
|
}
|
|
sess.SshRecoder.Resize(window.Width, window.Height)
|
|
sess.SshParser.Resize(window.Width, window.Height)
|
|
}
|
|
}
|
|
})
|
|
|
|
sess.G.Wait()
|
|
|
|
return
|
|
}
|
|
|
|
// HandleTerm handles terminal sessions
|
|
func HandleTerm(sess *gsession.Session) (err error) {
|
|
defer func() {
|
|
logger.L().Debug("defer HandleTerm", zap.String("sessionId", sess.SessionId))
|
|
sess.SshParser.Close(sess.Prompt)
|
|
sess.Status = model.SESSIONSTATUS_OFFLINE
|
|
sess.ClosedAt = lo.ToPtr(time.Now())
|
|
if err = gsession.UpsertSession(sess); err != nil {
|
|
logger.L().Error("offline session failed", zap.String("sessionId", sess.SessionId), zap.Error(err))
|
|
return
|
|
}
|
|
}()
|
|
chs := sess.Chans
|
|
tk, tk1s, tk1m := time.NewTicker(time.Millisecond*100), time.NewTicker(time.Second), time.NewTicker(time.Minute)
|
|
assetService := service.NewAssetService()
|
|
sess.G.Go(func() error {
|
|
return Read(sess)
|
|
})
|
|
sess.G.Go(func() (err error) {
|
|
defer sess.Chans.Rin.Close()
|
|
defer sess.Chans.Wout.Close()
|
|
for {
|
|
select {
|
|
case <-sess.Gctx.Done():
|
|
Write(sess)
|
|
return
|
|
case <-chs.AwayChan:
|
|
return
|
|
case <-sess.IdleTk.C:
|
|
WriteErrMsg(sess, "idle timeout\n\n")
|
|
return &myErrors.ApiError{Code: myErrors.ErrIdleTimeout, Data: map[string]any{"second": model.GlobalConfig.Load().Timeout}}
|
|
case <-tk1m.C:
|
|
asset, err := assetService.GetById(sess.Gctx, sess.AssetId)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if CheckTime(asset.AccessAuth) && (sess.ShareId == 0 || time.Now().Before(sess.ShareEnd)) {
|
|
continue
|
|
}
|
|
return &myErrors.ApiError{Code: myErrors.ErrAccessTime}
|
|
case closeBy := <-chs.CloseChan:
|
|
WriteErrMsg(sess, "closed by admin\n\n")
|
|
logger.L().Info("closed by", zap.String("admin", closeBy))
|
|
return &myErrors.ApiError{Code: myErrors.ErrAdminClose, Data: map[string]any{"admin": closeBy}}
|
|
case err = <-chs.ErrChan:
|
|
WriteErrMsg(sess, err.Error())
|
|
return
|
|
case in := <-chs.InChan:
|
|
if sess.SessionType == model.SESSIONTYPE_WEB {
|
|
rt := in[0]
|
|
msg := in[1:]
|
|
switch rt {
|
|
case '1':
|
|
in = msg
|
|
case '9':
|
|
continue
|
|
case 'w':
|
|
wh := strings.Split(string(msg), ",")
|
|
if len(wh) < 2 {
|
|
continue
|
|
}
|
|
chs.WindowChan <- ssh.Window{
|
|
Width: cast.ToInt(wh[0]),
|
|
Height: cast.ToInt(wh[1]),
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
if cmd, forbidden := sess.SshParser.AddInput(in); forbidden {
|
|
WriteErrMsg(sess, fmt.Sprintf("%s is forbidden\n", cmd))
|
|
sess.SshParser.AddInput(byteClearAll)
|
|
chs.Win.Write(byteClearAll)
|
|
continue
|
|
}
|
|
if _, err = chs.Win.Write(in); err != nil {
|
|
return
|
|
}
|
|
case out := <-chs.OutChan:
|
|
if _, err = chs.OutBuf.Write(out); err != nil {
|
|
return
|
|
}
|
|
sess.SshParser.AddOutput(out)
|
|
case <-tk.C:
|
|
if err = Write(sess); err != nil {
|
|
return
|
|
}
|
|
case <-tk1s.C:
|
|
if sess.Ws == nil {
|
|
continue
|
|
}
|
|
if err = sess.Ws.WriteMessage(websocket.TextMessage, nil); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if err = sess.G.Wait(); err != nil {
|
|
logger.L().Debug("handle term wait end", zap.String("id", sess.SessionId), zap.Error(err))
|
|
}
|
|
|
|
return
|
|
}
|