package handler import ( "bytes" "carrotskin/internal/container" "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/pkg/utils" "io" "net/http" "regexp" "github.com/gin-gonic/gin" "go.uber.org/zap" ) // 常量定义 const ( ErrInternalServer = "服务器内部错误" // 错误类型 ErrInvalidEmailFormat = "邮箱格式不正确" ErrInvalidPassword = "密码必须至少包含8个字符,只能包含字母、数字和特殊字符" ErrWrongPassword = "密码错误" ErrUserNotMatch = "用户不匹配" // 错误消息 ErrInvalidRequest = "请求格式无效" ErrJoinServerFailed = "加入服务器失败" ErrServerIDRequired = "服务器ID不能为空" ErrUsernameRequired = "用户名不能为空" ErrSessionVerifyFailed = "会话验证失败" ErrProfileNotFound = "未找到用户配置文件" ErrInvalidParams = "无效的请求参数" ErrEmptyUserID = "用户ID为空" ErrUnauthorized = "无权操作此配置文件" ErrGetProfileService = "获取配置文件服务失败" // 成功信息 SuccessProfileCreated = "创建成功" MsgRegisterSuccess = "注册成功" // 错误消息 ErrGetProfile = "获取配置文件失败" ErrGetTextureService = "获取材质服务失败" ErrInvalidContentType = "无效的请求内容类型" ErrParseMultipartForm = "解析多部分表单失败" ErrGetFileFromForm = "从表单获取文件失败" ErrInvalidFileType = "无效的文件类型,仅支持PNG图片" ErrSaveTexture = "保存材质失败" ErrSetTexture = "设置材质失败" ErrGetTexture = "获取材质失败" // 内存限制 MaxMultipartMemory = 32 << 20 // 32 MB // 材质类型 TextureTypeSkin = "SKIN" TextureTypeCape = "CAPE" // 内容类型 ContentTypePNG = "image/png" ContentTypeMultipart = "multipart/form-data" // 表单参数 FormKeyModel = "model" FormKeyFile = "file" // 元数据键 MetaKeyModel = "model" ) // 正则表达式 var ( // 邮箱正则表达式 emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) // 密码强度正则表达式(最少8位,只允许字母、数字和特定特殊字符) passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#$%^&*]{8,}$`) ) // 请求结构体 type ( // AuthenticateRequest 认证请求 AuthenticateRequest struct { Agent map[string]interface{} `json:"agent"` ClientToken string `json:"clientToken"` Identifier string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` RequestUser bool `json:"requestUser"` } // ValidTokenRequest 验证令牌请求 ValidTokenRequest struct { AccessToken string `json:"accessToken" binding:"required"` ClientToken string `json:"clientToken"` } // RefreshRequest 刷新令牌请求 RefreshRequest struct { AccessToken string `json:"accessToken" binding:"required"` ClientToken string `json:"clientToken"` RequestUser bool `json:"requestUser"` SelectedProfile map[string]interface{} `json:"selectedProfile"` } // SignOutRequest 登出请求 SignOutRequest struct { Email string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // JoinServerRequest 加入服务器请求 JoinServerRequest struct { ServerID string `json:"serverId" binding:"required"` AccessToken string `json:"accessToken" binding:"required"` SelectedProfile string `json:"selectedProfile" binding:"required"` } ) // 响应结构体 type ( // AuthenticateResponse 认证响应 AuthenticateResponse struct { AccessToken string `json:"accessToken"` ClientToken string `json:"clientToken"` SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"` AvailableProfiles []map[string]interface{} `json:"availableProfiles"` User map[string]interface{} `json:"user,omitempty"` } // RefreshResponse 刷新令牌响应 RefreshResponse struct { AccessToken string `json:"accessToken"` ClientToken string `json:"clientToken"` SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"` User map[string]interface{} `json:"user,omitempty"` } ) // APIResponse API响应 type APIResponse struct { Status int `json:"status"` Data interface{} `json:"data"` Error interface{} `json:"error"` } // standardResponse 生成标准响应 func standardResponse(c *gin.Context, status int, data interface{}, err interface{}) { c.JSON(status, APIResponse{ Status: status, Data: data, Error: err, }) } // YggdrasilHandler Yggdrasil API处理器 type YggdrasilHandler struct { container *container.Container logger *zap.Logger } // NewYggdrasilHandler 创建YggdrasilHandler实例 func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler { return &YggdrasilHandler{ container: c, logger: c.Logger, } } // Authenticate 用户认证 func (h *YggdrasilHandler) Authenticate(c *gin.Context) { rawData, err := io.ReadAll(c.Request.Body) if err != nil { h.logger.Error("读取请求体失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) return } c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) var request AuthenticateRequest if err = c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析认证请求失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var userId int64 var profile *model.Profile var UUID string if emailRegex.MatchString(request.Identifier) { userId, err = service.GetUserIDByEmail(h.container.DB, request.Identifier) } else { profile, err = service.GetProfileByProfileName(h.container.DB, request.Identifier) if err != nil { h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err)) c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } userId = profile.UserID UUID = profile.UUID } if err != nil { h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err)) c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"}) return } if err := service.VerifyPassword(h.container.DB, request.Password, userId); err != nil { h.logger.Warn("认证失败: 密码错误", zap.Error(err)) c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword}) return } selectedProfile, availableProfiles, accessToken, clientToken, err := h.container.TokenService.Create(userId, UUID, request.ClientToken) if err != nil { h.logger.Error("生成令牌失败", zap.Error(err), zap.Int64("userId", userId)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } user, err := h.container.UserService.GetByID(userId) if err != nil { h.logger.Error("获取用户信息失败", zap.Error(err), zap.Int64("userId", userId)) } availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles)) for _, p := range availableProfiles { availableProfilesData = append(availableProfilesData, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *p)) } response := AuthenticateResponse{ AccessToken: accessToken, ClientToken: clientToken, AvailableProfiles: availableProfilesData, } if selectedProfile != nil { response.SelectedProfile = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *selectedProfile) } if request.RequestUser && user != nil { response.User = service.SerializeUser(h.logger, user, UUID) } h.logger.Info("用户认证成功", zap.Int64("userId", userId)) c.JSON(http.StatusOK, response) } // ValidToken 验证令牌 func (h *YggdrasilHandler) ValidToken(c *gin.Context) { var request ValidTokenRequest if err := c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析验证令牌请求失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if h.container.TokenService.Validate(request.AccessToken, request.ClientToken) { h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken)) c.JSON(http.StatusNoContent, gin.H{"valid": true}) } else { h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken)) c.JSON(http.StatusForbidden, gin.H{"valid": false}) } } // RefreshToken 刷新令牌 func (h *YggdrasilHandler) RefreshToken(c *gin.Context) { var request RefreshRequest if err := c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析刷新令牌请求失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } UUID, err := h.container.TokenService.GetUUIDByAccessToken(request.AccessToken) if err != nil { h.logger.Warn("刷新令牌失败: 无效的访问令牌", zap.String("token", request.AccessToken), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, _ := h.container.TokenService.GetUserIDByAccessToken(request.AccessToken) UUID = utils.FormatUUID(UUID) profile, err := h.container.ProfileService.GetByUUID(UUID) if err != nil { h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var profileData map[string]interface{} var userData map[string]interface{} var profileID string if request.SelectedProfile != nil { profileIDValue, ok := request.SelectedProfile["id"] if !ok { h.logger.Error("刷新令牌失败: 缺少配置文件ID", zap.Int64("userId", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": "缺少配置文件ID"}) return } profileID, ok = profileIDValue.(string) if !ok { h.logger.Error("刷新令牌失败: 配置文件ID类型错误", zap.Int64("userId", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": "配置文件ID必须是字符串"}) return } profileID = utils.FormatUUID(profileID) if profile.UserID != userID { h.logger.Warn("刷新令牌失败: 用户不匹配", zap.Int64("userId", userID), zap.Int64("profileUserId", profile.UserID), ) c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch}) return } profileData = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile) } user, _ := h.container.UserService.GetByID(userID) if request.RequestUser && user != nil { userData = service.SerializeUser(h.logger, user, UUID) } newAccessToken, newClientToken, err := h.container.TokenService.Refresh( request.AccessToken, request.ClientToken, profileID, ) if err != nil { h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.logger.Info("刷新令牌成功", zap.Int64("userId", userID)) c.JSON(http.StatusOK, RefreshResponse{ AccessToken: newAccessToken, ClientToken: newClientToken, SelectedProfile: profileData, User: userData, }) } // InvalidToken 使令牌失效 func (h *YggdrasilHandler) InvalidToken(c *gin.Context) { var request ValidTokenRequest if err := c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析使令牌失效请求失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.container.TokenService.Invalidate(request.AccessToken) h.logger.Info("令牌已失效", zap.String("token", request.AccessToken)) c.JSON(http.StatusNoContent, gin.H{}) } // SignOut 用户登出 func (h *YggdrasilHandler) SignOut(c *gin.Context) { var request SignOutRequest if err := c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析登出请求失败", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if !emailRegex.MatchString(request.Email) { h.logger.Warn("登出失败: 邮箱格式不正确", zap.String("email", request.Email)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat}) return } user, err := h.container.UserService.GetByEmail(request.Email) if err != nil || user == nil { h.logger.Warn("登出失败: 用户不存在", zap.String("email", request.Email), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"}) return } if err := service.VerifyPassword(h.container.DB, request.Password, user.ID); err != nil { h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword}) return } h.container.TokenService.InvalidateUserTokens(user.ID) h.logger.Info("用户登出成功", zap.Int64("userId", user.ID)) c.JSON(http.StatusNoContent, gin.H{"valid": true}) } // GetProfileByUUID 根据UUID获取档案 func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) { uuid := utils.FormatUUID(c.Param("uuid")) h.logger.Info("获取配置文件请求", zap.String("uuid", uuid)) profile, err := h.container.ProfileService.GetByUUID(uuid) if err != nil { h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid)) standardResponse(c, http.StatusInternalServerError, nil, err.Error()) return } h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name)) c.JSON(http.StatusOK, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile)) } // JoinServer 加入服务器 func (h *YggdrasilHandler) JoinServer(c *gin.Context) { var request JoinServerRequest clientIP := c.ClientIP() if err := c.ShouldBindJSON(&request); err != nil { h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP)) standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest) return } h.logger.Info("收到加入服务器请求", zap.String("serverId", request.ServerID), zap.String("userUUID", request.SelectedProfile), zap.String("ip", clientIP), ) if err := service.JoinServer(h.container.DB, h.logger, h.container.Redis, request.ServerID, request.AccessToken, request.SelectedProfile, clientIP); err != nil { h.logger.Error("加入服务器失败", zap.Error(err), zap.String("serverId", request.ServerID), zap.String("userUUID", request.SelectedProfile), zap.String("ip", clientIP), ) standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed) return } h.logger.Info("加入服务器成功", zap.String("serverId", request.ServerID), zap.String("userUUID", request.SelectedProfile), zap.String("ip", clientIP), ) c.Status(http.StatusNoContent) } // HasJoinedServer 验证玩家是否已加入服务器 func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) { clientIP, _ := c.GetQuery("ip") serverID, exists := c.GetQuery("serverId") if !exists || serverID == "" { h.logger.Warn("缺少服务器ID参数", zap.String("ip", clientIP)) standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired) return } username, exists := c.GetQuery("username") if !exists || username == "" { h.logger.Warn("缺少用户名参数", zap.String("serverId", serverID), zap.String("ip", clientIP)) standardResponse(c, http.StatusNoContent, nil, ErrUsernameRequired) return } h.logger.Info("收到会话验证请求", zap.String("serverId", serverID), zap.String("username", username), zap.String("ip", clientIP), ) if err := service.HasJoinedServer(h.logger, h.container.Redis, serverID, username, clientIP); err != nil { h.logger.Warn("会话验证失败", zap.Error(err), zap.String("serverId", serverID), zap.String("username", username), zap.String("ip", clientIP), ) standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed) return } profile, err := h.container.ProfileService.GetByUUID(username) if err != nil { h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username)) standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound) return } h.logger.Info("会话验证成功", zap.String("serverId", serverID), zap.String("username", username), zap.String("uuid", profile.UUID), ) c.JSON(200, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile)) } // GetProfilesByName 批量获取配置文件 func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) { var names []string if err := c.ShouldBindJSON(&names); err != nil { h.logger.Error("解析名称数组请求失败", zap.Error(err)) standardResponse(c, http.StatusBadRequest, nil, ErrInvalidParams) return } h.logger.Info("接收到批量获取配置文件请求", zap.Int("count", len(names))) profiles, err := h.container.ProfileService.GetByNames(names) if err != nil { h.logger.Error("获取配置文件失败", zap.Error(err)) } h.logger.Info("成功获取配置文件", zap.Int("requested", len(names)), zap.Int("returned", len(profiles))) c.JSON(http.StatusOK, profiles) } // GetMetaData 获取Yggdrasil元数据 func (h *YggdrasilHandler) GetMetaData(c *gin.Context) { meta := gin.H{ "implementationName": "CellAuth", "implementationVersion": "0.0.1", "serverName": "LittleLan's Yggdrasil Server Implementation.", "links": gin.H{ "homepage": "https://skin.littlelan.cn", "register": "https://skin.littlelan.cn/auth", }, "feature.non_email_login": true, "feature.enable_profile_key": true, } skinDomains := []string{".hitwh.games", ".littlelan.cn"} signature, err := service.GetPublicKeyFromRedisFunc(h.logger, h.container.Redis) if err != nil { h.logger.Error("获取公钥失败", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } h.logger.Info("提供元数据") c.JSON(http.StatusOK, gin.H{ "meta": meta, "skinDomains": skinDomains, "signaturePublickey": signature, }) } // GetPlayerCertificates 获取玩家证书 func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"}) c.Abort() return } bearerPrefix := "Bearer " if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) c.Abort() return } tokenID := authHeader[len(bearerPrefix):] if tokenID == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"}) c.Abort() return } uuid, err := h.container.TokenService.GetUUIDByAccessToken(tokenID) if uuid == "" { h.logger.Error("获取玩家UUID失败", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } uuid = utils.FormatUUID(uuid) certificate, err := service.GeneratePlayerCertificate(h.container.DB, h.logger, h.container.Redis, uuid) if err != nil { h.logger.Error("生成玩家证书失败", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } h.logger.Info("成功生成玩家证书") c.JSON(http.StatusOK, certificate) }