mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 10:56:06 -04:00
Implement GET /minecraft/profile/lookup/:id
New route on api.minecraftservices.com, see https://minecraft.wiki/w/Mojang_API#Query_player's_username
This commit is contained in:
parent
95893eb211
commit
d6d5c08131
@ -211,11 +211,10 @@ func AccountPlayerNameToID(app *App) func(c echo.Context) error {
|
||||
// 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),
|
||||
ErrorMessage: mo.Some("getProfileName.name: Invalid profile name"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,11 +260,10 @@ func AccountPlayerNamesToIDs(app *App) func(c echo.Context) error {
|
||||
if len(playerNames) == 0 {
|
||||
// This error message is consistent with POST
|
||||
// https://api.mojang.com/profiles/minecraft as of 2025-04-02
|
||||
errorMessage := fmt.Sprintf("getProfileName.profileNames: must not be empty")
|
||||
return &YggdrasilError{
|
||||
Code: http.StatusBadRequest,
|
||||
Error_: mo.Some("CONSTRAINT_VIOLATION"),
|
||||
ErrorMessage: mo.Some(errorMessage),
|
||||
ErrorMessage: mo.Some("getProfileName.profileNames: must not be empty"),
|
||||
}
|
||||
}
|
||||
if len(playerNames) > MAX_PLAYER_NAMES_TO_IDS {
|
||||
|
4
main.go
4
main.go
@ -353,6 +353,7 @@ func (app *App) MakeServer() *echo.Echo {
|
||||
servicesUploadSkin := ServicesUploadSkin(app)
|
||||
servicesChangeName := ServicesChangeName(app)
|
||||
servicesPublicKeys := ServicesPublicKeys(app)
|
||||
servicesIDToPlayerName := app.ServicesIDToPlayerName()
|
||||
|
||||
e.GET("/privileges", servicesPlayerAttributes)
|
||||
e.GET("/player/attributes", servicesPlayerAttributes)
|
||||
@ -367,6 +368,7 @@ func (app *App) MakeServer() *echo.Echo {
|
||||
e.POST("/minecraft/profile/skins", servicesUploadSkin)
|
||||
e.PUT("/minecraft/profile/name/:playerName", servicesChangeName)
|
||||
e.GET("/publickeys", servicesPublicKeys)
|
||||
e.GET("/minecraft/profile/lookup/:id", servicesIDToPlayerName)
|
||||
e.GET("/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||
e.POST("/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
||||
|
||||
@ -383,6 +385,7 @@ func (app *App) MakeServer() *echo.Echo {
|
||||
e.POST("/services/minecraft/profile/skins", servicesUploadSkin)
|
||||
e.PUT("/services/minecraft/profile/name/:playerName", servicesChangeName)
|
||||
e.GET("/services/publickeys", servicesPublicKeys)
|
||||
e.GET("/services/minecraft/profile/lookup/:id", servicesIDToPlayerName)
|
||||
e.GET("/services/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||
e.POST("/services/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
||||
|
||||
@ -399,6 +402,7 @@ func (app *App) MakeServer() *echo.Echo {
|
||||
e.POST("/authlib-injector/minecraftservices/minecraft/profile/skins", servicesUploadSkin)
|
||||
e.PUT("/authlib-injector/minecraftservices/minecraft/profile/name/:playerName", servicesChangeName)
|
||||
e.GET("/authlib-injector/minecraftservices/publickeys", servicesPublicKeys)
|
||||
e.GET("/authlib-injector/minecraftservices/minecraft/profile/lookup/:id", servicesIDToPlayerName)
|
||||
e.GET("/authlib-injector/minecraftservices/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||
e.POST("/authlib-injector/minecraftservices/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
||||
|
||||
|
20
model.go
20
model.go
@ -86,6 +86,26 @@ func IDToUUID(id string) (string, error) {
|
||||
return id[0:8] + "-" + id[8:12] + "-" + id[12:16] + "-" + id[16:20] + "-" + id[20:], nil
|
||||
}
|
||||
|
||||
func ParseUUID(idOrUUID string) (string, error) {
|
||||
if len(idOrUUID) == 32 {
|
||||
uuid_, err := IDToUUID(idOrUUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := uuid.Parse(uuid_); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return uuid_, nil
|
||||
}
|
||||
if len(idOrUUID) == 36 {
|
||||
if _, err := uuid.Parse(idOrUUID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return idOrUUID, nil
|
||||
}
|
||||
return "", errors.New("invalid ID or UUID")
|
||||
}
|
||||
|
||||
func (app *App) ValidatePlayerName(playerName string) error {
|
||||
if app.TransientLoginEligible(playerName) {
|
||||
return errors.New("name is reserved for transient login")
|
||||
|
27
player.go
27
player.go
@ -497,10 +497,6 @@ func (app *App) ValidateChallenge(playerName string, challengeToken *string) (*P
|
||||
return nil, errors.New("skin does not match")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &details, nil
|
||||
}
|
||||
}
|
||||
@ -621,3 +617,26 @@ func (app *App) PlayerSkinURL(player *Player) (*string, error) {
|
||||
}
|
||||
return &url, nil
|
||||
}
|
||||
|
||||
func (app *App) FindPlayerByUUIDOrOfflineUUID(uuid_ string) (*Player, *User, error) {
|
||||
var player Player
|
||||
result := app.DB.Preload("User").First(&player, "uuid = ?", uuid_)
|
||||
if result.Error == nil {
|
||||
return &player, &player.User, nil
|
||||
}
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, result.Error
|
||||
}
|
||||
|
||||
if app.Config.OfflineSkins {
|
||||
result = app.DB.Preload("User").First(&player, "offline_uuid = ?", uuid_)
|
||||
if result.Error == nil {
|
||||
return &player, &player.User, nil
|
||||
}
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, result.Error
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
68
services.go
68
services.go
@ -13,8 +13,10 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/samber/mo"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@ -588,3 +590,69 @@ func ServicesPublicKeys(app *App) func(c echo.Context) error {
|
||||
return c.JSONBlob(http.StatusOK, responseBlob)
|
||||
}
|
||||
}
|
||||
|
||||
// GET /minecraft/profile/lookup/:id
|
||||
func (app *App) ServicesIDToPlayerName() func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
idParam := c.Param("id")
|
||||
uuid_, err := ParseUUID(idParam)
|
||||
if err != nil {
|
||||
return &YggdrasilError{
|
||||
Code: http.StatusBadRequest,
|
||||
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", idParam)),
|
||||
}
|
||||
}
|
||||
|
||||
playerName := mo.None[*string]()
|
||||
|
||||
player, _, err := app.FindPlayerByUUIDOrOfflineUUID(uuid_)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if player != nil {
|
||||
playerName = mo.Some(&player.Name)
|
||||
} else {
|
||||
for _, fallbackAPIServer := range app.FallbackAPIServers {
|
||||
reqURL := fallbackAPIServer.Config.SessionURL + "/session/minecraft/profile/" + url.PathEscape(uuid_)
|
||||
res, err := app.CachedGet(reqURL+"?unsigned=true", fallbackAPIServer.Config.CacheTTLSeconds)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
var profileRes SessionProfileResponse
|
||||
err = json.Unmarshal(res.BodyBytes, &profileRes)
|
||||
if err != nil {
|
||||
log.Printf("Received invalid response from fallback API server at %s\n", reqURL)
|
||||
}
|
||||
|
||||
playerName = mo.Some(&profileRes.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if n, ok := playerName.Get(); ok {
|
||||
id, err := UUIDToID(uuid_)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, PlayerNameToIDResponse{
|
||||
Name: *n,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
// Consistent with
|
||||
// https://api.minecraftservices.com/minecraft/profile/lookup/:uuid as
|
||||
// of 2025-04-04
|
||||
return &YggdrasilError{
|
||||
Code: http.StatusNotFound,
|
||||
Error_: mo.Some("NOT FOUND"),
|
||||
ErrorMessage: mo.Some("Not Found"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,8 +41,9 @@ func TestServices(t *testing.T) {
|
||||
ts.CreateTestUser(t, ts.App, ts.Server, TEST_USERNAME)
|
||||
ts.CreateTestUser(t, ts.App, ts.Server, SERVICES_EXISTING_USERNAME)
|
||||
ts.CreateTestUser(t, ts.AuxApp, ts.AuxServer, TEST_USERNAME)
|
||||
ts.CreateTestUser(t, ts.AuxApp, ts.AuxServer, TEST_OTHER_USERNAME)
|
||||
|
||||
// Set the red skin on the aux user
|
||||
// Set the red skin on the aux TEST_USERNAME user
|
||||
var user User
|
||||
assert.Nil(t, ts.AuxApp.DB.First(&user, "username = ?", TEST_USERNAME).Error)
|
||||
player := user.Players[0]
|
||||
@ -61,6 +62,7 @@ func TestServices(t *testing.T) {
|
||||
t.Run("Test GET /rollout/v1/msamigration", ts.testServicesMSAMigration)
|
||||
t.Run("Test POST /publickey", ts.testServicesPublicKeys)
|
||||
t.Run("Test POST /minecraft/profile/lookup/bulk/byname", ts.makeTestAccountPlayerNamesToIDs("/minecraft/profile/lookup/bulk/byname"))
|
||||
t.Run("Test GET /minecraft/profile/lookup/:id", ts.testServicesIDToPlayerName)
|
||||
}
|
||||
{
|
||||
ts := &TestSuite{}
|
||||
@ -491,6 +493,35 @@ func (ts *TestSuite) testServicesPublicKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) testServicesIDToPlayerName(t *testing.T) {
|
||||
{
|
||||
// Non-fallback
|
||||
var player Player
|
||||
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_PLAYER_NAME).Error)
|
||||
|
||||
rec := ts.Get(t, ts.Server, "/minecraft/profile/lookup/"+player.UUID, nil, nil)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
var response PlayerNameToIDResponse
|
||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
|
||||
|
||||
assert.Equal(t, response.Name, player.Name)
|
||||
assert.Equal(t, response.ID, Unwrap(UUIDToID(player.UUID)))
|
||||
}
|
||||
{
|
||||
// Fallback
|
||||
var player Player
|
||||
assert.Nil(t, ts.AuxApp.DB.First(&player, "name = ?", TEST_OTHER_PLAYER_NAME).Error)
|
||||
|
||||
rec := ts.Get(t, ts.Server, "/minecraft/profile/lookup/"+player.UUID, nil, nil)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
var response PlayerNameToIDResponse
|
||||
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
|
||||
|
||||
assert.Equal(t, response.Name, player.Name)
|
||||
assert.Equal(t, response.ID, Unwrap(UUIDToID(player.UUID)))
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *TestSuite) makeTestAccountPlayerNamesToIDs(url string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
payload := []string{TEST_USERNAME, "nonexistent"}
|
||||
|
51
session.go
51
session.go
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/samber/mo"
|
||||
"gorm.io/gorm"
|
||||
@ -220,57 +219,23 @@ func SessionCheckServer(app *App) func(c echo.Context) error {
|
||||
func SessionProfile(app *App, fromAuthlibInjector bool) func(c echo.Context) error {
|
||||
return func(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
var uuid_ string
|
||||
uuid_, err := IDToUUID(id)
|
||||
uuid_, err := ParseUUID(id)
|
||||
if err != nil {
|
||||
_, err = uuid.Parse(id)
|
||||
if err != nil {
|
||||
return &YggdrasilError{
|
||||
Code: http.StatusBadRequest,
|
||||
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", c.Param("id"))),
|
||||
}
|
||||
return &YggdrasilError{
|
||||
Code: http.StatusBadRequest,
|
||||
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", id)),
|
||||
}
|
||||
uuid_ = id
|
||||
}
|
||||
|
||||
findPlayer := func() (*Player, *User, error) {
|
||||
var player Player
|
||||
result := app.DB.Preload("User").First(&player, "uuid = ?", uuid_)
|
||||
if result.Error == nil {
|
||||
return &player, &player.User, nil
|
||||
}
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Could be an offline UUID
|
||||
if app.Config.OfflineSkins {
|
||||
result = app.DB.Preload("User").First(&player, "offline_uuid = ?", uuid_)
|
||||
if result.Error == nil {
|
||||
return &player, &player.User, nil
|
||||
}
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
player, user, err := findPlayer()
|
||||
player, user, err := app.FindPlayerByUUIDOrOfflineUUID(uuid_)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if player == nil {
|
||||
for _, fallbackAPIServer := range app.Config.FallbackAPIServers {
|
||||
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)
|
||||
for _, fallbackAPIServer := range app.FallbackAPIServers {
|
||||
reqURL := fallbackAPIServer.Config.SessionURL + "/session/minecraft/profile/" + url.PathEscape(uuid_)
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user