package handler import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/1024XEngineer/xinfra/server/internal/config" "github.com/1024XEngineer/xinfra/server/internal/service" gmsm2 "github.com/tjfoc/gmsm/sm2" gmx509 "github.com/tjfoc/gmsm/x509" "github.com/gin-gonic/gin" ) type CloudDMHandler struct { cfg config.Config audit *service.AuditService } func NewCloudDMHandler(cfg config.Config, audit *service.AuditService) *CloudDMHandler { return &CloudDMHandler{cfg: cfg, audit: audit} } func (h *CloudDMHandler) Login(c *gin.Context) { claims, ok := CurrentClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) return } targetURL := strings.TrimSpace(h.cfg.CloudDMTargetURL) if targetURL == "" { h.writeAudit(c, claims.UserID, claims.Username, "deny", "clouddm target url is not configured") c.JSON(http.StatusServiceUnavailable, gin.H{"error": "clouddm target url is not configured"}) return } if claims.IsAdmin { targetURL, err := h.adminLogin(c, targetURL) if err != nil { h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error()) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } h.writeAudit(c, claims.UserID, claims.Username, "allow", "platform admin uses clouddm admin account") if strings.Contains(c.GetHeader("Accept"), "application/json") { c.JSON(http.StatusOK, gin.H{"target_url": targetURL}) return } c.Redirect(http.StatusFound, targetURL) return } jumpURL, err := h.loginJumpURL(targetURL) if err != nil { h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error()) c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } h.writeAudit(c, claims.UserID, claims.Username, "allow", "") if strings.Contains(c.GetHeader("Accept"), "application/json") { c.JSON(http.StatusOK, gin.H{"target_url": jumpURL}) return } c.Redirect(http.StatusFound, jumpURL) } func (h *CloudDMHandler) loginJumpURL(targetURL string) (string, error) { requestURL := strings.TrimRight(targetURL, "/") + "/requestJumpUrl" body, _ := json.Marshal(gin.H{"type": "OIDC"}) req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("clouddm requestJumpUrl failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) } var result struct { Success bool `json:"success"` Data string `json:"data"` Msg string `json:"msg"` MsgContent string `json:"msgContent"` } if err := json.Unmarshal(raw, &result); err != nil { return "", err } if !result.Success || strings.TrimSpace(result.Data) == "" { reason := strings.TrimSpace(result.MsgContent) if reason == "" { reason = strings.TrimSpace(result.Msg) } if reason == "" { reason = "empty clouddm jump url" } return "", fmt.Errorf("clouddm requestJumpUrl failed: %s", reason) } return result.Data, nil } func (h *CloudDMHandler) adminLogin(c *gin.Context, targetURL string) (string, error) { username := strings.TrimSpace(h.cfg.CloudDMAdminUsername) password := h.cfg.CloudDMAdminPassword if username == "" || password == "" { return "", fmt.Errorf("clouddm admin account is not configured") } publicKey, err := h.fetchPublicKey(targetURL) if err != nil { return "", err } encryptedPassword, err := encryptCloudDMPassword(publicKey, password) if err != nil { return "", err } loginURL := strings.TrimSpace(h.cfg.CloudDMLoginURL) if loginURL == "" { loginURL = strings.TrimRight(targetURL, "/") + "/login" } body, _ := json.Marshal(gin.H{ "accountType": "SUB_ACCOUNT", "loginType": "PASSWORD", "account": username, "password": encryptedPassword, }) req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewReader(body)) if err != nil { return "", err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("clouddm admin login failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) } var result struct { Success bool `json:"success"` Data struct { NeedMore bool `json:"needMore"` NeedMfa bool `json:"needMfa"` } `json:"data"` Msg string `json:"msg"` MsgContent string `json:"msgContent"` } if err := json.Unmarshal(raw, &result); err != nil { return "", err } if !result.Success || result.Data.NeedMore || result.Data.NeedMfa { reason := strings.TrimSpace(result.MsgContent) if reason == "" { reason = strings.TrimSpace(result.Msg) } if reason == "" { reason = "admin login did not complete" } return "", fmt.Errorf("clouddm admin login failed: %s", reason) } for _, cookie := range resp.Header.Values("Set-Cookie") { c.Writer.Header().Add("Set-Cookie", cookie) } return h.publicSQLURL(), nil } func (h *CloudDMHandler) fetchPublicKey(targetURL string) (string, error) { requestURL := strings.TrimRight(targetURL, "/") + "/api/entry/dmGlobalSettings" req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader([]byte("{}"))) if err != nil { return "", err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return "", fmt.Errorf("clouddm dmGlobalSettings failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw))) } var result struct { Success bool `json:"success"` Data struct { PublicKey string `json:"publicKey"` } `json:"data"` Msg string `json:"msg"` MsgContent string `json:"msgContent"` } if err := json.Unmarshal(raw, &result); err != nil { return "", err } if !result.Success || strings.TrimSpace(result.Data.PublicKey) == "" { reason := strings.TrimSpace(result.MsgContent) if reason == "" { reason = strings.TrimSpace(result.Msg) } if reason == "" { reason = "empty public key" } return "", fmt.Errorf("clouddm dmGlobalSettings failed: %s", reason) } return result.Data.PublicKey, nil } func encryptCloudDMPassword(publicKey, password string) (string, error) { pub, err := gmx509.ReadPublicKeyFromHex(strings.TrimSpace(publicKey)) if err != nil { return "", err } ciphertext, err := gmsm2.Encrypt(pub, []byte(password), rand.Reader, gmsm2.C1C3C2) if err != nil { return "", err } return hex.EncodeToString(ciphertext), nil } func (h *CloudDMHandler) publicSQLURL() string { base := strings.TrimSpace(h.cfg.CloudDMPublicURL) if base == "" { if redirectURI, err := url.Parse(strings.TrimSpace(h.cfg.CloudDMRedirectURI)); err == nil && redirectURI.Scheme != "" && redirectURI.Host != "" { base = redirectURI.Scheme + "://" + redirectURI.Host } } if base == "" { base = strings.TrimSpace(h.cfg.CloudDMTargetURL) } base = strings.TrimRight(base, "/") if strings.Contains(base, "#") { return base } return base + "/#/sql" } func (h *CloudDMHandler) writeAudit(c *gin.Context, userID uint64, username, decision, reason string) { h.audit.Write(service.AuditEntry{ ActorUserID: userID, ActorUsername: username, ClientIP: c.ClientIP(), UserAgent: c.Request.UserAgent(), Action: "clouddm.login", ResourceType: "clouddm", Decision: decision, Reason: reason, }) }