diff --git a/account.go b/account.go index e096c42..f8f661d 100644 --- a/account.go +++ b/account.go @@ -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].: 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() { diff --git a/common.go b/common.go index 56faef6..fb93ac6 100644 --- a/common.go +++ b/common.go @@ -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" @@ -687,7 +688,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 @@ -695,38 +696,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 @@ -806,11 +786,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) { @@ -844,11 +820,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, diff --git a/main.go b/main.go index fe042df..c88da73 100644 --- a/main.go +++ b/main.go @@ -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) } }