mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
239 lines
6.3 KiB
Go
239 lines
6.3 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"clicd/internal/config"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type webVNCTicket struct {
|
|
ContainerName string
|
|
ContainerUUID string
|
|
SubUser bool
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
var webVNCTickets = struct {
|
|
sync.Mutex
|
|
items map[string]webVNCTicket
|
|
}{items: map[string]webVNCTicket{}}
|
|
|
|
func HandleVNCTicket(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
ContainerName string `json:"container_name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ContainerName == "" {
|
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Container name required"})
|
|
return
|
|
}
|
|
if !isContainerAllowedForRequest(r, req.ContainerName) {
|
|
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: "Access denied to this container"})
|
|
return
|
|
}
|
|
c := config.FindContainerByName(req.ContainerName)
|
|
if c == nil {
|
|
jsonResponse(w, http.StatusNotFound, APIResponse{Success: false, Message: "Container not found"})
|
|
return
|
|
}
|
|
if isSubUserRequest(r) && c.PolicyBlocked {
|
|
jsonResponse(w, http.StatusForbidden, APIResponse{Success: false, Message: policyBlockedMessage(c)})
|
|
return
|
|
}
|
|
if !c.IsKVM() {
|
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "VNC console is only available for KVM VMs"})
|
|
return
|
|
}
|
|
|
|
ticket := randomHex(32)
|
|
webVNCTickets.Lock()
|
|
cleanupExpiredWebVNCTicketsLocked(time.Now())
|
|
webVNCTickets.items[ticket] = webVNCTicket{
|
|
ContainerName: c.Name,
|
|
ContainerUUID: c.UUID,
|
|
SubUser: isSubUserRequest(r),
|
|
ExpiresAt: time.Now().Add(60 * time.Second),
|
|
}
|
|
webVNCTickets.Unlock()
|
|
|
|
jsonResponse(w, http.StatusOK, APIResponse{
|
|
Success: true,
|
|
Data: map[string]string{"ticket": ticket},
|
|
})
|
|
}
|
|
|
|
// HandleVNCProxy proxies a KVM VM's local libvirt VNC socket to the browser.
|
|
func HandleVNCProxy(w http.ResponseWriter, r *http.Request) {
|
|
ticket := webVNCTicketFromRequest(r)
|
|
if ticket == "" {
|
|
http.Error(w, "ticket required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
containerName := r.URL.Query().Get("container")
|
|
if containerName == "" {
|
|
http.Error(w, "container name required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
item, ok := consumeWebVNCTicket(ticket, containerName)
|
|
if !ok {
|
|
http.Error(w, "invalid or expired ticket", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
c := config.FindContainerByName(containerName)
|
|
if c == nil || c.UUID != item.ContainerUUID {
|
|
http.Error(w, "container not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if item.SubUser && c.PolicyBlocked {
|
|
http.Error(w, "虚拟机被策略临时封禁", http.StatusForbidden)
|
|
return
|
|
}
|
|
if !c.IsKVM() {
|
|
http.Error(w, "VNC console is only available for KVM VMs", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if c.Status != "running" {
|
|
http.Error(w, "container is not running", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
vncPort, err := kvmManager.RefreshVNCPort(c.ID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("VNC display is not available: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
vncConn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", vncPort)), 5*time.Second)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("VNC connection failed: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer vncConn.Close()
|
|
|
|
responseHeader := http.Header{}
|
|
if protocol := webVNCResponseProtocol(r); protocol != "" {
|
|
responseHeader.Set("Sec-WebSocket-Protocol", protocol)
|
|
}
|
|
ws, err := upgrader.Upgrade(w, r, responseHeader)
|
|
if err != nil {
|
|
log.Printf("WebVNC upgrade failed: %v", err)
|
|
return
|
|
}
|
|
defer ws.Close()
|
|
|
|
log.Printf("WebVNC connected for container %s -> 127.0.0.1:%d", containerName, vncPort)
|
|
|
|
done := make(chan string, 2)
|
|
var writeMu sync.Mutex
|
|
go streamVNCToWebSocket(ws, &writeMu, vncConn, done)
|
|
go streamWebSocketToVNC(ws, vncConn, done)
|
|
|
|
reason := <-done
|
|
_ = vncConn.Close()
|
|
_ = ws.Close()
|
|
log.Printf("WebVNC disconnected for container %s: %s", containerName, reason)
|
|
}
|
|
|
|
func webVNCTicketFromRequest(r *http.Request) string {
|
|
for _, protocol := range websocket.Subprotocols(r) {
|
|
const prefix = "clicd-vnc-ticket."
|
|
if len(protocol) > len(prefix) && protocol[:len(prefix)] == prefix {
|
|
return protocol[len(prefix):]
|
|
}
|
|
}
|
|
return r.URL.Query().Get("ticket")
|
|
}
|
|
|
|
func webVNCResponseProtocol(r *http.Request) string {
|
|
for _, protocol := range websocket.Subprotocols(r) {
|
|
if protocol == "binary" {
|
|
return protocol
|
|
}
|
|
}
|
|
for _, protocol := range websocket.Subprotocols(r) {
|
|
const prefix = "clicd-vnc-ticket."
|
|
if len(protocol) > len(prefix) && protocol[:len(prefix)] == prefix {
|
|
return protocol
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func consumeWebVNCTicket(ticket, containerName string) (webVNCTicket, bool) {
|
|
now := time.Now()
|
|
webVNCTickets.Lock()
|
|
defer webVNCTickets.Unlock()
|
|
cleanupExpiredWebVNCTicketsLocked(now)
|
|
item, ok := webVNCTickets.items[ticket]
|
|
if !ok {
|
|
return webVNCTicket{}, false
|
|
}
|
|
delete(webVNCTickets.items, ticket)
|
|
return item, item.ContainerName == containerName && now.Before(item.ExpiresAt)
|
|
}
|
|
|
|
func cleanupExpiredWebVNCTicketsLocked(now time.Time) {
|
|
for ticket, item := range webVNCTickets.items {
|
|
if !now.Before(item.ExpiresAt) {
|
|
delete(webVNCTickets.items, ticket)
|
|
}
|
|
}
|
|
}
|
|
|
|
func streamVNCToWebSocket(ws *websocket.Conn, writeMu *sync.Mutex, src io.Reader, done chan<- string) {
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, err := src.Read(buf)
|
|
if n > 0 {
|
|
writeMu.Lock()
|
|
writeErr := ws.WriteMessage(websocket.BinaryMessage, buf[:n])
|
|
writeMu.Unlock()
|
|
if writeErr != nil {
|
|
done <- fmt.Sprintf("browser websocket write failed: %v", writeErr)
|
|
return
|
|
}
|
|
}
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
done <- "VNC server closed connection"
|
|
} else {
|
|
done <- fmt.Sprintf("VNC server read failed: %v", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func streamWebSocketToVNC(ws *websocket.Conn, dst net.Conn, done chan<- string) {
|
|
for {
|
|
messageType, msg, err := ws.ReadMessage()
|
|
if err != nil {
|
|
done <- fmt.Sprintf("browser websocket read failed: %v", err)
|
|
return
|
|
}
|
|
if messageType != websocket.BinaryMessage && messageType != websocket.TextMessage {
|
|
continue
|
|
}
|
|
if _, err := dst.Write(msg); err != nil {
|
|
done <- fmt.Sprintf("VNC server write failed: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|