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,18 +1,23 @@
package api
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"path"
"strconv"
"time"
"unicode/utf8"
"next-terminal/pkg/constant"
"next-terminal/pkg/global"
"next-terminal/pkg/guacd"
"next-terminal/pkg/log"
"next-terminal/pkg/term"
"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/term"
"next-terminal/server/utils"
"github.com/gorilla/websocket"
@ -27,17 +32,44 @@ var UpGrader = websocket.Upgrader{
}
const (
Connected = "connected"
Data = "data"
Resize = "resize"
Closed = "closed"
Closed = 0
Connected = 1
Data = 2
Resize = 3
Ping = 4
)
type Message struct {
Type string `json:"type"`
Type int `json:"type"`
Content string `json:"content"`
}
func (r Message) ToString() string {
if r.Content != "" {
return strconv.Itoa(r.Type) + r.Content
} else {
return strconv.Itoa(r.Type)
}
}
func NewMessage(_type int, content string) Message {
return Message{Content: content, Type: _type}
}
func ParseMessage(value string) (message Message, err error) {
if value == "" {
return
}
_type, err := strconv.Atoi(value[:1])
if err != nil {
return
}
var content = value[1:]
message = NewMessage(_type, content)
return
}
type WindowSize struct {
Cols int `json:"cols"`
Rows int `json:"rows"`
@ -50,92 +82,73 @@ func SSHEndpoint(c echo.Context) (err error) {
return err
}
defer ws.Close()
sessionId := c.QueryParam("sessionId")
cols, _ := strconv.Atoi(c.QueryParam("cols"))
rows, _ := strconv.Atoi(c.QueryParam("rows"))
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
msg := Message{
Type: Closed,
Content: "get sshSession error." + err.Error(),
}
_ = WriteMessage(ws, msg)
return err
return WriteMessage(ws, NewMessage(Closed, "获取会话失败"))
}
user, _ := GetCurrentAccount(c)
if constant.TypeUser == user.Type {
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
if !utils.Contains(assetIds, session.AssetId) {
msg := Message{
Type: Closed,
Content: "您没有权限访问此资产",
}
return WriteMessage(ws, msg)
}
if err := permissionCheck(c, s.AssetId); err != nil {
return WriteMessage(ws, NewMessage(Closed, err.Error()))
}
var (
username = session.Username
password = session.Password
privateKey = session.PrivateKey
passphrase = session.Passphrase
ip = session.IP
port = session.Port
username = s.Username
password = s.Password
privateKey = s.PrivateKey
passphrase = s.Passphrase
ip = s.IP
port = s.Port
)
recording := ""
propertyMap := propertyRepository.FindAllMap()
if propertyMap[guacd.EnableRecording] == "true" {
recording = path.Join(propertyMap[guacd.RecordingPath], sessionId, "recording.cast")
}
tun := global.Tun{
Protocol: session.Protocol,
Mode: session.Mode,
WebSocket: ws,
}
if session.ConnectionId != "" {
// 监控会话
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))
if s.AccessGatewayId != "" && s.AccessGatewayId != "-" {
g, err := accessGatewayService.GetGatewayAndReconnectById(s.AccessGatewayId)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "获取接入网关失败:"+err.Error()))
}
return err
if !g.Connected {
return WriteMessage(ws, NewMessage(Closed, "接入网关不可用:"+g.Message))
}
exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "创建隧道失败:"+err.Error()))
}
defer g.CloseSshTunnel(s.ID)
ip = exposedIP
port = exposedPort
}
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording)
recording := ""
var isRecording = false
property, err := propertyRepository.FindByName(guacd.EnableRecording)
if err == nil && property.Value == "true" {
isRecording = true
}
if isRecording {
recording = path.Join(config.GlobalCfg.Guacd.Recording, sessionId, "recording.cast")
}
var xterm = "xterm-256color"
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording, xterm, true)
if err != nil {
log.Errorf("创建SSH客户端失败%v", err.Error())
msg := Message{
Type: Closed,
Content: err.Error(),
}
err := WriteMessage(ws, msg)
return WriteMessage(ws, NewMessage(Closed, "创建SSH客户端失败"+err.Error()))
}
if err := nextTerminal.RequestPty(xterm, rows, cols); err != nil {
return err
}
tun.NextTerminal = nextTerminal
var observers []global.Tun
observable := global.Observable{
Subject: &tun,
Observers: observers,
if err := nextTerminal.Shell(); err != nil {
return err
}
global.Store.Set(sessionId, &observable)
sess := model.Session{
ConnectionId: sessionId,
Width: cols,
@ -149,106 +162,209 @@ func SSHEndpoint(c echo.Context) (err error) {
return err
}
msg := Message{
Type: Connected,
Content: "",
if err := WriteMessage(ws, NewMessage(Connected, "")); err != nil {
return err
}
_ = WriteMessage(ws, msg)
quitChan := make(chan bool)
nextSession := &session.Session{
ID: s.ID,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
GuacdTunnel: nil,
NextTerminal: nextTerminal,
Observer: session.NewObserver(s.ID),
}
go nextSession.Observer.Run()
session.GlobalSessionManager.Add <- nextSession
go ReadMessage(nextTerminal, quitChan, ws)
ctx, cancel := context.WithCancel(context.Background())
tick := time.NewTicker(time.Millisecond * time.Duration(60))
defer tick.Stop()
var buf []byte
dataChan := make(chan rune)
go func() {
SshLoop:
for {
select {
case <-ctx.Done():
log.Debugf("WebSocket已关闭即将关闭SSH连接...")
break SshLoop
default:
r, size, err := nextTerminal.StdoutReader.ReadRune()
if err != nil {
log.Debugf("SSH 读取失败,即将退出循环...")
_ = WriteMessage(ws, NewMessage(Closed, ""))
break SshLoop
}
if size > 0 {
dataChan <- r
}
}
}
log.Debugf("SSH 连接已关闭,退出循环。")
}()
go func() {
tickLoop:
for {
select {
case <-ctx.Done():
break tickLoop
case <-tick.C:
if len(buf) > 0 {
s := string(buf)
// 录屏
if isRecording {
_ = nextTerminal.Recorder.WriteData(s)
}
// 监控
if len(nextSession.Observer.All()) > 0 {
obs := nextSession.Observer.All()
for _, ob := range obs {
_ = WriteMessage(ob.WebSocket, NewMessage(Data, s))
}
}
if err := WriteMessage(ws, NewMessage(Data, s)); err != nil {
log.Debugf("WebSocket写入失败即将退出循环...")
cancel()
}
buf = []byte{}
}
case data := <-dataChan:
if data != utf8.RuneError {
p := make([]byte, utf8.RuneLen(data))
utf8.EncodeRune(p, data)
buf = append(buf, p...)
} else {
buf = append(buf, []byte("@")...)
}
}
}
log.Debugf("SSH 连接已关闭,退出定时器循环。")
}()
//var enterKeys []rune
//enterIndex := 0
for {
_, message, err := ws.ReadMessage()
if err != nil {
// web socket会话关闭后主动关闭ssh会话
CloseSessionById(sessionId, Normal, "正常退出")
quitChan <- true
quitChan <- true
log.Debugf("WebSocket已关闭")
CloseSessionById(sessionId, Normal, "用户正常退出")
cancel()
break
}
var msg Message
err = json.Unmarshal(message, &msg)
msg, err := ParseMessage(string(message))
if err != nil {
log.Warnf("解析Json失败: %v, 原始字符串:%v", err, string(message))
log.Warnf("消息解码失败: %v, 原始字符串:%v", err, string(message))
continue
}
switch msg.Type {
case Resize:
var winSize WindowSize
err = json.Unmarshal([]byte(msg.Content), &winSize)
decodeString, err := base64.StdEncoding.DecodeString(msg.Content)
if err != nil {
log.Warnf("解析SSH会话窗口大小失败: %v", err)
log.Warnf("Base64解码失败: %v原始字符串%v", err, msg.Content)
continue
}
var winSize WindowSize
err = json.Unmarshal(decodeString, &winSize)
if err != nil {
log.Warnf("解析SSH会话窗口大小失败: %v原始字符串%v", err, msg.Content)
continue
}
if err := nextTerminal.WindowChange(winSize.Rows, winSize.Cols); err != nil {
log.Warnf("更改SSH会话窗口大小失败: %v", err)
continue
}
_ = sessionRepository.UpdateWindowSizeById(winSize.Rows, winSize.Cols, sessionId)
case Data:
_, err = nextTerminal.Write([]byte(msg.Content))
input := []byte(msg.Content)
//hexInput := hex.EncodeToString(input)
//switch hexInput {
//case "0d": // 回车
// DealCommand(enterKeys)
// // 清空输入的字符
// enterKeys = enterKeys[:0]
// enterIndex = 0
//case "7f": // backspace
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
// temp := enterKeys[:enterIndex]
// if len(enterKeys) > enterIndex {
// enterKeys = append(temp, enterKeys[enterIndex+1:]...)
// } else {
// enterKeys = temp
// }
//case "1b5b337e": // del
// temp := enterKeys[:enterIndex]
// if len(enterKeys) > enterIndex {
// enterKeys = append(temp, enterKeys[enterIndex+1:]...)
// } else {
// enterKeys = temp
// }
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
//case "1b5b41":
//case "1b5b42":
// break
//case "1b5b43": // ->
// enterIndex++
// if enterIndex > len(enterKeys) {
// enterIndex = len(enterKeys)
// }
//case "1b5b44": // <-
// enterIndex--
// if enterIndex < 0 {
// enterIndex = 0
// }
//default:
// enterKeys = utils.InsertSlice(enterIndex, []rune(msg.Content), enterKeys)
// enterIndex++
//}
_, err := nextTerminal.Write(input)
if err != nil {
log.Debugf("SSH会话写入失败: %v", err)
msg := Message{
Type: Closed,
Content: "the remote connection is closed.",
}
_ = WriteMessage(ws, msg)
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
}
case Ping:
_, _, err := nextTerminal.SshClient.Conn.SendRequest("helloworld1024@foxmail.com", true, nil)
if err != nil {
CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭")
} else {
_ = WriteMessage(ws, NewMessage(Ping, ""))
}
}
}
}
return err
}
func ReadMessage(nextTerminal *term.NextTerminal, quitChan chan bool, ws *websocket.Conn) {
func permissionCheck(c echo.Context, assetId string) error {
user, _ := GetCurrentAccount(c)
if constant.TypeUser == user.Type {
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
var quit bool
for {
select {
case quit = <-quitChan:
if quit {
return
}
default:
p, n, err := nextTerminal.Read()
if err != nil {
msg := Message{
Type: Closed,
Content: err.Error(),
}
_ = WriteMessage(ws, msg)
}
if n > 0 {
s := string(p)
msg := Message{
Type: Data,
Content: s,
}
_ = WriteMessage(ws, msg)
}
time.Sleep(time.Duration(10) * time.Millisecond)
if !utils.Contains(assetIds, assetId) {
return errors.New("您没有权限访问此资产")
}
}
return nil
}
func WriteMessage(ws *websocket.Conn, msg Message) error {
message, err := json.Marshal(msg)
if err != nil {
return err
}
WriteByteMessage(ws, message)
return err
}
func WriteByteMessage(ws *websocket.Conn, p []byte) {
err := ws.WriteMessage(websocket.TextMessage, p)
if err != nil {
log.Debugf("write: %v", err)
}
message := []byte(msg.ToString())
return ws.WriteMessage(websocket.TextMessage, message)
}
func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, error) {
@ -260,5 +376,46 @@ func CreateNextTerminalBySession(session model.Session) (*term.NextTerminal, err
ip = session.IP
port = session.Port
)
return term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, 10, 10, "")
return term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, 10, 10, "", "", false)
}
func SshMonitor(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
log.Errorf("升级为WebSocket协议失败%v", err.Error())
return err
}
defer ws.Close()
sessionId := c.QueryParam("sessionId")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "获取会话失败"))
}
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return WriteMessage(ws, NewMessage(Closed, "会话已离线"))
}
obId := utils.UUID()
obSession := &session.Session{
ID: obId,
Protocol: s.Protocol,
Mode: s.Mode,
WebSocket: ws,
}
nextSession.Observer.Add <- obSession
log.Debugf("会话 %v 观察者 %v 进入", sessionId, obId)
for {
_, _, err := ws.ReadMessage()
if err != nil {
log.Debugf("会话 %v 观察者 %v 退出", sessionId, obId)
nextSession.Observer.Del <- obId
break
}
}
return nil
}