mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-03 07:50:25 +08:00
239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
package guac
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"mayfly-go/pkg/logx"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
readPrefix string = "read:"
|
|
writePrefix string = "write:"
|
|
readPrefixLength = len(readPrefix)
|
|
writePrefixLength = len(writePrefix)
|
|
uuidLength = 36
|
|
)
|
|
|
|
// Server uses HTTP requests to talk to guacd (as opposed to WebSockets in ws_server.go)
|
|
type Server struct {
|
|
tunnels *TunnelMap
|
|
connect func(*http.Request) (Tunnel, error)
|
|
}
|
|
|
|
// NewServer constructor
|
|
func NewServer(connect func(r *http.Request) (Tunnel, error)) *Server {
|
|
return &Server{
|
|
tunnels: NewTunnelMap(),
|
|
connect: connect,
|
|
}
|
|
}
|
|
|
|
// Registers the given tunnel such that future read/write requests to that tunnel will be properly directed.
|
|
func (s *Server) registerTunnel(tunnel Tunnel) {
|
|
s.tunnels.Put(tunnel.GetUUID(), tunnel)
|
|
logx.Debugf("Registered tunnel %v.", tunnel.GetUUID())
|
|
}
|
|
|
|
// Deregisters the given tunnel such that future read/write requests to that tunnel will be rejected.
|
|
func (s *Server) deregisterTunnel(tunnel Tunnel) {
|
|
s.tunnels.Remove(tunnel.GetUUID())
|
|
logx.Debugf("Deregistered tunnel %v.", tunnel.GetUUID())
|
|
}
|
|
|
|
// Returns the tunnel with the given UUID.
|
|
func (s *Server) getTunnel(tunnelUUID string) (ret Tunnel, err error) {
|
|
var ok bool
|
|
ret, ok = s.tunnels.Get(tunnelUUID)
|
|
|
|
if !ok {
|
|
err = ErrResourceNotFound.NewError("No such tunnel.")
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *Server) sendError(response http.ResponseWriter, guacStatus Status, message string) {
|
|
response.Header().Set("Guacamole-Status-Code", fmt.Sprintf("%v", guacStatus.GetGuacamoleStatusCode()))
|
|
response.Header().Set("Guacamole-Error-Message", message)
|
|
response.WriteHeader(guacStatus.GetHTTPStatusCode())
|
|
}
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
err := s.handleTunnelRequestCore(w, r)
|
|
if err == nil {
|
|
return
|
|
}
|
|
guacErr := err.(*ErrGuac)
|
|
switch guacErr.Kind {
|
|
case ErrClient:
|
|
logx.Warnf("HTTP tunnel request rejected: %s", err.Error())
|
|
s.sendError(w, guacErr.Status, err.Error())
|
|
default:
|
|
logx.Errorf("HTTP tunnel request failed: %s", err.Error())
|
|
s.sendError(w, guacErr.Status, "Internal server error.")
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *Server) handleTunnelRequestCore(response http.ResponseWriter, request *http.Request) (err error) {
|
|
query := request.URL.RawQuery
|
|
if len(query) == 0 {
|
|
return ErrClient.NewError("No query string provided.")
|
|
}
|
|
|
|
// Call the supplied connect callback upon HTTP connect request
|
|
if query == "connect" {
|
|
tunnel, e := s.connect(request)
|
|
if e != nil {
|
|
err = ErrResourceNotFound.NewError("No tunnel created.", e.Error())
|
|
return
|
|
}
|
|
|
|
s.registerTunnel(tunnel)
|
|
|
|
// Ensure buggy browsers do not cache response
|
|
response.Header().Set("Cache-Control", "no-cache")
|
|
|
|
_, e = response.Write([]byte(tunnel.GetUUID()))
|
|
|
|
if e != nil {
|
|
err = ErrServer.NewError(e.Error())
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// Connect has already been called so we use the UUID to do read and writes to the existing session
|
|
if strings.HasPrefix(query, readPrefix) && len(query) >= readPrefixLength+uuidLength {
|
|
err = s.doRead(response, request, query[readPrefixLength:readPrefixLength+uuidLength])
|
|
} else if strings.HasPrefix(query, writePrefix) && len(query) >= writePrefixLength+uuidLength {
|
|
err = s.doWrite(response, request, query[writePrefixLength:writePrefixLength+uuidLength])
|
|
} else {
|
|
err = ErrClient.NewError("Invalid tunnel operation: " + query)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// doRead takes guacd messages and sends them in the response
|
|
func (s *Server) doRead(response http.ResponseWriter, request *http.Request, tunnelUUID string) error {
|
|
tunnel, err := s.getTunnel(tunnelUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reader := tunnel.AcquireReader()
|
|
defer tunnel.ReleaseReader()
|
|
|
|
// Note that although we are sending text, Webkit browsers will
|
|
// buffer 1024 bytes before starting a normal stream if we use
|
|
// anything but application/octet-stream.
|
|
response.Header().Set("Content-Type", "application/octet-stream")
|
|
response.Header().Set("Cache-Control", "no-cache")
|
|
|
|
if v, ok := response.(http.Flusher); ok {
|
|
v.Flush()
|
|
}
|
|
|
|
err = s.writeSome(response, reader, tunnel)
|
|
|
|
if err == nil {
|
|
// success
|
|
return err
|
|
}
|
|
|
|
switch err.(*ErrGuac).Kind {
|
|
// Send end-of-stream marker and close tunnel if connection is closed
|
|
case ErrConnectionClosed:
|
|
s.deregisterTunnel(tunnel)
|
|
tunnel.Close()
|
|
|
|
// End-of-instructions marker
|
|
_, _ = response.Write([]byte("0.;"))
|
|
if v, ok := response.(http.Flusher); ok {
|
|
v.Flush()
|
|
}
|
|
default:
|
|
logx.Debugf("Error writing to output, %v", err)
|
|
s.deregisterTunnel(tunnel)
|
|
tunnel.Close()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// writeSome drains the guacd buffer holding instructions into the response
|
|
func (s *Server) writeSome(response http.ResponseWriter, guacd InstructionReader, tunnel Tunnel) (err error) {
|
|
var message []byte
|
|
|
|
for {
|
|
message, err = guacd.ReadSome()
|
|
if err != nil {
|
|
s.deregisterTunnel(tunnel)
|
|
tunnel.Close()
|
|
return
|
|
}
|
|
|
|
if len(message) == 0 {
|
|
return
|
|
}
|
|
|
|
_, e := response.Write(message)
|
|
if e != nil {
|
|
err = ErrOther.NewError(e.Error())
|
|
return
|
|
}
|
|
|
|
if !guacd.Available() {
|
|
if v, ok := response.(http.Flusher); ok {
|
|
v.Flush()
|
|
}
|
|
}
|
|
|
|
// No more messages another guacd can take over
|
|
if tunnel.HasQueuedReaderThreads() {
|
|
break
|
|
}
|
|
}
|
|
|
|
// End-of-instructions marker
|
|
if _, err = response.Write([]byte("0.;")); err != nil {
|
|
return err
|
|
}
|
|
if v, ok := response.(http.Flusher); ok {
|
|
v.Flush()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// doWrite takes data from the request and sends it to guacd
|
|
func (s *Server) doWrite(response http.ResponseWriter, request *http.Request, tunnelUUID string) error {
|
|
tunnel, err := s.getTunnel(tunnelUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We still need to set the content type to avoid the default of
|
|
// text/html, as such a content type would cause some browsers to
|
|
// attempt to parse the result, even though the JavaScript client
|
|
// does not explicitly request such parsing.
|
|
response.Header().Set("Content-Type", "application/octet-stream")
|
|
response.Header().Set("Cache-Control", "no-cache")
|
|
response.Header().Set("Content-Length", "0")
|
|
|
|
writer := tunnel.AcquireWriter()
|
|
defer tunnel.ReleaseWriter()
|
|
|
|
_, err = io.Copy(writer, request.Body)
|
|
|
|
if err != nil {
|
|
s.deregisterTunnel(tunnel)
|
|
if err = tunnel.Close(); err != nil {
|
|
logx.Debugf("Error closing tunnel: %v", err)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|