mirror of
https://github.com/veops/oneterm.git
synced 2025-10-10 17:50:09 +08:00
feat: guacd for rdp/vnc
This commit is contained in:
@@ -26,6 +26,7 @@ import (
|
|||||||
myi18n "github.com/veops/oneterm/pkg/i18n"
|
myi18n "github.com/veops/oneterm/pkg/i18n"
|
||||||
"github.com/veops/oneterm/pkg/logger"
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||||
|
"github.com/veops/oneterm/pkg/server/guacd"
|
||||||
"github.com/veops/oneterm/pkg/server/model"
|
"github.com/veops/oneterm/pkg/server/model"
|
||||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||||
)
|
)
|
||||||
@@ -158,7 +159,11 @@ func sendMsg(ws *websocket.Conn, session *model.Session, chs *model.SessionChans
|
|||||||
// @Router /connect/:asset_id/:account_id/:protocol [post]
|
// @Router /connect/:asset_id/:account_id/:protocol [post]
|
||||||
func (c *Controller) Connect(ctx *gin.Context) {
|
func (c *Controller) Connect(ctx *gin.Context) {
|
||||||
chs := makeChans()
|
chs := makeChans()
|
||||||
|
protocol := ctx.Param("protocol")
|
||||||
|
sessionId := ""
|
||||||
|
|
||||||
|
switch strings.Split(protocol, ":")[0] {
|
||||||
|
case "ssh":
|
||||||
resp := &model.SshResp{}
|
resp := &model.SshResp{}
|
||||||
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), newSshReq(ctx, model.SESSIONACTION_NEW), chs)
|
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), newSshReq(ctx, model.SESSIONACTION_NEW), chs)
|
||||||
if err := <-chs.ErrChan; err != nil {
|
if err := <-chs.ErrChan; err != nil {
|
||||||
@@ -172,7 +177,29 @@ func (c *Controller) Connect(ctx *gin.Context) {
|
|||||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}})
|
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v, ok := onlineSession.Load(resp.SessionId)
|
sessionId = resp.SessionId
|
||||||
|
case "vnc", "rdp":
|
||||||
|
asset, account := &model.Asset{}, &model.Account{}
|
||||||
|
if err := mysql.DB.Model(&asset).Where("id = ?", ctx.Param("asset_id")).First(asset).Error; err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mysql.DB.Model(&account).Where("id = ?", ctx.Param("account_id")).First(asset).Error; err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := guacd.NewTunnel(protocol, asset, account, nil)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Handshake()
|
||||||
|
default:
|
||||||
|
logger.L.Error("wrong protocol " + protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := onlineSession.Load(sessionId)
|
||||||
if !ok {
|
if !ok {
|
||||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "cannot find in sync map"}})
|
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "cannot find in sync map"}})
|
||||||
return
|
return
|
||||||
|
158
backend/pkg/server/guacd/conn.go
Normal file
158
backend/pkg/server/guacd/conn.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package guacd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/veops/oneterm/pkg/server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = "VERSION_1_5_0"
|
||||||
|
|
||||||
|
type Configuration struct {
|
||||||
|
ConnectionId string
|
||||||
|
Protocol string
|
||||||
|
Parameters map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfiguration() (config *Configuration) {
|
||||||
|
config = &Configuration{}
|
||||||
|
config.Parameters = make(map[string]string)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tunnel struct {
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
writer *bufio.Writer
|
||||||
|
Uuid string
|
||||||
|
Config *Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTunnel(protocol string, asset *model.Asset, account *model.Account, gateway *model.Gateway) (t *Tunnel, err error) {
|
||||||
|
ss := strings.Split(protocol, ":")
|
||||||
|
protocol, port := ss[0], ss[1]
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", asset.Ip, port), time.Second*3)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t = &Tunnel{
|
||||||
|
conn: conn,
|
||||||
|
reader: bufio.NewReader(conn),
|
||||||
|
writer: bufio.NewWriter(conn),
|
||||||
|
Config: &Configuration{
|
||||||
|
Protocol: protocol,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake
|
||||||
|
//
|
||||||
|
// https://guacamole.apache.org/doc/gug/guacamole-protocol.html#handshake-phase
|
||||||
|
func (t *Tunnel) Handshake() (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
defer t.conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// select
|
||||||
|
if err = t.WriteInstruction(NewInstruction("select", lo.Ternary(t.Config.ConnectionId == "", t.Config.Protocol, t.Config.ConnectionId))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// args
|
||||||
|
args, err := t.assert("args")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parameters := make([]string, len(args.Args))
|
||||||
|
for i, name := range args.Args {
|
||||||
|
if strings.Contains(name, "VERSION") {
|
||||||
|
parameters[i] = Version
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parameters[i] = t.Config.Parameters[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// size audio ...
|
||||||
|
if err = t.WriteInstruction(NewInstruction("size", t.Config.Parameters["width"], t.Config.Parameters["height"], t.Config.Parameters["dpi"])); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = t.WriteInstruction(NewInstruction("audio", "audio/L8", "audio/L16")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = t.WriteInstruction(NewInstruction("video")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = t.WriteInstruction(NewInstruction("image", "image/jpeg", "image/png", "image/webp")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = t.WriteInstruction(NewInstruction("timezone", "Asia/Shanghai")); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect
|
||||||
|
if err = t.WriteInstruction(NewInstruction("connect", parameters...)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ready
|
||||||
|
ready, err := t.assert("ready")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ready.Args) == 0 {
|
||||||
|
err = fmt.Errorf("empty connection id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Uuid = ready.Args[0]
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) WriteInstruction(instruction *Instruction) (err error) {
|
||||||
|
_, err = t.writer.Write([]byte(instruction.String()))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.writer.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) ReadInstruction() (instruction *Instruction, err error) {
|
||||||
|
data, err := t.reader.ReadBytes(Delimiter)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instruction = (&Instruction{}).Parse(string(data))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) assert(opcode string) (instruction *Instruction, err error) {
|
||||||
|
instruction, err = t.ReadInstruction()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if opcode != instruction.Opcode {
|
||||||
|
err = fmt.Errorf(`expect instruction "%s" but got "%s"`, opcode, instruction.Opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
46
backend/pkg/server/guacd/instruction.go
Normal file
46
backend/pkg/server/guacd/instruction.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package guacd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
const Delimiter = ';'
|
||||||
|
|
||||||
|
type Instruction struct {
|
||||||
|
Opcode string
|
||||||
|
Args []string
|
||||||
|
cache string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstruction(opcode string, args ...string) *Instruction {
|
||||||
|
return &Instruction{
|
||||||
|
Opcode: opcode,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instruction) String() string {
|
||||||
|
if len(i.cache) > 0 {
|
||||||
|
return i.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
i.cache = fmt.Sprintf("%d.%s", len(i.Opcode), i.Opcode)
|
||||||
|
for _, value := range i.Args {
|
||||||
|
i.cache += fmt.Sprintf(",%d.%s", len(value), value)
|
||||||
|
}
|
||||||
|
i.cache += string(Delimiter)
|
||||||
|
return i.cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instruction) Parse(content string) *Instruction {
|
||||||
|
if strings.LastIndex(content, ";") > 0 {
|
||||||
|
content = strings.TrimRight(content, ";")
|
||||||
|
}
|
||||||
|
elements := strings.Split(content, ",")
|
||||||
|
|
||||||
|
var args = make([]string, len(elements))
|
||||||
|
for i, e := range elements {
|
||||||
|
args[i] = strings.Split(e, ".")[1]
|
||||||
|
}
|
||||||
|
return NewInstruction(args[0], args[1:]...)
|
||||||
|
}
|
Reference in New Issue
Block a user