accounts-profiles-endpoint/internal/http/mojang_api.go
Octol1ttle ebe85fba93
feat: sign custom Ely property (#2)
* feat: sign custom Ely property

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* fix: avoid duplicating custom property value across code

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>

* Remove customPropertySignature cache (and fix race condition)

---------

Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
2024-12-02 11:18:15 +01:00

193 lines
5.0 KiB
Go

package http
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
var timeNow = time.Now
var emptyTextures = []byte("{}")
var customPropertyValue = []byte("but why are you asking?")
type AccountsRepository interface {
FindUsernameByUuid(ctx context.Context, uuid string) (string, error)
// Should return uuid, correctly cased username and an error
FindUuidByUsername(ctx context.Context, username string) (string, string, error)
}
type TexturesProvider interface {
GetTexturesByUsername(ctx context.Context, username string) ([]byte, error)
}
// SignerService uses context because in the future we may separate this logic as an external microservice
type SignerService interface {
Sign(ctx context.Context, data []byte) ([]byte, error)
GetPublicKey(ctx context.Context, format string) ([]byte, error)
}
type MojangApi struct {
AccountsRepository
TexturesProvider
SignerService
}
func NewMojangApi(
accountsRepository AccountsRepository,
texturesProvider TexturesProvider,
signerService SignerService,
) MojangApi {
return MojangApi{
AccountsRepository: accountsRepository,
TexturesProvider: texturesProvider,
SignerService: signerService,
}
}
func (s *MojangApi) DefineRoutes(r gin.IRouter) {
r.GET("/api/minecraft/session/profile/:uuid", s.getProfileByUuidHandler)
r.GET("/api/mojang/profiles/:username", s.getUuidByUsernameHandler)
}
func (s *MojangApi) getProfileByUuidHandler(c *gin.Context) {
uuid, err := formatUuid(c.Param("uuid"))
if err != nil {
c.Status(http.StatusNoContent)
return
}
username, err := s.AccountsRepository.FindUsernameByUuid(c.Request.Context(), uuid)
if err != nil {
c.Error(fmt.Errorf("unable to retrieve account information: %w", err))
return
}
if username == "" {
c.Status(http.StatusNoContent)
return
}
textures, err := s.TexturesProvider.GetTexturesByUsername(c.Request.Context(), username)
if err != nil {
c.Error(fmt.Errorf("unable to retrieve textures information: %w", err))
return
}
if textures == nil {
textures = emptyTextures
}
serializedProfile, err := s.createProfileResponse(c.Request.Context(), uuid, username, textures, c.Query("unsigned") == "false")
if err != nil {
c.Error(fmt.Errorf("unable to create a profile response: %w", err))
return
}
c.Data(http.StatusOK, "application/json", serializedProfile)
}
func (s *MojangApi) getUuidByUsernameHandler(c *gin.Context) {
uuid, username, err := s.AccountsRepository.FindUuidByUsername(c.Request.Context(), c.Param("username"))
if err != nil {
c.Error(fmt.Errorf("unable to retrieve user's uuid and correctly cased username: %w", err))
return
}
if uuid == "" {
c.Status(http.StatusNoContent)
return
}
c.JSON(http.StatusOK, gin.H{
"id": strings.ReplaceAll(uuid, "-", ""),
"name": username,
})
}
func (s *MojangApi) createProfileResponse(
ctx context.Context,
uuid string,
username string,
texturesJson []byte,
sign bool,
) ([]byte, error) {
uuidWithoutDashes := strings.ReplaceAll(uuid, "-", "")
texturesPropValueJson := fmt.Appendf(
nil,
`{"timestamp":%d,"profileId":%q,"profileName":%q,"textures":%s}`,
timeNow().UnixMilli(),
uuidWithoutDashes,
username,
texturesJson,
)
encodedTexturesBuf := make([]byte, base64.StdEncoding.EncodedLen(len(texturesPropValueJson)))
base64.StdEncoding.Encode(encodedTexturesBuf, texturesPropValueJson)
result := fmt.Appendf(
nil,
`{"id":%q,"name":%q,"properties":[{"name":"textures","value":%q`,
uuidWithoutDashes,
username,
encodedTexturesBuf,
)
if sign {
textureSignature, err := s.signAndEncodeBase64(ctx, encodedTexturesBuf)
if err != nil {
return nil, fmt.Errorf("unable to sign textures: %w", err)
}
result = fmt.Appendf(result, `,"signature":%q`, textureSignature)
}
result = fmt.Appendf(result, `},{"name":"ely","value":%q`, customPropertyValue)
if sign {
// Despite the fact that the signed value itself is always the same,
// the signature service may rotate keys and at some point the signature will change.
// It is better to avoid the cache for now and add it on the signature service side later.
customPropertySignature, err := s.signAndEncodeBase64(ctx, customPropertyValue)
if err != nil {
return nil, fmt.Errorf("unable to sign custom property: %w", err)
}
result = fmt.Appendf(result, `,"signature":%q`, customPropertySignature)
}
result = fmt.Appendf(result, `}]}`)
return result, nil
}
func (s *MojangApi) signAndEncodeBase64(ctx context.Context, data []byte) ([]byte, error) {
signature, err := s.SignerService.Sign(ctx, data)
if err != nil {
return nil, err
}
encodedSignatureBuf := make([]byte, base64.StdEncoding.EncodedLen(len(signature)))
base64.StdEncoding.Encode(encodedSignatureBuf, signature)
return encodedSignatureBuf, nil
}
var invalidUuid = errors.New("invalid uuid")
func formatUuid(input string) (string, error) {
uuid := strings.ReplaceAll(input, "-", "")
if len(uuid) != 32 {
return "", invalidUuid
}
return fmt.Sprintf("%s-%s-%s-%s-%s", uuid[0:8], uuid[8:12], uuid[12:16], uuid[16:20], uuid[20:]), nil
}