mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-05 11:56:10 -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
|
// This error message is consistent with POST
|
||||||
// https://api.mojang.com/users/profiles/minecraft/:playerName as
|
// https://api.mojang.com/users/profiles/minecraft/:playerName as
|
||||||
// of 2025-04-03
|
// of 2025-04-03
|
||||||
errorMessage := fmt.Sprintf("getProfileName.name: Invalid profile name")
|
|
||||||
return &YggdrasilError{
|
return &YggdrasilError{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Error_: mo.Some("CONSTRAINT_VIOLATION"),
|
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 {
|
if len(playerNames) == 0 {
|
||||||
// This error message is consistent with POST
|
// This error message is consistent with POST
|
||||||
// https://api.mojang.com/profiles/minecraft as of 2025-04-02
|
// https://api.mojang.com/profiles/minecraft as of 2025-04-02
|
||||||
errorMessage := fmt.Sprintf("getProfileName.profileNames: must not be empty")
|
|
||||||
return &YggdrasilError{
|
return &YggdrasilError{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
Error_: mo.Some("CONSTRAINT_VIOLATION"),
|
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 {
|
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)
|
servicesUploadSkin := ServicesUploadSkin(app)
|
||||||
servicesChangeName := ServicesChangeName(app)
|
servicesChangeName := ServicesChangeName(app)
|
||||||
servicesPublicKeys := ServicesPublicKeys(app)
|
servicesPublicKeys := ServicesPublicKeys(app)
|
||||||
|
servicesIDToPlayerName := app.ServicesIDToPlayerName()
|
||||||
|
|
||||||
e.GET("/privileges", servicesPlayerAttributes)
|
e.GET("/privileges", servicesPlayerAttributes)
|
||||||
e.GET("/player/attributes", servicesPlayerAttributes)
|
e.GET("/player/attributes", servicesPlayerAttributes)
|
||||||
@ -367,6 +368,7 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
e.POST("/minecraft/profile/skins", servicesUploadSkin)
|
e.POST("/minecraft/profile/skins", servicesUploadSkin)
|
||||||
e.PUT("/minecraft/profile/name/:playerName", servicesChangeName)
|
e.PUT("/minecraft/profile/name/:playerName", servicesChangeName)
|
||||||
e.GET("/publickeys", servicesPublicKeys)
|
e.GET("/publickeys", servicesPublicKeys)
|
||||||
|
e.GET("/minecraft/profile/lookup/:id", servicesIDToPlayerName)
|
||||||
e.GET("/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
e.GET("/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||||
e.POST("/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
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.POST("/services/minecraft/profile/skins", servicesUploadSkin)
|
||||||
e.PUT("/services/minecraft/profile/name/:playerName", servicesChangeName)
|
e.PUT("/services/minecraft/profile/name/:playerName", servicesChangeName)
|
||||||
e.GET("/services/publickeys", servicesPublicKeys)
|
e.GET("/services/publickeys", servicesPublicKeys)
|
||||||
|
e.GET("/services/minecraft/profile/lookup/:id", servicesIDToPlayerName)
|
||||||
e.GET("/services/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
e.GET("/services/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||||
e.POST("/services/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
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.POST("/authlib-injector/minecraftservices/minecraft/profile/skins", servicesUploadSkin)
|
||||||
e.PUT("/authlib-injector/minecraftservices/minecraft/profile/name/:playerName", servicesChangeName)
|
e.PUT("/authlib-injector/minecraftservices/minecraft/profile/name/:playerName", servicesChangeName)
|
||||||
e.GET("/authlib-injector/minecraftservices/publickeys", servicesPublicKeys)
|
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.GET("/authlib-injector/minecraftservices/minecraft/profile/lookup/name/:playerName", accountPlayerNameToID)
|
||||||
e.POST("/authlib-injector/minecraftservices/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
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
|
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 {
|
func (app *App) ValidatePlayerName(playerName string) error {
|
||||||
if app.TransientLoginEligible(playerName) {
|
if app.TransientLoginEligible(playerName) {
|
||||||
return errors.New("name is reserved for transient login")
|
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")
|
return nil, errors.New("skin does not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &details, nil
|
return &details, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -621,3 +617,26 @@ func (app *App) PlayerSkinURL(player *Player) (*string, error) {
|
|||||||
}
|
}
|
||||||
return &url, nil
|
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/labstack/echo/v4"
|
||||||
"github.com/samber/mo"
|
"github.com/samber/mo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -588,3 +590,69 @@ func ServicesPublicKeys(app *App) func(c echo.Context) error {
|
|||||||
return c.JSONBlob(http.StatusOK, responseBlob)
|
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, TEST_USERNAME)
|
||||||
ts.CreateTestUser(t, ts.App, ts.Server, SERVICES_EXISTING_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_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
|
var user User
|
||||||
assert.Nil(t, ts.AuxApp.DB.First(&user, "username = ?", TEST_USERNAME).Error)
|
assert.Nil(t, ts.AuxApp.DB.First(&user, "username = ?", TEST_USERNAME).Error)
|
||||||
player := user.Players[0]
|
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 GET /rollout/v1/msamigration", ts.testServicesMSAMigration)
|
||||||
t.Run("Test POST /publickey", ts.testServicesPublicKeys)
|
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 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{}
|
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) {
|
func (ts *TestSuite) makeTestAccountPlayerNamesToIDs(url string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
payload := []string{TEST_USERNAME, "nonexistent"}
|
payload := []string{TEST_USERNAME, "nonexistent"}
|
||||||
|
51
session.go
51
session.go
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/samber/mo"
|
"github.com/samber/mo"
|
||||||
"gorm.io/gorm"
|
"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 {
|
func SessionProfile(app *App, fromAuthlibInjector bool) func(c echo.Context) error {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
uuid_, err := ParseUUID(id)
|
||||||
var uuid_ string
|
|
||||||
uuid_, err := IDToUUID(id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, err = uuid.Parse(id)
|
return &YggdrasilError{
|
||||||
if err != nil {
|
Code: http.StatusBadRequest,
|
||||||
return &YggdrasilError{
|
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", id)),
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
ErrorMessage: mo.Some(fmt.Sprintf("Not a valid UUID: %s", c.Param("id"))),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
uuid_ = id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findPlayer := func() (*Player, *User, error) {
|
player, user, err := app.FindPlayerByUUIDOrOfflineUUID(uuid_)
|
||||||
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()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if player == nil {
|
if player == nil {
|
||||||
for _, fallbackAPIServer := range app.Config.FallbackAPIServers {
|
for _, fallbackAPIServer := range app.FallbackAPIServers {
|
||||||
reqURL, err := url.JoinPath(fallbackAPIServer.SessionURL, "session/minecraft/profile", id)
|
reqURL := fallbackAPIServer.Config.SessionURL + "/session/minecraft/profile/" + url.PathEscape(uuid_)
|
||||||
if err != nil {
|
res, err := app.CachedGet(reqURL+"?unsigned=false", fallbackAPIServer.Config.CacheTTLSeconds)
|
||||||
log.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res, err := app.CachedGet(reqURL+"?unsigned=false", fallbackAPIServer.CacheTTLSeconds)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
|
log.Printf("Couldn't access fallback API server at %s: %s\n", reqURL, err)
|
||||||
continue
|
continue
|
||||||
|
Loading…
x
Reference in New Issue
Block a user