mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 00:10:25 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			280 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package guac
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"mayfly-go/pkg/logx"
 | 
						|
	"net"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	SocketTimeout  = 15 * time.Second
 | 
						|
	MaxGuacMessage = 8192 // TODO is this bytes or runes?
 | 
						|
)
 | 
						|
 | 
						|
// Stream wraps the connection to Guacamole providing timeouts and reading
 | 
						|
// a single instruction at a time (since returning partial instructions
 | 
						|
// would be an error)
 | 
						|
type Stream struct {
 | 
						|
	conn net.Conn
 | 
						|
 | 
						|
	// ConnectionID is the ID Guacamole gives and can be used to reconnect or share sessions
 | 
						|
	ConnectionID string
 | 
						|
	timeout      time.Duration
 | 
						|
 | 
						|
	// if more than a single instruction is read, the rest are buffered here
 | 
						|
	parseStart int
 | 
						|
	buffer     []rune
 | 
						|
	reset      []rune
 | 
						|
}
 | 
						|
 | 
						|
// NewStream creates a new stream
 | 
						|
func NewStream(conn net.Conn, timeout time.Duration) (ret *Stream) {
 | 
						|
	buffer := make([]rune, 0, MaxGuacMessage*3)
 | 
						|
	return &Stream{
 | 
						|
		conn:    conn,
 | 
						|
		timeout: timeout,
 | 
						|
		buffer:  buffer,
 | 
						|
		reset:   buffer[:cap(buffer)],
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Write sends messages to Guacamole with a timeout
 | 
						|
func (s *Stream) Write(data []byte) (n int, err error) {
 | 
						|
	if err = s.conn.SetWriteDeadline(time.Now().Add(s.timeout)); err != nil {
 | 
						|
		logx.Errorf("sends messages to Guacamole error: %v", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	return s.conn.Write(data)
 | 
						|
}
 | 
						|
 | 
						|
// Available returns true if there are messages buffered
 | 
						|
func (s *Stream) Available() bool {
 | 
						|
	return len(s.buffer) > 0
 | 
						|
}
 | 
						|
 | 
						|
// Flush resets the internal buffer
 | 
						|
func (s *Stream) Flush() {
 | 
						|
	copy(s.reset, s.buffer)
 | 
						|
	s.buffer = s.reset[:len(s.buffer)]
 | 
						|
}
 | 
						|
 | 
						|
// ReadSome takes the next instruction (from the network or from the buffer) and returns it.
 | 
						|
// io.Reader is not implemented because this seems like the right place to maintain a buffer.
 | 
						|
func (s *Stream) ReadSome() (instruction []byte, err error) {
 | 
						|
	if err = s.conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil {
 | 
						|
		logx.Errorf("read messages from Guacamole error: %v", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	buffer := make([]byte, MaxGuacMessage)
 | 
						|
	var n int
 | 
						|
	// While we're blocking, or input is available
 | 
						|
	for {
 | 
						|
		// Length of element
 | 
						|
		var elementLength int
 | 
						|
 | 
						|
		// Resume where we left off
 | 
						|
		i := s.parseStart
 | 
						|
 | 
						|
	parseLoop:
 | 
						|
		// Parse instruction in buffer
 | 
						|
		for i < len(s.buffer) {
 | 
						|
			// ReadSome character
 | 
						|
			readChar := s.buffer[i]
 | 
						|
			i++
 | 
						|
 | 
						|
			switch readChar {
 | 
						|
			// If digit, update length
 | 
						|
			case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
 | 
						|
				elementLength = elementLength*10 + int(readChar-'0')
 | 
						|
 | 
						|
			// If not digit, check for end-of-length character
 | 
						|
			case '.':
 | 
						|
				if i+elementLength >= len(s.buffer) {
 | 
						|
					// break for i < s.usedLength { ... }
 | 
						|
					// Otherwise, read more data
 | 
						|
					break parseLoop
 | 
						|
				}
 | 
						|
				// Check if element present in buffer
 | 
						|
				terminator := s.buffer[i+elementLength]
 | 
						|
				// Move to character after terminator
 | 
						|
				i += elementLength + 1
 | 
						|
 | 
						|
				// Reset length
 | 
						|
				elementLength = 0
 | 
						|
 | 
						|
				// Continue here if necessary
 | 
						|
				s.parseStart = i
 | 
						|
 | 
						|
				// If terminator is semicolon, we have a full
 | 
						|
				// instruction.
 | 
						|
				switch terminator {
 | 
						|
				case ';':
 | 
						|
					instruction = []byte(string(s.buffer[0:i]))
 | 
						|
					s.parseStart = 0
 | 
						|
					s.buffer = s.buffer[i:]
 | 
						|
					return
 | 
						|
				case ',':
 | 
						|
					// keep going
 | 
						|
				default:
 | 
						|
					err = ErrServer.NewError("Element terminator of instruction was not ';' nor ','")
 | 
						|
					return
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				// Otherwise, parse error
 | 
						|
				err = ErrServer.NewError("Non-numeric character in element length:", string(readChar))
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		n, err = s.conn.Read(buffer)
 | 
						|
		if err != nil && n == 0 {
 | 
						|
			switch err.(type) {
 | 
						|
			case net.Error:
 | 
						|
				ex := err.(net.Error)
 | 
						|
				if ex.Timeout() {
 | 
						|
					err = ErrUpstreamTimeout.NewError("Connection to guacd timed out.", err.Error())
 | 
						|
				} else {
 | 
						|
					err = ErrConnectionClosed.NewError("Connection to guacd is closed.", err.Error())
 | 
						|
				}
 | 
						|
			default:
 | 
						|
				err = ErrServer.NewError(err.Error())
 | 
						|
			}
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if n == 0 {
 | 
						|
			err = ErrServer.NewError("read 0 bytes")
 | 
						|
		}
 | 
						|
		runes := []rune(string(buffer[:n]))
 | 
						|
 | 
						|
		if cap(s.buffer)-len(s.buffer) < len(runes) {
 | 
						|
			s.Flush()
 | 
						|
		}
 | 
						|
 | 
						|
		n = copy(s.buffer[len(s.buffer):cap(s.buffer)], runes)
 | 
						|
		// must reslice so len is changed
 | 
						|
		s.buffer = s.buffer[:len(s.buffer)+n]
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Close closes the underlying network connection
 | 
						|
func (s *Stream) Close() error {
 | 
						|
	return s.conn.Close()
 | 
						|
}
 | 
						|
 | 
						|
// Handshake configures the guacd session
 | 
						|
func (s *Stream) Handshake(config *Config) error {
 | 
						|
	// Get protocol / connection ID
 | 
						|
	selectArg := config.ConnectionID
 | 
						|
	if len(selectArg) == 0 {
 | 
						|
		selectArg = config.Protocol
 | 
						|
	}
 | 
						|
 | 
						|
	// Send requested protocol or connection ID
 | 
						|
	_, err := s.Write(NewInstruction("select", selectArg).Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Wait for server Args
 | 
						|
	args, err := s.AssertOpcode("args")
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Build Args list off provided names and config
 | 
						|
	argNameS := args.Args
 | 
						|
	argValueS := make([]string, 0, len(argNameS))
 | 
						|
	for _, argName := range argNameS {
 | 
						|
 | 
						|
		// Retrieve argument name
 | 
						|
 | 
						|
		// Get defined value for name
 | 
						|
		value := config.Parameters[argName]
 | 
						|
 | 
						|
		// If value defined, set that value
 | 
						|
		if len(value) == 0 {
 | 
						|
			value = ""
 | 
						|
		}
 | 
						|
		argValueS = append(argValueS, value)
 | 
						|
	}
 | 
						|
 | 
						|
	// Send size
 | 
						|
	_, err = s.Write(NewInstruction("size",
 | 
						|
		fmt.Sprintf("%v", config.OptimalScreenWidth),
 | 
						|
		fmt.Sprintf("%v", config.OptimalScreenHeight),
 | 
						|
		fmt.Sprintf("%v", config.OptimalResolution)).Byte(),
 | 
						|
	)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Send supported audio formats
 | 
						|
	_, err = s.Write(NewInstruction("audio", config.AudioMimetypes...).Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Send supported video formats
 | 
						|
	_, err = s.Write(NewInstruction("video", config.VideoMimetypes...).Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Send supported image formats
 | 
						|
	_, err = s.Write(NewInstruction("image", config.ImageMimetypes...).Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// timezone
 | 
						|
	_, err = s.Write(NewInstruction("timezone", "Asia/Shanghai").Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Send Args
 | 
						|
	_, err = s.Write(NewInstruction("connect", argValueS...).Byte())
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Wait for ready, store ID
 | 
						|
	ready, err := s.AssertOpcode("ready")
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	readyArgs := ready.Args
 | 
						|
	if len(readyArgs) == 0 {
 | 
						|
		err = ErrServer.NewError("No connection ID received")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	s.Flush()
 | 
						|
	s.ConnectionID = readyArgs[0]
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// AssertOpcode checks the next opcode in the stream matches what is expected. Useful during handshake.
 | 
						|
func (s *Stream) AssertOpcode(opcode string) (instruction *Instruction, err error) {
 | 
						|
	instruction, err = ReadOne(s)
 | 
						|
	if err != nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(instruction.Opcode) == 0 {
 | 
						|
		err = ErrServer.NewError("End of stream while waiting for \"" + opcode + "\".")
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if instruction.Opcode != opcode {
 | 
						|
		err = ErrServer.NewError("Expected \"" + opcode + "\" instruction but instead received \"" + instruction.Opcode + "\".")
 | 
						|
		return
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 |