Look up fallback player ID using POST /profiles/minecraft

authlib-injector specifies POST /profiles/minecraft as the only
available route for player name -> UUID, so we have to use it if we want
to support authlib-injector-compatible fallback API servers.
This commit is contained in:
Evan Goode 2025-04-03 18:03:35 -04:00
parent 770ceededb
commit 4ea506eae1
3 changed files with 50 additions and 43 deletions

View File

@ -59,7 +59,7 @@ func (fallbackAPIServer *FallbackAPIServer) PlayerNamesToIDs(remainingLowerNames
return responses
}
func (fallbackAPIServer *FallbackAPIServer) PlayerNamesToIDsWorker() {
func (app *App) PlayerNamesToIDsWorker(fallbackAPIServer *FallbackAPIServer) {
// All communication with the POST /profiles/minecraft (a.k.a. POST
// /minecraft/profile/lookup/bulk/byname) route on a fallback API server is
// done by a single goroutine running this function. It buffers a queue of
@ -93,6 +93,16 @@ func (fallbackAPIServer *FallbackAPIServer) PlayerNamesToIDsWorker() {
}
}
// Double-check for validity, invalid player names will spoil
// the entire batch. We will assume that if a player name is
// valid to Drasl, it is valid on all fallback API servers (if
// this becomes a problem in the future, we may need a
// FallbackAPIServer.ValidPlayerNameRegex.
if app.ValidatePlayerName(job.LowerName) != nil {
job.ReturnCh <- mo.None[PlayerNameToIDResponse]()
continue
}
if _, ok := lowerNameToResponseChs[job.LowerName]; !ok {
lowerNameQueue = append(lowerNameQueue, &job.LowerName)
}
@ -197,6 +207,17 @@ func AccountPlayerNameToID(app *App) func(c echo.Context) error {
}
lowerName := strings.ToLower(playerName)
if app.ValidatePlayerName(lowerName) != nil {
// This error message is consistent with POST
// https://api.mojang.com/users/profiles/minecraft/:playerName as
// of 2025-04-03
errorMessage := fmt.Sprintf("getProfileName.name: Invalid profile name")
return &YggdrasilError{
Code: http.StatusBadRequest,
Error_: mo.Some("CONSTRAINT_VIOLATION"),
ErrorMessage: mo.Some(errorMessage),
}
}
var player Player
result := app.DB.First(&player, "name = ?", lowerName)
@ -246,7 +267,8 @@ func AccountPlayerNamesToIDs(app *App) func(c echo.Context) error {
Error_: mo.Some("CONSTRAINT_VIOLATION"),
ErrorMessage: mo.Some(errorMessage),
}
} else if len(playerNames) > MAX_PLAYER_NAMES_TO_IDS {
}
if len(playerNames) > MAX_PLAYER_NAMES_TO_IDS {
// This error message is consistent with POST
// https://api.mojang.com/profiles/minecraft as of 2025-04-02
errorMessage := fmt.Sprintf("getProfileName.profileNames: size must be between 0 and %d", MAX_PLAYER_NAMES_TO_IDS)
@ -271,7 +293,20 @@ func AccountPlayerNamesToIDs(app *App) func(c echo.Context) error {
ErrorMessage: mo.Some(errorMessage),
}
}
remainingLowerNames.Add(strings.ToLower(playerName))
lowerName := strings.ToLower(playerName)
if app.ValidatePlayerName(lowerName) != nil {
// This error message is consistent with POST
// https://api.mojang.com/profiles/minecraft as of 2025-04-03
errorMessage := fmt.Sprintf("getProfileName.profileNames[%d].<list element>: Invalid profile name", i)
return &YggdrasilError{
Code: http.StatusBadRequest,
Error_: mo.Some("CONSTRAINT_VIOLATION"),
ErrorMessage: mo.Some(errorMessage),
}
}
remainingLowerNames.Add(lowerName)
}
for _, lowerName := range remainingLowerNames.ToSlice() {

View File

@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
mapset "github.com/deckarep/golang-set/v2"
"github.com/dgraph-io/ristretto"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
@ -706,7 +707,7 @@ func (app *App) GetFallbackSkinTexturesProperty(player *Player) (*SessionProfile
fallbackPlayer = player.FallbackPlayer
}
for _, fallbackAPIServer := range app.Config.FallbackAPIServers {
for _, fallbackAPIServer := range app.FallbackAPIServers {
var id string
if fallbackPlayerIsUUID {
// If we have the UUID already, use it
@ -714,38 +715,17 @@ func (app *App) GetFallbackSkinTexturesProperty(player *Player) (*SessionProfile
} else {
// Otherwise, we only know the player name. Query the fallback API
// server to get the fallback player's UUID
// TODO this should POST /profiles/minecraft instead to be authlib-injector-compatible
reqURL, err := url.JoinPath(fallbackAPIServer.AccountURL, "/users/profiles/minecraft/", fallbackPlayer)
if err != nil {
log.Println(err)
lowerName := strings.ToLower(fallbackPlayer)
fallbackResponses := fallbackAPIServer.PlayerNamesToIDs(mapset.NewSet(lowerName))
if len(fallbackResponses) == 1 && strings.EqualFold(lowerName, fallbackResponses[0].Name) {
id = fallbackResponses[0].ID
} else {
continue
}
res, err := app.CachedGet(reqURL, fallbackAPIServer.CacheTTLSeconds)
if err != nil {
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
continue
}
if res.StatusCode != http.StatusOK {
// Be silent, 404s will be common here
continue
}
var playerResponse PlayerNameToIDResponse
err = json.Unmarshal(res.BodyBytes, &playerResponse)
if err != nil {
log.Printf("Received invalid response from fallback API server at %s\n", reqURL)
continue
}
id = playerResponse.ID
}
reqURL, err := url.JoinPath(fallbackAPIServer.SessionURL, "session/minecraft/profile", id)
if err != nil {
log.Println(err)
continue
}
res, err := app.CachedGet(reqURL+"?unsigned=false", fallbackAPIServer.CacheTTLSeconds)
reqURL := fallbackAPIServer.Config.SessionURL + "/session/minecraft/profile/" + url.PathEscape(id)
res, err := app.CachedGet(reqURL+"?unsigned=false", fallbackAPIServer.Config.CacheTTLSeconds)
if err != nil {
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
continue
@ -825,11 +805,7 @@ func (app *App) GetDefaultSkinTexture(player *Player) *texture {
return nil
}
defaultSkinURL, err := url.JoinPath(app.FrontEndURL, "web/texture/default-skin/"+filename)
if err != nil {
log.Printf("Error generating default skin URL for file %s\n", *defaultSkinPath)
return nil
}
defaultSkinURL := app.FrontEndURL + "/web/texture/default-skin/" + url.PathEscape(filename)
skinModel := SkinModelClassic
if slimSkinRegex.MatchString(*defaultSkinPath) {
@ -863,11 +839,7 @@ func (app *App) GetDefaultCapeTexture(player *Player) *texture {
return nil
}
defaultCapeURL, err := url.JoinPath(app.FrontEndURL, "web/texture/default-cape/"+filename)
if err != nil {
log.Printf("Error generating default cape URL for file %s\n", *defaultCapePath)
return nil
}
defaultCapeURL := app.FrontEndURL + "/web/texture/default-cape/" + url.PathEscape(filename)
return &texture{
URL: defaultCapeURL,

View File

@ -612,7 +612,7 @@ func setup(config *Config) *App {
func (app *App) Run() {
for _, fallbackAPIServer := range PtrSlice(app.FallbackAPIServers) {
go (*fallbackAPIServer).PlayerNamesToIDsWorker()
go app.PlayerNamesToIDsWorker(fallbackAPIServer)
}
}