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,11 +1,11 @@
package api
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
@ -13,14 +13,18 @@ import (
"strings"
"sync"
"next-terminal/pkg/constant"
"next-terminal/pkg/global"
"next-terminal/pkg/log"
"next-terminal/server/constant"
"next-terminal/server/global/session"
"next-terminal/server/guacd"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/service"
"next-terminal/server/utils"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
"gorm.io/gorm"
)
func SessionPagingEndpoint(c echo.Context) error {
@ -42,7 +46,7 @@ func SessionPagingEndpoint(c echo.Context) error {
if status == constant.Disconnected && len(items[i].Recording) > 0 {
var recording string
if items[i].Mode == constant.Naive {
if items[i].Mode == constant.Naive || items[i].Mode == constant.Terminal {
recording = items[i].Recording
} else {
recording = items[i].Recording + "/recording"
@ -78,14 +82,28 @@ func SessionDeleteEndpoint(c echo.Context) error {
func SessionConnectEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session := model.Session{}
session.ID = sessionId
session.Status = constant.Connected
session.ConnectedTime = utils.NowJsonTime()
s := model.Session{}
s.ID = sessionId
s.Status = constant.Connected
s.ConnectedTime = utils.NowJsonTime()
if err := sessionRepository.UpdateById(&session, sessionId); err != nil {
if err := sessionRepository.UpdateById(&s, sessionId); err != nil {
return err
}
o, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
asset, err := assetRepository.FindById(o.AssetId)
if err != nil {
return err
}
if !asset.Active {
asset.Active = true
_ = assetRepository.UpdateById(&asset, asset.ID)
}
return Success(c, nil)
}
@ -104,18 +122,48 @@ var mutex sync.Mutex
func CloseSessionById(sessionId string, code int, reason string) {
mutex.Lock()
defer mutex.Unlock()
observable, _ := global.Store.Get(sessionId)
if observable != nil {
log.Debugf("会话%v创建者退出,原因:%v", sessionId, reason)
observable.Subject.Close(code, reason)
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession != nil {
log.Debugf("[%v] 会话关闭,原因:%v", sessionId, reason)
WriteCloseMessage(nextSession.WebSocket, nextSession.Mode, code, reason)
for i := 0; i < len(observable.Observers); i++ {
observable.Observers[i].Close(code, reason)
log.Debugf("强制踢出会话%v的观察者", sessionId)
if nextSession.Observer != nil {
obs := nextSession.Observer.All()
for _, ob := range obs {
WriteCloseMessage(ob.WebSocket, ob.Mode, code, reason)
log.Debugf("[%v] 强制踢出会话的观察者: %v", sessionId, ob.ID)
}
}
}
global.Store.Del(sessionId)
session.GlobalSessionManager.Del <- sessionId
DisDBSess(sessionId, code, reason)
}
func WriteCloseMessage(ws *websocket.Conn, mode string, code int, reason string) {
switch mode {
case constant.Guacd:
if ws != nil {
err := guacd.NewInstruction("error", "", strconv.Itoa(code))
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.String()))
disconnect := guacd.NewInstruction("disconnect")
_ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String()))
}
case constant.Naive:
if ws != nil {
msg := `0` + reason
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
}
case constant.Terminal:
// 这里是关闭观察者的ssh会话
if ws != nil {
msg := `0` + reason
_ = ws.WriteMessage(websocket.TextMessage, []byte(msg))
}
}
}
func DisDBSess(sessionId string, code int, reason string) {
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return
@ -131,17 +179,17 @@ func CloseSessionById(sessionId string, code int, reason string) {
return
}
session := model.Session{}
session.ID = sessionId
session.Status = constant.Disconnected
session.DisconnectedTime = utils.NowJsonTime()
session.Code = code
session.Message = reason
session.Password = "-"
session.PrivateKey = "-"
session.Passphrase = "-"
ss := model.Session{}
ss.ID = sessionId
ss.Status = constant.Disconnected
ss.DisconnectedTime = utils.NowJsonTime()
ss.Code = code
ss.Message = reason
ss.Password = "-"
ss.PrivateKey = "-"
ss.Passphrase = "-"
_ = sessionRepository.UpdateById(&session, sessionId)
_ = sessionRepository.UpdateById(&ss, sessionId)
}
func SessionResizeEndpoint(c echo.Context) error {
@ -150,11 +198,10 @@ func SessionResizeEndpoint(c echo.Context) error {
sessionId := c.Param("id")
if len(width) == 0 || len(height) == 0 {
panic("参数异常")
return errors.New("参数异常")
}
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
if err := sessionRepository.UpdateWindowSizeById(intWidth, intHeight, sessionId); err != nil {
@ -175,37 +222,83 @@ func SessionCreateEndpoint(c echo.Context) error {
user, _ := GetCurrentAccount(c)
if constant.TypeUser == user.Type {
// 检测是否有访问权限
assetIds, err := resourceSharerRepository.FindAssetIdsByUserId(user.ID)
if err != nil {
return err
}
if !utils.Contains(assetIds, assetId) {
return errors.New("您没有权限访问此资产")
}
}
asset, err := assetRepository.FindById(assetId)
if err != nil {
return err
}
session := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: constant.NoConnect,
Creator: user.ID,
ClientIP: c.RealIP(),
Mode: mode,
var (
upload = "1"
download = "1"
_delete = "1"
rename = "1"
edit = "1"
fileSystem = "1"
)
if asset.Owner != user.ID && constant.TypeUser == user.Type {
// 普通用户访问非自己创建的资产需要校验权限
resourceSharers, err := resourceSharerRepository.FindByResourceIdAndUserId(assetId, user.ID)
if err != nil {
return err
}
if len(resourceSharers) == 0 {
return errors.New("您没有权限访问此资产")
}
strategyId := resourceSharers[0].StrategyId
if strategyId != "" {
strategy, err := strategyRepository.FindById(strategyId)
if err != nil {
if !errors.Is(gorm.ErrRecordNotFound, err) {
return err
}
} else {
upload = strategy.Upload
download = strategy.Download
_delete = strategy.Delete
rename = strategy.Rename
edit = strategy.Edit
}
}
}
var storageId = ""
if constant.RDP == asset.Protocol {
attr, err := assetRepository.FindAssetAttrMapByAssetId(assetId)
if err != nil {
return err
}
if "true" == attr[guacd.EnableDrive] {
fileSystem = "1"
storageId = attr[guacd.DrivePath]
if storageId == "" {
storageId = user.ID
}
} else {
fileSystem = "0"
}
}
s := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: constant.NoConnect,
Creator: user.ID,
ClientIP: c.RealIP(),
Mode: mode,
Upload: upload,
Download: download,
Delete: _delete,
Rename: rename,
Edit: edit,
StorageId: storageId,
AccessGatewayId: asset.AccessGatewayId,
}
if asset.AccountType == "credential" {
@ -215,28 +308,41 @@ func SessionCreateEndpoint(c echo.Context) error {
}
if credential.Type == constant.Custom {
session.Username = credential.Username
session.Password = credential.Password
s.Username = credential.Username
s.Password = credential.Password
} else {
session.Username = credential.Username
session.PrivateKey = credential.PrivateKey
session.Passphrase = credential.Passphrase
s.Username = credential.Username
s.PrivateKey = credential.PrivateKey
s.Passphrase = credential.Passphrase
}
}
if err := sessionRepository.Create(session); err != nil {
if err := sessionRepository.Create(s); err != nil {
return err
}
return Success(c, echo.Map{"id": session.ID})
return Success(c, echo.Map{
"id": s.ID,
"upload": s.Upload,
"download": s.Download,
"delete": s.Delete,
"rename": s.Rename,
"edit": s.Edit,
"storageId": s.StorageId,
"fileSystem": fileSystem,
})
}
func SessionUploadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
if s.Upload != "1" {
return errors.New("禁止操作")
}
file, err := c.FormFile("file")
if err != nil {
return err
@ -251,77 +357,94 @@ func SessionUploadEndpoint(c echo.Context) error {
remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename)
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
dstFile, err := tun.Subject.NextTerminal.SftpClient.Create(remoteFile)
sftpClient := nextSession.NextTerminal.SftpClient
// 文件夹不存在时自动创建文件夹
if _, err := sftpClient.Stat(remoteDir); os.IsNotExist(err) {
if err := sftpClient.MkdirAll(remoteDir); err != nil {
return err
}
}
dstFile, err := sftpClient.Create(remoteFile)
if err != nil {
return err
}
defer dstFile.Close()
buf := make([]byte, 1024)
for {
n, err := src.Read(buf)
if err != nil {
if err != io.EOF {
log.Warnf("文件上传错误 %v", err)
} else {
break
}
}
_, _ = dstFile.Write(buf[:n])
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
if strings.Contains(remoteFile, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
// Destination
dst, err := os.Create(path.Join(drivePath, remoteFile))
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
if _, err = io.Copy(dstFile, src); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageUpload(c, file, s.StorageId)
}
return err
}
func SessionDownloadEndpoint(c echo.Context) error {
func SessionEditEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
//remoteDir := c.Query("dir")
if s.Edit != "1" {
return errors.New("禁止操作")
}
file := c.FormValue("file")
fileContent := c.FormValue("fileContent")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sftpClient := nextSession.NextTerminal.SftpClient
dstFile, err := sftpClient.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return err
}
defer dstFile.Close()
write := bufio.NewWriter(dstFile)
if _, err := write.WriteString(fileContent); err != nil {
return err
}
if err := write.Flush(); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageEdit(c, file, fileContent, s.StorageId)
}
return err
}
func SessionDownloadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
if s.Download != "1" {
return errors.New("禁止操作")
}
remoteFile := c.QueryParam("file")
// 获取带后缀的文件名称
filenameWithSuffix := path.Base(remoteFile)
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
dstFile, err := tun.Subject.NextTerminal.SftpClient.Open(remoteFile)
dstFile, err := nextSession.NextTerminal.SftpClient.Open(remoteFile)
if err != nil {
return err
}
@ -335,105 +458,51 @@ func SessionDownloadEndpoint(c echo.Context) error {
}
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
} else if "rdp" == session.Protocol {
if strings.Contains(remoteFile, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
return c.Attachment(path.Join(drivePath, remoteFile), filenameWithSuffix)
} else if "rdp" == s.Protocol {
storageId := s.StorageId
return StorageDownload(c, remoteFile, storageId)
}
return err
}
type File struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Mode string `json:"mode"`
IsLink bool `json:"isLink"`
ModTime utils.JsonTime `json:"modTime"`
Size int64 `json:"size"`
}
func SessionLsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindByIdAndDecrypt(sessionId)
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
remoteDir := c.FormValue("dir")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
if tun.Subject.NextTerminal == nil {
nextTerminal, err := CreateNextTerminalBySession(session)
if err != nil {
return err
}
tun.Subject.NextTerminal = nextTerminal
}
if tun.Subject.NextTerminal.SftpClient == nil {
sftpClient, err := sftp.NewClient(tun.Subject.NextTerminal.SshClient)
if nextSession.NextTerminal.SftpClient == nil {
sftpClient, err := sftp.NewClient(nextSession.NextTerminal.SshClient)
if err != nil {
log.Errorf("创建sftp客户端失败%v", err.Error())
return err
}
tun.Subject.NextTerminal.SftpClient = sftpClient
nextSession.NextTerminal.SftpClient = sftpClient
}
fileInfos, err := tun.Subject.NextTerminal.SftpClient.ReadDir(remoteDir)
fileInfos, err := nextSession.NextTerminal.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
var files = make([]File, 0)
var files = make([]service.File, 0)
for i := range fileInfos {
// 忽略因此文件
// 忽略隐藏文件
if strings.HasPrefix(fileInfos[i].Name(), ".") {
continue
}
file := File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
Mode: fileInfos[i].Mode().String(),
IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink,
ModTime: utils.NewJsonTime(fileInfos[i].ModTime()),
Size: fileInfos[i].Size(),
}
files = append(files, file)
}
return Success(c, files)
} else if "rdp" == session.Protocol {
if strings.Contains(remoteDir, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
fileInfos, err := ioutil.ReadDir(path.Join(drivePath, remoteDir))
if err != nil {
return err
}
var files = make([]File, 0)
for i := range fileInfos {
file := File{
file := service.File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
@ -447,115 +516,87 @@ func SessionLsEndpoint(c echo.Context) error {
}
return Success(c, files)
} else if "rdp" == s.Protocol {
storageId := s.StorageId
return StorageLs(c, remoteDir, storageId)
}
return errors.New("当前协议不支持此操作")
}
func SafetyRuleTrigger(c echo.Context) {
log.Warnf("IP %v 尝试进行攻击请ban掉此IP", c.RealIP())
security := model.AccessSecurity{
ID: utils.UUID(),
Source: "安全规则触发",
IP: c.RealIP(),
Rule: constant.AccessRuleReject,
}
_ = accessSecurityRepository.Create(&security)
}
func SessionMkDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
if err := tun.Subject.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
if strings.Contains(remoteDir, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.MkdirAll(path.Join(drivePath, remoteDir), os.ModePerm); err != nil {
return err
}
return Success(c, nil)
if s.Upload != "1" {
return errors.New("禁止操作")
}
remoteDir := c.QueryParam("dir")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
if err := nextSession.NextTerminal.SftpClient.Mkdir(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageMkDir(c, remoteDir, s.StorageId)
}
return errors.New("当前协议不支持此操作")
}
func SessionRmEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
key := c.QueryParam("key")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
if s.Delete != "1" {
return errors.New("禁止操作")
}
// 文件夹或者文件
file := c.FormValue("file")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sftpClient := tun.Subject.NextTerminal.SftpClient
sftpClient := nextSession.NextTerminal.SftpClient
stat, err := sftpClient.Stat(key)
stat, err := sftpClient.Stat(file)
if err != nil {
return err
}
if stat.IsDir() {
fileInfos, err := sftpClient.ReadDir(key)
fileInfos, err := sftpClient.ReadDir(file)
if err != nil {
return err
}
for i := range fileInfos {
if err := sftpClient.Remove(path.Join(key, fileInfos[i].Name())); err != nil {
if err := sftpClient.Remove(path.Join(file, fileInfos[i].Name())); err != nil {
return err
}
}
if err := sftpClient.RemoveDirectory(key); err != nil {
if err := sftpClient.RemoveDirectory(file); err != nil {
return err
}
} else {
if err := sftpClient.Remove(key); err != nil {
if err := sftpClient.Remove(file); err != nil {
return err
}
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
if strings.Contains(key, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.RemoveAll(path.Join(drivePath, key)); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageRm(c, file, s.StorageId)
}
return errors.New("当前协议不支持此操作")
@ -563,58 +604,80 @@ func SessionRmEndpoint(c echo.Context) error {
func SessionRenameEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
if s.Rename != "1" {
return errors.New("禁止操作")
}
oldName := c.QueryParam("oldName")
newName := c.QueryParam("newName")
if "ssh" == session.Protocol {
tun, ok := global.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
if "ssh" == s.Protocol {
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sftpClient := tun.Subject.NextTerminal.SftpClient
sftpClient := nextSession.NextTerminal.SftpClient
if err := sftpClient.Rename(oldName, newName); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
if strings.Contains(oldName, "../") {
SafetyRuleTrigger(c)
return Fail(c, -1, ":) 您的IP已被记录请去向管理员自首。")
}
drivePath, err := propertyRepository.GetDrivePath()
if err != nil {
return err
}
if err := os.Rename(path.Join(drivePath, oldName), path.Join(drivePath, newName)); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == s.Protocol {
return StorageRename(c, oldName, newName, s.StorageId)
}
return errors.New("当前协议不支持此操作")
}
func SessionRecordingEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := sessionRepository.FindById(sessionId)
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
var recording string
if session.Mode == constant.Naive {
recording = session.Recording
if s.Mode == constant.Naive || s.Mode == constant.Terminal {
recording = s.Recording
} else {
recording = session.Recording + "/recording"
recording = s.Recording + "/recording"
}
log.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording))
return c.File(recording)
}
func SessionGetEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindById(sessionId)
if err != nil {
return err
}
return Success(c, s)
}
func SessionStatsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
s, err := sessionRepository.FindByIdAndDecrypt(sessionId)
if err != nil {
return err
}
if "ssh" != s.Protocol {
return Fail(c, -1, "不支持当前协议")
}
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession == nil {
return errors.New("获取会话失败")
}
sshClient := nextSession.NextTerminal.SshClient
stats, err := GetAllStats(sshClient)
if err != nil {
return err
}
return Success(c, stats)
}