feat: guacd for rdp/vnc

This commit is contained in:
ttk
2024-02-21 18:21:35 +08:00
parent a7f670422c
commit eed3db67cb
3 changed files with 244 additions and 13 deletions

View File

@@ -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

View 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
}

View 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:]...)
}