mirror of
https://github.com/elyby/accounts-profiles-endpoint.git
synced 2024-12-22 13:09:51 +05:30
ebe85fba93
* 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>
193 lines
5.0 KiB
Go
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
|
|
}
|