Files
backend/internal/core/profile_service.go

120 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"time"
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
"gitea.mrixs.me/minecraft-platform/backend/internal/models"
"github.com/google/uuid"
)
type ProfileService struct {
UserRepo *database.UserRepository
privateKey *rsa.PrivateKey
domain string
}
// NewProfileService создает новый сервис и загружает приватный ключ.
func NewProfileService(repo *database.UserRepository, keyPath, domain string) (*ProfileService, error) {
keyData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
block, _ := pem.Decode(keyData)
if block == nil {
return nil, errors.New("failed to parse PEM block containing the key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Попробуем PKCS#8, если PKCS#1 не удался
key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes)
if errPkcs8 != nil {
return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8)
}
var ok bool
privateKey, ok = key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("key is not an RSA private key")
}
}
return &ProfileService{
UserRepo: repo,
privateKey: privateKey,
domain: domain,
}, nil
}
// GetSignedProfile формирует и подписывает профиль игрока.
func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.UUID) (*models.SessionProfileResponse, error) {
user, profile, err := s.UserRepo.GetProfileByUUID(ctx, playerUUID)
if err != nil {
return nil, err
}
// 1. Формируем структуру со свойствами текстур
textures := models.Textures{}
if profile.SkinHash != "" {
textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)}
}
if profile.CapeHash != "" {
textures.CAPE = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.CapeHash)}
}
profileID := strings.ReplaceAll(user.UUID.String(), "-", "")
propValue := models.ProfilePropertyValue{
Timestamp: time.Now().UnixMilli(),
ProfileID: profileID,
ProfileName: user.Username,
Textures: textures,
}
// 2. Маршализация в JSON и кодирование в Base64
valueJSON, err := json.Marshal(propValue)
if err != nil {
return nil, err
}
valueBase64 := base64.StdEncoding.EncodeToString(valueJSON)
// 3. Подпись
hasher := sha1.New()
hasher.Write([]byte(valueBase64))
hashed := hasher.Sum(nil)
signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA1, hashed)
if err != nil {
return nil, err
}
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
// 4. Формирование итогового ответа
response := &models.SessionProfileResponse{
ID: profileID,
Name: user.Username,
Properties: []models.ProfileProperty{
{
Name: "textures",
Value: valueBase64,
Signature: signatureBase64,
},
},
}
return response, nil
}