Files
backend/internal/core/profile_service.go

174 lines
5.1 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/hex" // Для преобразования хеша в строку
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"image/png" // Для валидации PNG
"io"
"mime/multipart"
"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
}
// UpdateUserSkin обрабатывает загрузку, валидацию и сохранение файла скина.
func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file multipart.File, header *multipart.FileHeader) error {
// 1. Читаем файл в память
fileBytes, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
// Возвращаем указатель файла в начало, чтобы его можно было прочитать снова
file.Seek(0, 0)
// 2. Валидация PNG 64x64
config, err := png.DecodeConfig(file)
if err != nil {
return errors.New("invalid PNG file")
}
if config.Width != 64 || config.Height != 64 {
return errors.New("skin must be 64x64 pixels")
}
// 3. Вычисляем SHA1 хеш
hasher := sha1.New()
hasher.Write(fileBytes)
hash := hex.EncodeToString(hasher.Sum(nil))
// 4. Сохраняем файл на диск
// Путь к хранилищу текстур должен быть конфигурируемым
storagePath := os.Getenv("TEXTURES_STORAGE_PATH")
if storagePath == "" {
return errors.New("textures storage path not configured")
}
filePath := fmt.Sprintf("%s/%s", storagePath, hash)
// Создаем файл только если его еще нет
if _, err := os.Stat(filePath); os.IsNotExist(err) {
outFile, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("failed to create skin file: %w", err)
}
defer outFile.Close()
_, err = outFile.Write(fileBytes)
if err != nil {
return fmt.Errorf("failed to write skin file: %w", err)
}
}
// 5. Сохраняем хеш в БД
return s.UserRepo.UpdateSkinHash(ctx, userID, hash)
}