Initial commit

This commit is contained in:
dushixiang
2020-12-20 21:19:11 +08:00
commit e7f2773c77
77 changed files with 27866 additions and 0 deletions

52
pkg/api/account.go Normal file
View File

@ -0,0 +1,52 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"time"
)
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
}
func LoginEndpoint(c echo.Context) error {
var loginAccount LoginAccount
if err := c.Bind(&loginAccount); err != nil {
return err
}
user, err := model.FindUserByUsername(loginAccount.Username)
if err != nil {
return err
}
if err := utils.Encoder.Match([]byte(user.Password), []byte(loginAccount.Password)); err != nil {
return err
}
token := utils.UUID()
config.Cache.Set(token, user, time.Minute*time.Duration(30))
model.UpdateUserById(&model.User{Online: true}, user.ID)
return Success(c, token)
}
func LogoutEndpoint(c echo.Context) error {
token := GetToken(c)
config.Cache.Delete(token)
return Success(c, nil)
}
func ChangePasswordEndpoint(c echo.Context) error {
return nil
}
func InfoEndpoint(c echo.Context) error {
account, _ := GetCurrentAccount(c)
return Success(c, account)
}

96
pkg/api/asset.go Normal file
View File

@ -0,0 +1,96 @@
package api
import (
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"github.com/labstack/echo/v4"
"strconv"
"strings"
)
func AssetCreateEndpoint(c echo.Context) error {
var item model.Asset
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewAsset(&item); err != nil {
return err
}
return Success(c, item)
}
func AssetPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
protocol := c.QueryParam("protocol")
items, total, _ := model.FindPageAsset(pageIndex, pageSize, name, protocol)
return Success(c, H{
"total": total,
"items": items,
})
}
func AssetAllEndpoint(c echo.Context) error {
protocol := c.QueryParam("protocol")
items, _ := model.FindAssetByConditions(protocol)
return Success(c, items)
}
func AssetUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Asset
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateAssetById(&item, id)
return Success(c, nil)
}
func AssetDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteAssetById(split[i])
}
return Success(c, nil)
}
func AssetGetEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Asset
if item, err = model.FindAssetById(id); err != nil {
return err
}
return Success(c, item)
}
func AssetTcpingEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Asset
if item, err = model.FindAssetById(id); err != nil {
return err
}
active := utils.Tcping(item.IP, item.Port)
asset := model.Asset{
Active: active,
}
model.UpdateAssetById(&asset, item.ID)
return Success(c, active)
}

70
pkg/api/command.go Normal file
View File

@ -0,0 +1,70 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func CommandCreateEndpoint(c echo.Context) error {
var item model.Command
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewCommand(&item); err != nil {
return err
}
return Success(c, item)
}
func CommandPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
content := c.QueryParam("content")
items, total, _ := model.FindPageCommand(pageIndex, pageSize, name, content)
return Success(c, H{
"total": total,
"items": items,
})
}
func CommandUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Command
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateCommandById(&item, id)
return Success(c, nil)
}
func CommandDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteCommandById(split[i])
}
return Success(c, nil)
}
func CommandGetEndpoint(c echo.Context) (err error) {
id := c.Param("id")
var item model.Command
if item, err = model.FindCommandById(id); err != nil {
return err
}
return Success(c, item)
}

72
pkg/api/credential.go Normal file
View File

@ -0,0 +1,72 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func CredentialAllEndpoint(c echo.Context) error {
items, _ := model.FindAllCredential()
return Success(c, items)
}
func CredentialCreateEndpoint(c echo.Context) error {
var item model.Credential
if err := c.Bind(&item); err != nil {
return err
}
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewCredential(&item); err != nil {
return err
}
return Success(c, item)
}
func CredentialPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
name := c.QueryParam("name")
items, total, _ := model.FindPageCredential(pageIndex, pageSize, name)
return Success(c, H{
"total": total,
"items": items,
})
}
func CredentialUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.Credential
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateCredentialById(&item, id)
return Success(c, nil)
}
func CredentialDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteCredentialById(split[i])
}
return Success(c, nil)
}
func CredentialGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, _ := model.FindCredentialById(id)
return Success(c, item)
}

34
pkg/api/middleware.go Normal file
View File

@ -0,0 +1,34 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/config"
"strings"
"time"
)
func Auth(next echo.HandlerFunc) echo.HandlerFunc {
urls := []string{"download", "login"}
return func(c echo.Context) error {
// 路由拦截 - 登录身份、资源权限判断等
for i := range urls {
if c.Request().RequestURI == "/" || strings.HasPrefix(c.Request().RequestURI, "/#") {
return next(c)
}
if strings.Contains(c.Request().RequestURI, urls[i]) {
return next(c)
}
}
token := GetToken(c)
user, found := config.Cache.Get(token)
if !found {
c.Logger().Error("您的登录信息已失效,请重新登录后再试。")
return Fail(c, 403, "您的登录信息已失效,请重新登录后再试。")
}
config.Cache.Set(token, user, time.Minute*time.Duration(30))
return next(c)
}
}

98
pkg/api/overview.go Normal file
View File

@ -0,0 +1,98 @@
package api
import (
"next-terminal/pkg/model"
"fmt"
"github.com/labstack/echo/v4"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
"time"
)
type OverviewStatus struct {
Load Load `json:"load"`
Memory Memory `json:"memory"`
CPU CPU `json:"cpu"`
}
type Load struct {
Load1 float64 `json:"load1"`
Load5 float64 `json:"load5"`
Load15 float64 `json:"load15"`
}
type Memory struct {
Total uint64 `json:"total"`
Available uint64 `json:"available"`
UsedPercent float64 `json:"usedPercent"`
Used uint64 `json:"used"`
}
type CPU struct {
PhysicalCount int `json:"physicalCount"`
LogicalCount int `json:"logicalCount"`
Percent float64 `json:"percent"`
ModelName string `json:"modelName"`
}
type Counter struct {
User int64 `json:"user"`
Asset int64 `json:"asset"`
Credential int64 `json:"credential"`
OnlineSession int64 `json:"onlineSession"`
}
func OverviewStatusEndPoint(c echo.Context) error {
info, _ := load.Avg()
memory, _ := mem.VirtualMemory()
infoStats, _ := cpu.Info()
physicalCount, _ := cpu.Counts(false)
logicalCount, _ := cpu.Counts(true)
cps, _ := cpu.Percent(time.Second, false)
fmt.Printf("%+v\n", info)
fmt.Printf("%+v\n", memory)
fmt.Printf("%+v\n", infoStats)
fmt.Printf("%+v\n", physicalCount)
fmt.Printf("%+v\n", logicalCount)
fmt.Printf("%+v\n", cps)
overviewStatus := OverviewStatus{
Load: Load{
Load1: info.Load1,
Load5: info.Load5,
Load15: info.Load15,
},
Memory: Memory{
Total: memory.Total,
Available: memory.Available,
UsedPercent: memory.UsedPercent,
Used: memory.Used,
},
CPU: CPU{
PhysicalCount: physicalCount,
LogicalCount: logicalCount,
Percent: cps[0],
ModelName: infoStats[0].ModelName,
},
}
return Success(c, overviewStatus)
}
func OverviewCounterEndPoint(c echo.Context) error {
countUser, _ := model.CountUser()
countOnlineSession, _ := model.CountOnlineSession()
credential, _ := model.CountCredential()
asset, _ := model.CountAsset()
counter := Counter{
User: countUser,
OnlineSession: countOnlineSession,
Credential: credential,
Asset: asset,
}
return Success(c, counter)
}

29
pkg/api/property.go Normal file
View File

@ -0,0 +1,29 @@
package api
import (
"next-terminal/pkg/model"
"fmt"
"github.com/labstack/echo/v4"
)
func PropertyGetEndpoint(c echo.Context) error {
properties := model.FindAllProperties()
return Success(c, properties)
}
func PropertyUpdateEndpoint(c echo.Context) error {
var item map[string]interface{}
if err := c.Bind(&item); err != nil {
return err
}
for key := range item {
value := fmt.Sprintf("%v", item[key])
property := model.Property{
Name: key,
Value: value,
}
model.UpdatePropertyByName(&property, key)
}
return Success(c, nil)
}

145
pkg/api/routes.go Normal file
View File

@ -0,0 +1,145 @@
package api
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
)
const Token = "X-Auth-Token"
func SetupRoutes() *echo.Echo {
// Echo instance
e := echo.New()
e.File("/", "web/build/index.html")
e.File("/logo.svg", "web/build/logo.svg")
e.File("/favicon.ico", "web/build/favicon.ico")
e.Static("/static", "web/build/static")
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
Skipper: middleware.DefaultSkipper,
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
e.Use(Auth)
e.POST("/login", LoginEndpoint)
e.GET("/tunnel", TunEndpoint)
e.GET("/ssh", SSHEndpoint)
e.POST("/logout", LogoutEndpoint)
e.POST("/change-password", ChangePasswordEndpoint)
e.GET("/info", InfoEndpoint)
users := e.Group("/users")
{
users.POST("", UserCreateEndpoint)
users.GET("/paging", UserPagingEndpoint)
users.PUT("/:id", UserUpdateEndpoint)
users.DELETE("/:id", UserDeleteEndpoint)
users.GET("/:id", UserGetEndpoint)
}
assets := e.Group("/assets", Auth)
{
assets.GET("", AssetAllEndpoint)
assets.POST("", AssetCreateEndpoint)
assets.GET("/paging", AssetPagingEndpoint)
assets.POST("/:id/tcping", AssetTcpingEndpoint)
assets.PUT("/:id", AssetUpdateEndpoint)
assets.DELETE("/:id", AssetDeleteEndpoint)
assets.GET("/:id", AssetGetEndpoint)
}
commands := e.Group("/commands")
{
commands.GET("/paging", CommandPagingEndpoint)
commands.POST("", CommandCreateEndpoint)
commands.PUT("/:id", CommandUpdateEndpoint)
commands.DELETE("/:id", CommandDeleteEndpoint)
commands.GET("/:id", CommandGetEndpoint)
}
credentials := e.Group("/credentials")
{
credentials.GET("", CredentialAllEndpoint)
credentials.GET("/paging", CredentialPagingEndpoint)
credentials.POST("", CredentialCreateEndpoint)
credentials.PUT("/:id", CredentialUpdateEndpoint)
credentials.DELETE("/:id", CredentialDeleteEndpoint)
credentials.GET("/:id", CredentialGetEndpoint)
}
sessions := e.Group("/sessions")
{
sessions.POST("", SessionCreateEndpoint)
sessions.GET("/paging", SessionPagingEndpoint)
sessions.POST("/:id/content", SessionContentEndpoint)
sessions.POST("/:id/discontent", SessionDiscontentEndpoint)
sessions.POST("/:id/resize", SessionResizeEndpoint)
sessions.POST("/:id/upload", SessionUploadEndpoint)
sessions.GET("/:id/download", SessionDownloadEndpoint)
sessions.GET("/:id/ls", SessionLsEndpoint)
sessions.POST("/:id/mkdir", SessionMkDirEndpoint)
sessions.DELETE("/:id/rmdir", SessionRmDirEndpoint)
sessions.DELETE("/:id/rm", SessionRmEndpoint)
sessions.DELETE("/:id", SessionDeleteEndpoint)
}
e.GET("/properties", PropertyGetEndpoint)
e.PUT("/properties", PropertyUpdateEndpoint)
e.GET("/overview/status", OverviewStatusEndPoint)
e.GET("/overview/counter", OverviewCounterEndPoint)
return e
}
type H map[string]interface{}
func Fail(c echo.Context, code int, message string) error {
return c.JSON(200, H{
"code": code,
"message": message,
})
}
func Success(c echo.Context, data interface{}) error {
return c.JSON(200, H{
"code": 1,
"message": "success",
"data": data,
})
}
func NotFound(c echo.Context, message string) error {
return c.JSON(200, H{
"code": -1,
"message": message,
})
}
func GetToken(c echo.Context) string {
token := c.Request().Header.Get(Token)
if len(token) > 0 {
return token
}
return c.QueryParam(Token)
}
func GetCurrentAccount(c echo.Context) (model.User, bool) {
token := GetToken(c)
get, b := config.Cache.Get(token)
if b {
return get.(model.User), true
}
return model.User{}, false
}

430
pkg/api/session.go Normal file
View File

@ -0,0 +1,430 @@
package api
import (
"bytes"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"io"
"io/ioutil"
"net/http"
"next-terminal/pkg/config"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"os"
"path"
"strconv"
"strings"
)
func SessionPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
status := c.QueryParam("status")
userId := c.QueryParam("userId")
clientIp := c.QueryParam("clientIp")
assetId := c.QueryParam("assetId")
protocol := c.QueryParam("protocol")
items, total, err := model.FindPageSession(pageIndex, pageSize, status, userId, clientIp, assetId, protocol)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func SessionDeleteEndpoint(c echo.Context) error {
sessionIds := c.Param("id")
split := strings.Split(sessionIds, ",")
for i := range split {
model.DeleteSessionById(split[i])
drivePath, err := model.GetDrivePath()
if err != nil {
continue
}
_ = os.Remove(path.Join(drivePath, split[i]))
}
return Success(c, nil)
}
func SessionContentEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session := model.Session{}
session.ID = sessionId
session.Status = model.Connected
session.ConnectedTime = utils.NowJsonTime()
model.UpdateSessionById(&session, sessionId)
return Success(c, nil)
}
func SessionDiscontentEndpoint(c echo.Context) error {
sessionIds := c.Param("id")
split := strings.Split(sessionIds, ",")
for i := range split {
tun, ok := config.Store.Get(split[i])
if ok {
CloseSession(split[i], tun)
}
}
return Success(c, nil)
}
func CloseSession(sessionId string, tun config.Tun) {
_ = tun.Tun.Close()
config.Store.Del(sessionId)
session := model.Session{}
session.ID = sessionId
session.Status = model.Disconnected
session.DisconnectedTime = utils.NowJsonTime()
model.UpdateSessionById(&session, sessionId)
}
func SessionResizeEndpoint(c echo.Context) error {
width := c.QueryParam("width")
height := c.QueryParam("height")
sessionId := c.Param("id")
if len(width) == 0 || len(height) == 0 {
panic("参数异常")
}
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
session := model.Session{}
session.ID = sessionId
session.Width = intWidth
session.Height = intHeight
model.UpdateSessionById(&session, sessionId)
return Success(c, session)
}
func SessionCreateEndpoint(c echo.Context) error {
assetId := c.QueryParam("assetId")
user, _ := GetCurrentAccount(c)
asset, err := model.FindAssetById(assetId)
if err != nil {
return err
}
session := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: model.NoConnect,
Creator: user.ID,
ClientIP: c.RealIP(),
}
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return err
}
session.Username = credential.Username
session.Password = credential.Password
}
if err := model.CreateNewSession(session); err != nil {
return err
}
return Success(c, session)
}
func SessionUploadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
file, err := c.FormFile("file")
if err != nil {
return err
}
filename := file.Filename
src, err := file.Open()
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename)
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
dstFile, err := tun.SftpClient.Create(remoteFile)
if err != nil {
return err
}
defer dstFile.Close()
buf := make([]byte, 1024)
for {
n, _ := src.Read(buf)
if n == 0 {
break
}
_, _ = dstFile.Write(buf)
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.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 {
return err
}
return Success(c, nil)
}
return err
}
func SessionDownloadEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
//remoteDir := c.Query("dir")
remoteFile := c.QueryParam("file")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
dstFile, err := tun.SftpClient.Open(remoteFile)
if err != nil {
return err
}
defer dstFile.Close()
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", remoteFile))
var buff bytes.Buffer
if _, err := dstFile.WriteTo(&buff); err != nil {
return err
}
return c.Stream(http.StatusOK, echo.MIMEOctetStream, bytes.NewReader(buff.Bytes()))
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
return c.File(path.Join(drivePath, remoteFile))
}
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"`
}
func SessionLsEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
fileInfos, err := tun.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
var files = make([]File, 0)
for i := range fileInfos {
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,
}
files = append(files, file)
}
return Success(c, files)
} else if "rdp" == session.Protocol {
drivePath, err := model.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{
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,
}
files = append(files, file)
}
return Success(c, files)
}
return err
}
func SessionMkDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
if err := tun.SftpClient.Mkdir(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.MkdirAll(path.Join(drivePath, remoteDir), os.ModePerm); err != nil {
return err
}
return Success(c, nil)
}
return nil
}
func SessionRmDirEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
fileInfos, err := tun.SftpClient.ReadDir(remoteDir)
if err != nil {
return err
}
for i := range fileInfos {
if err := tun.SftpClient.Remove(path.Join(remoteDir, fileInfos[i].Name())); err != nil {
return err
}
}
if err := tun.SftpClient.RemoveDirectory(remoteDir); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.RemoveAll(path.Join(drivePath, remoteDir)); err != nil {
return err
}
return Success(c, nil)
}
return nil
}
func SessionRmEndpoint(c echo.Context) error {
sessionId := c.Param("id")
session, err := model.FindSessionById(sessionId)
if err != nil {
return err
}
remoteFile := c.QueryParam("file")
if "ssh" == session.Protocol {
tun, ok := config.Store.Get(sessionId)
if !ok {
return errors.New("获取sftp客户端失败")
}
if err := tun.SftpClient.Remove(remoteFile); err != nil {
return err
}
return Success(c, nil)
} else if "rdp" == session.Protocol {
drivePath, err := model.GetDrivePath()
if err != nil {
return err
}
if err := os.Remove(path.Join(drivePath, remoteFile)); err != nil {
return err
}
return Success(c, nil)
}
return nil
}

175
pkg/api/ssh.go Normal file
View File

@ -0,0 +1,175 @@
package api
import (
"bytes"
"next-terminal/pkg/model"
"fmt"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
"log"
"net"
"net/http"
"strconv"
"sync"
"time"
)
var UpGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"guacamole"},
}
type NextWriter struct {
b bytes.Buffer
mu sync.Mutex
}
func (w *NextWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.b.Write(p)
}
func (w *NextWriter) Read() ([]byte, int, error) {
w.mu.Lock()
defer w.mu.Unlock()
p := w.b.Bytes()
buf := make([]byte, len(p))
read, err := w.b.Read(buf)
w.b.Reset()
if err != nil {
return nil, 0, err
}
return buf, read, err
}
func SSHEndpoint(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
return err
}
assetId := c.QueryParam("assetId")
width, _ := strconv.Atoi(c.QueryParam("width"))
height, _ := strconv.Atoi(c.QueryParam("height"))
asset, err := model.FindAssetById(assetId)
if err != nil {
return err
}
if asset.AccountType == "credential" {
credential, err := model.FindCredentialById(asset.CredentialId)
if err != nil {
return err
}
asset.Username = credential.Username
asset.Password = credential.Password
}
config := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: asset.Username,
Auth: []ssh.AuthMethod{ssh.Password(asset.Password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", asset.IP, asset.Port)
sshClient, err := ssh.Dial("tcp", addr, config)
if err != nil {
return err
}
session, err := sshClient.NewSession()
if err != nil {
return err
}
defer session.Close()
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err := session.RequestPty("xterm", height, width, modes); err != nil {
return err
}
var b NextWriter
session.Stdout = &b
session.Stderr = &b
stdinPipe, err := session.StdinPipe()
if err != nil {
return err
}
if err := session.Shell(); err != nil {
return err
}
go func() {
for true {
p, n, err := b.Read()
if err != nil {
continue
}
if n > 0 {
WriteByteMessage(ws, p)
}
time.Sleep(time.Duration(100) * time.Millisecond)
}
}()
for true {
_, message, err := ws.ReadMessage()
if err != nil {
continue
}
_, err = stdinPipe.Write(message)
if err != nil {
log.Println("Tunnel write:", err)
}
}
return err
}
func WriteMessage(ws *websocket.Conn, message string) {
WriteByteMessage(ws, []byte(message))
}
func WriteByteMessage(ws *websocket.Conn, p []byte) {
err := ws.WriteMessage(websocket.TextMessage, p)
if err != nil {
log.Println("write:", err)
}
}
func CreateSftpClient(username, password, ip string, port int) (sftpClient *sftp.Client, err error) {
clientConfig := &ssh.ClientConfig{
Timeout: 1 * time.Second,
User: username,
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr := fmt.Sprintf("%s:%d", ip, port)
sshClient, err := ssh.Dial("tcp", addr, clientConfig)
if err != nil {
return nil, err
}
return sftp.NewClient(sshClient)
}

33
pkg/api/test/ps.go Normal file
View File

@ -0,0 +1,33 @@
package main
func main() {
//v, _ := mem.VirtualMemory()
//c, _ := cpu.Info()
//cc, _ := cpu.Percent(time.Second, false)
//d, _ := disk.Usage("/")
//n, _ := host.Info()
//nv, _ := net.IOCounters(true)
//boottime, _ := host.BootTime()
//btime := time.Unix(int64(boottime), 0).Format("2006-01-02 15:04:05")
//
//fmt.Printf(" Mem : %v MB Free: %v MB Used:%v Usage:%f%%\n", v.Total/1024/1024, v.Available/1024/1024, v.Used/1024/1024, v.UsedPercent)
//if len(c) > 1 {
// for _, sub_cpu := range c {
// modelname := sub_cpu.ModelName
// cores := sub_cpu.Cores
// fmt.Printf(" CPU : %v %v cores \n", modelname, cores)
// }
//} else {
// sub_cpu := c[0]
// modelname := sub_cpu.ModelName
// cores := sub_cpu.Cores
// fmt.Printf(" CPU : %v %v cores \n", modelname, cores)
//
//}
//fmt.Printf(" Network: %v bytes / %v bytes\n", nv[0].BytesRecv, nv[0].BytesSent)
//fmt.Printf(" SystemBoot:%v\n", btime)
//fmt.Printf(" CPU Used : used %f%% \n", cc[0])
//fmt.Printf(" HD : %v GB Free: %v GB Usage:%f%%\n", d.Total/1024/1024/1024, d.Free/1024/1024/1024, d.UsedPercent)
//fmt.Printf(" OS : %v(%v) %v \n", n.Platform, n.PlatformFamily, n.PlatformVersion)
//fmt.Printf(" Hostname : %v \n", n.Hostname)
}

158
pkg/api/tunnel.go Normal file
View File

@ -0,0 +1,158 @@
package api
import (
"next-terminal/pkg/config"
"next-terminal/pkg/guacd"
"next-terminal/pkg/model"
"fmt"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/pkg/sftp"
"log"
"strconv"
)
func TunEndpoint(c echo.Context) error {
ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil)
if err != nil {
return err
}
width := c.QueryParam("width")
height := c.QueryParam("height")
sessionId := c.QueryParam("sessionId")
connectionId := c.QueryParam("connectionId")
intWidth, _ := strconv.Atoi(width)
intHeight, _ := strconv.Atoi(height)
configuration := guacd.NewConfiguration()
configuration.SetParameter("width", width)
configuration.SetParameter("height", height)
propertyMap := model.FindAllPropertiesMap()
for name := range propertyMap {
if name == model.GuacdFontSize {
fontSize, _ := strconv.Atoi(propertyMap[name])
fontSize = fontSize * 2
configuration.SetParameter(name, strconv.Itoa(fontSize))
} else {
configuration.SetParameter(name, propertyMap[name])
}
}
var session model.Session
var sftpClient *sftp.Client
if len(connectionId) > 0 {
session, err = model.FindSessionByConnectionId(connectionId)
if err != nil {
return err
}
configuration.ConnectionID = connectionId
} else {
session, err = model.FindSessionById(sessionId)
if err != nil {
return err
}
configuration.Protocol = session.Protocol
switch configuration.Protocol {
case "rdp":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("security", "any")
configuration.SetParameter("ignore-cert", "true")
configuration.SetParameter("create-drive-path", "true")
configuration.SetParameter("dpi", "96")
configuration.SetParameter("resize-method", "reconnect")
configuration.SetParameter("enable-sftp", "")
break
case "ssh":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
sftpClient, err = CreateSftpClient(session.Username, session.Password, session.IP, session.Port)
if err != nil {
return err
}
break
case "vnc":
configuration.SetParameter("password", session.Password)
configuration.SetParameter("enable-sftp", "")
break
case "telnet":
configuration.SetParameter("username", session.Username)
configuration.SetParameter("password", session.Password)
configuration.SetParameter("enable-sftp", "")
break
}
configuration.SetParameter("hostname", session.IP)
configuration.SetParameter("port", strconv.Itoa(session.Port))
}
addr := propertyMap[model.GuacdHost] + ":" + propertyMap[model.GuacdPort]
tunnel, err := guacd.NewTunnel(addr, configuration)
if err != nil {
return err
}
fmt.Printf("=====================================================\n")
fmt.Printf("connect to %v with config: %+v\n", addr, configuration)
fmt.Printf("=====================================================\n")
tun := config.Tun{
Tun: tunnel,
SftpClient: sftpClient,
}
config.Store.Set(sessionId, tun)
if len(session.ConnectionId) == 0 {
session.ConnectionId = tunnel.UUID
session.Width = intWidth
session.Height = intHeight
model.UpdateSessionById(&session, sessionId)
}
go func() {
for true {
instruction, err := tunnel.Read()
if err != nil {
CloseSession(sessionId, tun)
log.Printf("WS读取异常: %v", err)
break
}
//fmt.Printf("<= %v \n", string(instruction))
err = ws.WriteMessage(websocket.TextMessage, instruction)
if err != nil {
CloseSession(sessionId, tun)
log.Printf("WS写入异常: %v", err)
break
}
}
}()
for true {
_, message, err := ws.ReadMessage()
if err != nil {
CloseSession(sessionId, tun)
log.Printf("Tunnel读取异常: %v", err)
break
}
_, err = tunnel.WriteAndFlush(message)
if err != nil {
CloseSession(sessionId, tun)
log.Printf("Tunnel写入异常: %v", err)
break
}
}
return err
}

83
pkg/api/user.go Normal file
View File

@ -0,0 +1,83 @@
package api
import (
"github.com/labstack/echo/v4"
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"strconv"
"strings"
)
func UserCreateEndpoint(c echo.Context) error {
var item model.User
if err := c.Bind(&item); err != nil {
return err
}
var pass []byte
var err error
if pass, err = utils.Encoder.Encode([]byte("admin")); err != nil {
return err
}
item.Password = string(pass)
item.ID = utils.UUID()
item.Created = utils.NowJsonTime()
if err := model.CreateNewUser(&item); err != nil {
return err
}
return Success(c, item)
}
func UserPagingEndpoint(c echo.Context) error {
pageIndex, _ := strconv.Atoi(c.QueryParam("pageIndex"))
pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))
username := c.QueryParam("username")
nickname := c.QueryParam("nickname")
items, total, err := model.FindPageUser(pageIndex, pageSize, username, nickname)
if err != nil {
return err
}
return Success(c, H{
"total": total,
"items": items,
})
}
func UserUpdateEndpoint(c echo.Context) error {
id := c.Param("id")
var item model.User
if err := c.Bind(&item); err != nil {
return err
}
model.UpdateUserById(&item, id)
return Success(c, nil)
}
func UserDeleteEndpoint(c echo.Context) error {
id := c.Param("id")
split := strings.Split(id, ",")
for i := range split {
model.DeleteUserById(split[i])
}
return Success(c, nil)
}
func UserGetEndpoint(c echo.Context) error {
id := c.Param("id")
item, err := model.FindUserById(id)
if err != nil {
return err
}
return Success(c, item)
}

14
pkg/config/config.go Normal file
View File

@ -0,0 +1,14 @@
package config
import (
"github.com/patrickmn/go-cache"
"gorm.io/gorm"
)
var DB *gorm.DB
var Cache *cache.Cache
var NextTerminal *NextTerminalConfig
var Store *TunStore

View File

@ -0,0 +1,31 @@
package config
import (
"github.com/spf13/viper"
"log"
)
type NextTerminalConfig struct {
Dsn string
Addr string
}
func SetupConfig() *NextTerminalConfig {
viper.SetConfigName("next-terminal")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/next-terminal/")
viper.AddConfigPath("$HOME/.next-terminal")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatal(err)
}
var config = &NextTerminalConfig{
Dsn: viper.GetString("next-terminal.dsn"),
Addr: viper.GetString("next-terminal.addr"),
}
return config
}

37
pkg/config/store.go Normal file
View File

@ -0,0 +1,37 @@
package config
import (
"github.com/pkg/sftp"
"next-terminal/pkg/guacd"
"sync"
)
type Tun struct {
Tun guacd.Tunnel
SftpClient *sftp.Client
}
type TunStore struct {
m sync.Map
}
func (s *TunStore) Set(k string, v Tun) {
s.m.Store(k, v)
}
func (s *TunStore) Del(k string) {
s.m.Delete(k)
}
func (s *TunStore) Get(k string) (item Tun, ok bool) {
value, ok := s.m.Load(k)
if ok {
return value.(Tun), true
}
return item, false
}
func NewStore() *TunStore {
store := TunStore{sync.Map{}}
return &store
}

227
pkg/guacd/guacd.go Normal file
View File

@ -0,0 +1,227 @@
package guacd
import (
"bufio"
"errors"
"fmt"
"net"
"strings"
)
const Delimiter = ';'
const Version = "VERSION_1_1_0"
type Configuration struct {
ConnectionID string
Protocol string
Parameters map[string]string
}
func NewConfiguration() (ret Configuration) {
ret.Parameters = make(map[string]string)
return ret
}
func (opt *Configuration) SetParameter(name, value string) {
opt.Parameters[name] = value
}
func (opt *Configuration) UnSetParameter(name string) {
delete(opt.Parameters, name)
}
func (opt *Configuration) GetParameter(name string) string {
return opt.Parameters[name]
}
type Instruction struct {
Opcode string
Args []string
ProtocolForm string
}
func NewInstruction(opcode string, args ...string) (ret Instruction) {
ret.Opcode = opcode
ret.Args = args
return ret
}
func (opt *Instruction) String() string {
if len(opt.ProtocolForm) > 0 {
return opt.ProtocolForm
}
opt.ProtocolForm = fmt.Sprintf("%d.%s", len(opt.Opcode), opt.Opcode)
for _, value := range opt.Args {
opt.ProtocolForm += fmt.Sprintf(",%d.%s", len(value), value)
}
opt.ProtocolForm += string(Delimiter)
return opt.ProtocolForm
}
func (opt *Instruction) Parse(content string) Instruction {
if strings.LastIndex(content, ";") > 0 {
content = strings.TrimRight(content, ";")
}
messages := strings.Split(content, ",")
var args = make([]string, len(messages))
for i := range messages {
lm := strings.Split(messages[i], ".")
args[i] = lm[1]
}
return NewInstruction(args[0], args[1:]...)
}
type Tunnel struct {
rw *bufio.ReadWriter
conn net.Conn
UUID string
Config Configuration
IsOpen bool
}
func NewTunnel(address string, config Configuration) (ret Tunnel, err error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return
}
ret.conn = conn
ret.rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
ret.Config = config
selectArg := config.ConnectionID
if selectArg == "" {
selectArg = config.Protocol
}
if err := ret.WriteInstructionAndFlush(NewInstruction("select", selectArg)); err != nil {
return Tunnel{}, err
}
args, err := ret.expect("args")
if err != nil {
return
}
width := config.GetParameter("width")
height := config.GetParameter("height")
// send size
if err := ret.WriteInstructionAndFlush(NewInstruction("size", width, height, "96")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("audio")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("video")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("image")); err != nil {
return Tunnel{}, err
}
if err := ret.WriteInstructionAndFlush(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
return Tunnel{}, err
}
parameters := make([]string, len(args.Args))
for i := range args.Args {
argName := args.Args[i]
if strings.Contains(argName, "VERSION") {
parameters[i] = Version
continue
}
parameters[i] = config.GetParameter(argName)
}
// send connect
if err := ret.WriteInstructionAndFlush(NewInstruction("connect", parameters...)); err != nil {
return Tunnel{}, err
}
ready, err := ret.expect("ready")
if err != nil {
return
}
if len(ready.Args) == 0 {
return ret, errors.New("no connection id received")
}
ret.UUID = ready.Args[0]
ret.IsOpen = true
return ret, nil
}
func (opt *Tunnel) WriteInstructionAndFlush(instruction Instruction) error {
if _, err := opt.WriteAndFlush([]byte(instruction.String())); err != nil {
return err
}
return nil
}
func (opt *Tunnel) WriteInstruction(instruction Instruction) error {
if _, err := opt.Write([]byte(instruction.String())); err != nil {
return err
}
return nil
}
func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) {
//fmt.Printf("-> %v \n", string(p))
nn, err := opt.rw.Write(p)
if err != nil {
return nn, err
}
err = opt.rw.Flush()
if err != nil {
return nn, err
}
return nn, nil
}
func (opt *Tunnel) Write(p []byte) (int, error) {
//fmt.Printf("-> %v \n", string(p))
nn, err := opt.rw.Write(p)
if err != nil {
return nn, err
}
return nn, nil
}
func (opt *Tunnel) Flush() error {
return opt.rw.Flush()
}
func (opt *Tunnel) ReadInstruction() (instruction Instruction, err error) {
msg, err := opt.rw.ReadString(Delimiter)
if err != nil {
return instruction, err
}
fmt.Printf("<- %v \n", msg)
return instruction.Parse(msg), err
}
func (opt *Tunnel) Read() ([]byte, error) {
return opt.rw.ReadBytes(Delimiter)
}
func (opt *Tunnel) expect(opcode string) (instruction Instruction, err error) {
instruction, err = opt.ReadInstruction()
if err != nil {
return instruction, err
}
if opcode != instruction.Opcode {
msg := fmt.Sprintf(`expected "%s" instruction but instead received "%s"`, opcode, instruction.Opcode)
return instruction, errors.New(msg)
}
return instruction, nil
}
func (opt *Tunnel) Close() error {
opt.IsOpen = false
return opt.conn.Close()
}

199
pkg/handle/runner.go Normal file
View File

@ -0,0 +1,199 @@
package handle
import (
"next-terminal/pkg/model"
"next-terminal/pkg/utils"
"os"
"time"
)
func RunTicker() {
var ch chan int
//定时任务
ticker := time.NewTicker(time.Minute * 5)
go func() {
for range ticker.C {
items, _ := model.FindAllAsset()
for i := range items {
item := items[i]
active := utils.Tcping(item.IP, item.Port)
asset := model.Asset{
Active: active,
}
model.UpdateAssetById(&asset, item.ID)
}
}
ch <- 1
}()
<-ch
}
func RunDataFix() {
sessions, _ := model.FindSessionByStatus(model.Connected)
if sessions == nil {
return
}
for i := range sessions {
session := model.Session{
Status: model.Disconnected,
DisconnectedTime: utils.NowJsonTime(),
}
model.UpdateSessionById(&session, sessions[i].ID)
}
}
func InitProperties() {
propertyMap := model.FindAllPropertiesMap()
if len(propertyMap[model.GuacdHost]) == 0 {
property := model.Property{
Name: model.GuacdHost,
Value: "127.0.0.1",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdPort]) == 0 {
property := model.Property{
Name: model.GuacdPort,
Value: "4822",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDriveName]) == 0 {
property := model.Property{
Name: model.GuacdDriveName,
Value: "File-System",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDrivePath]) == 0 {
path, _ := os.Getwd()
property := model.Property{
Name: model.GuacdDrivePath,
Value: path + "/drive/",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdFontName]) == 0 {
property := model.Property{
Name: model.GuacdFontName,
Value: "menlo",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdFontSize]) == 0 {
property := model.Property{
Name: model.GuacdFontSize,
Value: "12",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdColorScheme]) == 0 {
property := model.Property{
Name: model.GuacdColorScheme,
Value: "gray-black",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableSftp]) == 0 {
property := model.Property{
Name: model.GuacdEnableSftp,
Value: "true",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableDrive]) == 0 {
property := model.Property{
Name: model.GuacdEnableDrive,
Value: "true",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableWallpaper]) == 0 {
property := model.Property{
Name: model.GuacdEnableWallpaper,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableTheming]) == 0 {
property := model.Property{
Name: model.GuacdEnableTheming,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableFontSmoothing]) == 0 {
property := model.Property{
Name: model.GuacdEnableFontSmoothing,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableFullWindowDrag]) == 0 {
property := model.Property{
Name: model.GuacdEnableFullWindowDrag,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableDesktopComposition]) == 0 {
property := model.Property{
Name: model.GuacdEnableDesktopComposition,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdEnableMenuAnimations]) == 0 {
property := model.Property{
Name: model.GuacdEnableMenuAnimations,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableBitmapCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableBitmapCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableOffscreenCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableOffscreenCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
if len(propertyMap[model.GuacdDisableGlyphCaching]) == 0 {
property := model.Property{
Name: model.GuacdDisableGlyphCaching,
Value: "false",
}
_ = model.CreateNewProperty(&property)
}
}

86
pkg/model/asset.go Normal file
View File

@ -0,0 +1,86 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Asset struct {
ID string `gorm:"primary_key " json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
Protocol string `json:"protocol"`
Port int `json:"port"`
AccountType string `json:"accountType"`
Username string `json:"username"`
Password string `json:"password"`
CredentialId string `json:"credentialId"`
PrivateKey string `json:"privateKey"`
Passphrase string `json:"passphrase"`
Description string `json:"description"`
Active bool `json:"active"`
Created utils.JsonTime `json:"created"`
}
func (r *Asset) TableName() string {
return "assets"
}
func FindAllAsset() (o []Asset, err error) {
err = config.DB.Find(&o).Error
return
}
func FindAssetByConditions(protocol string) (o []Asset, err error) {
db := config.DB
if len(protocol) > 0 {
db = db.Where("protocol = ?", protocol)
}
err = db.Find(&o).Error
return
}
func FindPageAsset(pageIndex, pageSize int, name, protocol string) (o []Asset, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
if len(protocol) > 0 {
db = db.Where("protocol = ?", protocol)
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Asset, 0)
}
return
}
func CreateNewAsset(o *Asset) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindAssetById(id string) (o Asset, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateAssetById(o *Asset, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteAssetById(id string) {
config.DB.Where("id = ?", id).Delete(&Asset{})
}
func CountAsset() (total int64, err error) {
err = config.DB.Find(&Asset{}).Count(&total).Error
return
}

56
pkg/model/command.go Normal file
View File

@ -0,0 +1,56 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Command struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Created utils.JsonTime `json:"created"`
}
func (r *Command) TableName() string {
return "commands"
}
func FindPageCommand(pageIndex, pageSize int, name, content string) (o []Command, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
if len(content) > 0 {
db = db.Where("content like ?", "%"+content+"%")
}
err = db.Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Command, 0)
}
return
}
func CreateNewCommand(o *Command) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindCommandById(id string) (o Command, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateCommandById(o *Command, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteCommandById(id string) {
config.DB.Where("id = ?", id).Delete(&Command{})
}

63
pkg/model/credential.go Normal file
View File

@ -0,0 +1,63 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
type Credential struct {
ID string `gorm:"primary_key" json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Created utils.JsonTime `json:"created"`
}
func (r *Credential) TableName() string {
return "credentials"
}
func FindAllCredential() (o []Credential, err error) {
err = config.DB.Find(&o).Error
return
}
func FindPageCredential(pageIndex, pageSize int, name string) (o []Credential, total int64, err error) {
db := config.DB
if len(name) > 0 {
db = db.Where("name like ?", "%"+name+"%")
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]Credential, 0)
}
return
}
func CreateNewCredential(o *Credential) (err error) {
if err = config.DB.Create(o).Error; err != nil {
return err
}
return nil
}
func FindCredentialById(id string) (o Credential, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func UpdateCredentialById(o *Credential, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteCredentialById(id string) {
config.DB.Where("id = ?", id).Delete(&Credential{})
}
func CountCredential() (total int64, err error) {
err = config.DB.Find(&Credential{}).Count(&total).Error
return
}

78
pkg/model/property.go Normal file
View File

@ -0,0 +1,78 @@
package model
import (
"next-terminal/pkg/config"
"errors"
)
const (
GuacdHost = "host"
GuacdPort = "port"
GuacdFontName = "font-name"
GuacdFontSize = "font-size"
GuacdColorScheme = "color-scheme"
GuacdEnableSftp = "enable-sftp"
GuacdEnableDrive = "enable-drive"
GuacdDriveName = "drive-name"
GuacdDrivePath = "drive-path"
GuacdEnableWallpaper = "enable-wallpaper"
GuacdEnableTheming = "enable-theming"
GuacdEnableFontSmoothing = "enable-font-smoothing"
GuacdEnableFullWindowDrag = "enable-full-window-drag"
GuacdEnableDesktopComposition = "enable-desktop-composition"
GuacdEnableMenuAnimations = "enable-menu-animations"
GuacdDisableBitmapCaching = "disable-bitmap-caching"
GuacdDisableOffscreenCaching = "disable-offscreen-caching"
GuacdDisableGlyphCaching = "disable-glyph-caching"
)
type Property struct {
Name string `gorm:"primary_key" json:"name"`
Value string `json:"value"`
}
func (r *Property) TableName() string {
return "properties"
}
func FindAllProperties() (o []Property) {
if config.DB.Find(&o).Error != nil {
return nil
}
return
}
func CreateNewProperty(o *Property) (err error) {
err = config.DB.Create(o).Error
return
}
func UpdatePropertyByName(o *Property, name string) {
o.Name = name
config.DB.Updates(o)
}
func FindPropertyByName(name string) (o Property, err error) {
err = config.DB.Where("name = ?", name).First(&o).Error
return
}
func FindAllPropertiesMap() map[string]string {
properties := FindAllProperties()
propertyMap := make(map[string]string)
for i := range properties {
propertyMap[properties[i].Name] = properties[i].Value
}
return propertyMap
}
func GetDrivePath() (string, error) {
propertiesMap := FindAllPropertiesMap()
drivePath := propertiesMap[GuacdDrivePath]
if len(drivePath) == 0 {
return "", errors.New("获取RDP挂载目录失败")
}
return drivePath, nil
}

138
pkg/model/session.go Normal file
View File

@ -0,0 +1,138 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
)
const (
Connected = "connected"
Disconnected = "disconnected"
NoConnect = "no_connect"
)
type Session struct {
ID string `gorm:"primary_key" json:"id"`
Protocol string `json:"protocol"`
IP string `json:"ip"`
Port int `json:"port"`
ConnectionId string `json:"connectionId"`
AssetId string `json:"assetId"`
Username string `json:"username"`
Password string `json:"password"`
Creator string `json:"creator"`
ClientIP string `json:"clientIp"`
Width int `json:"width"`
Height int `json:"height"`
Status string `json:"status"`
ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
}
func (r *Session) TableName() string {
return "sessions"
}
type SessionVo struct {
ID string `json:"id"`
Protocol string `json:"protocol"`
IP string `json:"ip"`
Port int `json:"port"`
Username string `json:"username"`
ConnectionId string `json:"connectionId"`
AssetId string `json:"assetId"`
Creator string `json:"creator"`
ClientIP string `json:"clientIp"`
Width int `json:"width"`
Height int `json:"height"`
Status string `json:"status"`
ConnectedTime utils.JsonTime `json:"connectedTime"`
DisconnectedTime utils.JsonTime `json:"disconnectedTime"`
AssetName string `json:"assetName"`
CreatorName string `json:"creatorName"`
}
func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol string) (results []SessionVo, total int64, err error) {
db := config.DB
var params []interface{}
params = append(params, status)
itemSql := "SELECT s.id, s.protocol, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? "
countSql := "select count(*) from sessions as s where s.status = ? "
if len(userId) > 0 {
itemSql += " and s.creator = ?"
countSql += " and s.creator = ?"
params = append(params, userId)
}
if len(clientIp) > 0 {
itemSql += " and s.client_ip like ?"
countSql += " and s.client_ip like ?"
params = append(params, "%"+clientIp+"%")
}
if len(assetId) > 0 {
itemSql += " and s.asset_id = ?"
countSql += " and s.asset_id = ?"
params = append(params, assetId)
}
if len(protocol) > 0 {
itemSql += " and s.protocol = ?"
countSql += " and s.protocol = ?"
params = append(params, protocol)
}
params = append(params, (pageIndex-1)*pageSize, pageSize)
itemSql += " order by s.connected_time desc LIMIT ?, ?"
db.Raw(countSql, params...).Scan(&total)
err = db.Raw(itemSql, params...).Scan(&results).Error
if results == nil {
results = make([]SessionVo, 0)
}
return
}
func FindSessionByStatus(status string) (o []Session, err error) {
err = config.DB.Where("status = ?", status).Find(&o).Error
return
}
func CreateNewSession(o *Session) (err error) {
err = config.DB.Create(o).Error
return
}
func FindSessionById(id string) (o Session, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func FindSessionByConnectionId(connectionId string) (o Session, err error) {
err = config.DB.Where("connection_id = ?", connectionId).First(&o).Error
return
}
func UpdateSessionById(o *Session, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteSessionById(id string) {
config.DB.Where("id = ?", id).Delete(&Session{})
}
func DeleteSessionByStatus(status string) {
config.DB.Where("status = ?", status).Delete(&Session{})
}
func CountOnlineSession() (total int64, err error) {
err = config.DB.Where("status = ?", Connected).Find(&Session{}).Count(&total).Error
return
}

79
pkg/model/user.go Normal file
View File

@ -0,0 +1,79 @@
package model
import (
"next-terminal/pkg/config"
"next-terminal/pkg/utils"
"reflect"
)
type User struct {
ID string `gorm:"primary_key" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Nickname string `json:"nickname"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`
Created utils.JsonTime `json:"created"`
}
func (r *User) TableName() string {
return "users"
}
func (r *User) IsEmpty() bool {
return reflect.DeepEqual(r, User{})
}
func FindAllUser() (o []User) {
if config.DB.Find(&o).Error != nil {
return nil
}
return
}
func FindPageUser(pageIndex, pageSize int, username, nickname string) (o []User, total int64, err error) {
db := config.DB
if len(username) > 0 {
db = db.Where("username like ?", "%"+username+"%")
}
if len(nickname) > 0 {
db = db.Where("nickname like ?", "%"+nickname+"%")
}
err = db.Order("created desc").Find(&o).Offset((pageIndex - 1) * pageSize).Limit(pageSize).Count(&total).Error
if o == nil {
o = make([]User, 0)
}
return
}
func CreateNewUser(o *User) (err error) {
err = config.DB.Create(o).Error
return
}
func FindUserById(id string) (o User, err error) {
err = config.DB.Where("id = ?", id).First(&o).Error
return
}
func FindUserByUsername(username string) (o User, err error) {
err = config.DB.Where("username = ?", username).First(&o).Error
return
}
func UpdateUserById(o *User, id string) {
o.ID = id
config.DB.Updates(o)
}
func DeleteUserById(id string) {
config.DB.Where("id = ?", id).Delete(&User{})
}
func CountUser() (total int64, err error) {
err = config.DB.Find(&User{}).Count(&total).Error
return
}

81
pkg/utils/utils.go Normal file
View File

@ -0,0 +1,81 @@
package utils
import (
"database/sql/driver"
"fmt"
"github.com/gofrs/uuid"
"golang.org/x/crypto/bcrypt"
"net"
"strconv"
"time"
)
type JsonTime struct {
time.Time
}
func NewJsonTime(t time.Time) JsonTime {
return JsonTime{
Time: t,
}
}
func NowJsonTime() JsonTime {
return JsonTime{
Time: time.Now(),
}
}
func (t JsonTime) MarshalJSON() ([]byte, error) {
var stamp = fmt.Sprintf("\"%s\"", t.Format("2006-01-02 15:04:05"))
return []byte(stamp), nil
}
func (t JsonTime) Value() (driver.Value, error) {
var zeroTime time.Time
if t.Time.UnixNano() == zeroTime.UnixNano() {
return nil, nil
}
return t.Time, nil
}
func (t *JsonTime) Scan(v interface{}) error {
value, ok := v.(time.Time)
if ok {
*t = JsonTime{Time: value}
return nil
}
return fmt.Errorf("can not convert %v to timestamp", v)
}
type Bcrypt struct {
cost int
}
func (b *Bcrypt) Encode(password []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(password, b.cost)
}
func (b *Bcrypt) Match(hashedPassword, password []byte) error {
return bcrypt.CompareHashAndPassword(hashedPassword, password)
}
var Encoder = Bcrypt{
cost: bcrypt.DefaultCost,
}
func UUID() string {
v4, _ := uuid.NewV4()
return v4.String()
}
func Tcping(ip string, port int) bool {
var conn net.Conn
var err error
if conn, err = net.DialTimeout("tcp", ip+":"+strconv.Itoa(port), 2*time.Second); err != nil {
return false
}
defer conn.Close()
return true
}