Authlib-Injector Skin API Support (#144)

* Initial support for Authlib-Injector Upload API

   - Support for HMCL

* Added Skin endpoint

* Support for capes

* Support for DELETE

* Explicitly route authlib-injector URLs, don't rewrite

* Test authlib-injector texture upload/delete

---------

Co-authored-by: Evan Goode <mail@evangoo.de>
This commit is contained in:
IkyMax 2025-03-09 14:15:29 -06:00 committed by GitHub
parent 0b7264230c
commit 09c9192cca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 312 additions and 38 deletions

View File

@ -1,11 +1,14 @@
prefix ?= /usr
.DEFAULT_GOAL := build
# TODO probably use `go tool` for this eventually
SWAG := $(shell command -v swag || echo 'go run github.com/swaggo/swag/cmd/swag@v1.16.4')
npm-install:
npm install
swag:
swag init --generalInfo api.go --output . --outputTypes json
$(SWAG) init --generalInfo api.go --output . --outputTypes json
prebuild: npm-install swag
node esbuild.config.js

View File

@ -5,7 +5,10 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/labstack/echo/v4"
"io"
"net/http"
"net/url"
)
@ -83,3 +86,113 @@ func AuthlibInjectorRoot(app *App) func(c echo.Context) error {
return c.JSONBlob(http.StatusOK, responseBlob)
}
}
func (app *App) AuthlibInjectorUploadTexture(textureType string) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, caller *User, _ *Player) error {
playerID := c.Param("id")
playerUUID, err := IDToUUID(playerID)
if err != nil {
return MakeErrorResponse(&c, http.StatusBadRequest, nil, Ptr("Invalid UUID format"))
}
textureFile, err := c.FormFile("file")
if err != nil {
return MakeErrorResponse(&c, http.StatusBadRequest, nil, Ptr("Missing texture file"))
}
textureHandle, err := textureFile.Open()
if err != nil {
return err
}
defer textureHandle.Close()
var textureReader io.Reader = textureHandle
var targetPlayer Player
result := app.DB.Preload("User").First(&targetPlayer, "uuid = ?", playerUUID)
if result.Error != nil {
return MakeErrorResponse(&c, http.StatusNotFound, nil, Ptr("Player not found"))
}
var updatePlayerErr error
switch textureType {
case TextureTypeSkin:
var model string
switch m := c.FormValue("model"); m {
case "slim":
model = SkinModelSlim
case "":
model = SkinModelClassic
default:
message := fmt.Sprintf("Unknown model: %s", m)
return MakeErrorResponse(&c, http.StatusBadRequest, nil, &message)
}
_, updatePlayerErr = app.UpdatePlayer(
caller,
targetPlayer,
nil, // playerName
nil, // fallbackPlayer
&model, // skinModel
&textureReader, // skinReader
nil, // skinURL
false, // deleteSkin
nil, // capeReader
nil, // capeURL
false, // deleteCape
)
case TextureTypeCape:
_, updatePlayerErr = app.UpdatePlayer(
caller,
targetPlayer,
nil, // playerName
nil, // fallbackPlayer
nil, // skinModel
nil, // skinReader
nil, // skinURL
false, // deleteSkin
&textureReader, // capeReader
nil, // capeURL
false, // deleteCape
)
}
if updatePlayerErr != nil {
var userError *UserError
if errors.As(updatePlayerErr, &userError) {
return MakeErrorResponse(&c, userError.Code, nil, Ptr(userError.Err.Error()))
}
return err
}
return c.NoContent(http.StatusNoContent)
})
}
func (app *App) AuthlibInjectorDeleteTexture(textureType string) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, caller *User, _ *Player) error {
playerID := c.Param("id")
playerUUID, err := IDToUUID(playerID)
if err != nil {
return MakeErrorResponse(&c, http.StatusBadRequest, nil, Ptr("Invalid player UUID"))
}
var targetPlayer Player
result := app.DB.Preload("User").First(&targetPlayer, "uuid = ?", playerUUID)
if result.Error != nil {
return MakeErrorResponse(&c, http.StatusNotFound, nil, Ptr("Player not found"))
}
_, err = app.UpdatePlayer(
caller,
targetPlayer,
nil, // playerName
nil, // fallbackPlayer
nil, // skinModel
nil, // skinReader
nil, // skinURL
textureType == TextureTypeSkin, // deleteSkin
nil, // capeReader
nil, // capeURL
textureType == TextureTypeCape, // deleteCape
)
return c.NoContent(http.StatusNoContent)
})
}

View File

@ -1,8 +1,10 @@
package main
import (
"bytes"
"encoding/json"
"github.com/stretchr/testify/assert"
"mime/multipart"
"net/http"
"net/url"
"testing"
@ -13,7 +15,6 @@ const FALLBACK_SKIN_DOMAIN_B = "b.example.com"
func TestAuthlibInjector(t *testing.T) {
t.Parallel()
// Just check that AuthlibInjectorRoot works.
// authlib-injector also expects a X-Authlib-Injector-API-Location header
// on the authserver and sessionserver routes that it uses; those are
// tested in auth_test.go and session_test.go.
@ -27,6 +28,7 @@ func TestAuthlibInjector(t *testing.T) {
ts.CreateTestUser(t, ts.App, ts.Server, TEST_USERNAME)
t.Run("Test /authlib-injector", ts.testAuthlibInjectorRoot)
t.Run("Test /authlib-injector/api/user/profile/:playerUUID/:textureType", ts.testAuthlibInjectorTextureUploadDelete)
}
{
ts := &TestSuite{}
@ -70,3 +72,87 @@ func (ts *TestSuite) testAuthlibInjectorRootFallback(t *testing.T) {
assert.Equal(t, []string{ts.App.Config.Domain, FALLBACK_SKIN_DOMAIN_A, FALLBACK_SKIN_DOMAIN_B}, response.SkinDomains)
}
func (ts *TestSuite) testAuthlibInjectorTextureUploadDelete(t *testing.T) {
accessToken := ts.authenticate(t, TEST_USERNAME, TEST_PASSWORD).AccessToken
var player Player
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Nil(t, UnmakeNullString(&player.SkinHash))
assert.Nil(t, UnmakeNullString(&player.CapeHash))
assert.Equal(t, SkinModelClassic, player.SkinModel)
playerID, err := UUIDToID(player.UUID)
assert.Nil(t, err)
{
// Successful skin upload
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
assert.Nil(t, writer.WriteField("model", ""))
skinFileField, err := writer.CreateFormFile("file", "redSkin.png")
assert.Nil(t, err)
_, err = skinFileField.Write(RED_SKIN)
assert.Nil(t, err)
rec := ts.PutMultipart(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/skin", body, writer, nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Equal(t, RED_SKIN_HASH, *UnmakeNullString(&player.SkinHash))
assert.Equal(t, SkinModelClassic, player.SkinModel)
}
{
// Successful skin upload, slim model
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
assert.Nil(t, writer.WriteField("model", "slim"))
skinFileField, err := writer.CreateFormFile("file", "blueSkin.png")
assert.Nil(t, err)
_, err = skinFileField.Write(BLUE_SKIN)
assert.Nil(t, err)
rec := ts.PutMultipart(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/skin", body, writer, nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Equal(t, BLUE_SKIN_HASH, *UnmakeNullString(&player.SkinHash))
assert.Equal(t, SkinModelSlim, player.SkinModel)
}
{
// Successful cape upload
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
skinFileField, err := writer.CreateFormFile("file", "redCape.png")
assert.Nil(t, err)
_, err = skinFileField.Write(RED_CAPE)
assert.Nil(t, err)
rec := ts.PutMultipart(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/cape", body, writer, nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Equal(t, RED_CAPE_HASH, *UnmakeNullString(&player.CapeHash))
}
{
// Successful skin delete
rec := ts.Delete(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/skin", nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Nil(t, UnmakeNullString(&player.SkinHash))
// Delete should be idempotent
rec = ts.Delete(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/skin", nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
}
{
// Successful cape delete
rec := ts.Delete(t, ts.Server, "/authlib-injector/api/user/profile/"+playerID+"/cape", nil, &accessToken)
assert.Equal(t, http.StatusNoContent, rec.Code)
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_USERNAME).Error)
assert.Nil(t, UnmakeNullString(&player.CapeHash))
}
}

View File

@ -566,10 +566,12 @@ func FrontPlayer(app *App) func(c echo.Context) error {
}
return result.Error
}
playerUser := player.User
if !user.IsAdmin && (player.User.UUID != user.UUID) {
return NewWebError(app.FrontEndURL, "You don't own that player.")
}
adminView := player.User.UUID != user.UUID
adminView := playerUser.UUID != user.UUID
skinURL, err := app.GetSkinURL(&player)
if err != nil {

41
main.go
View File

@ -125,12 +125,6 @@ func (app *App) MakeServer() *echo.Echo {
e.HidePort = app.Config.TestMode
e.HTTPErrorHandler = app.HandleError
e.Pre(middleware.Rewrite(map[string]string{
"/authlib-injector/authserver/*": "/auth/$1",
"/authlib-injector/api/*": "/account/$1",
"/authlib-injector/sessionserver/*": "/session/$1",
"/authlib-injector/minecraftservices/*": "/services/$1",
}))
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("X-Authlib-Injector-API-Location", app.AuthlibInjectorURL)
@ -217,6 +211,10 @@ func (app *App) MakeServer() *echo.Echo {
// authlib-injector
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
e.GET("/authlib-injector/", AuthlibInjectorRoot(app))
e.PUT("/authlib-injector/api/user/profile/:id/skin", app.AuthlibInjectorUploadTexture(TextureTypeSkin))
e.PUT("/authlib-injector/api/user/profile/:id/cape", app.AuthlibInjectorUploadTexture(TextureTypeCape))
e.DELETE("/authlib-injector/api/user/profile/:id/skin", app.AuthlibInjectorDeleteTexture(TextureTypeSkin))
e.DELETE("/authlib-injector/api/user/profile/:id/cape", app.AuthlibInjectorDeleteTexture(TextureTypeCape))
// Auth
authAuthenticate := AuthAuthenticate(app)
@ -238,6 +236,13 @@ func (app *App) MakeServer() *echo.Echo {
e.POST("/auth/signout", authSignout)
e.POST("/auth/validate", authValidate)
e.GET("/authlib-injector/authserver", AuthServerInfo(app))
e.POST("/authlib-injector/authserver/authenticate", authAuthenticate)
e.POST("/authlib-injector/authserver/invalidate", authInvalidate)
e.POST("/authlib-injector/authserver/refresh", authRefresh)
e.POST("/authlib-injector/authserver/signout", authSignout)
e.POST("/authlib-injector/authserver/validate", authValidate)
// Account
accountVerifySecurityLocation := AccountVerifySecurityLocation(app)
accountPlayerNameToID := AccountPlayerNameToID(app)
@ -256,7 +261,7 @@ func (app *App) MakeServer() *echo.Echo {
sessionCheckServer := SessionCheckServer(app)
sessionJoin := SessionJoin(app)
sessionJoinServer := SessionJoinServer(app)
sessionProfile := SessionProfile(app)
sessionProfile := SessionProfile(app, false)
sessionBlockedServers := SessionBlockedServers(app)
e.GET("/session/minecraft/hasJoined", sessionHasJoined)
e.GET("/game/checkserver.jsp", sessionCheckServer)
@ -272,6 +277,13 @@ func (app *App) MakeServer() *echo.Echo {
e.GET("/session/session/minecraft/profile/:id", sessionProfile)
e.GET("/session/blockedservers", sessionBlockedServers)
e.GET("/authlib-injector/sessionserver/session/minecraft/hasJoined", sessionHasJoined)
e.GET("/authlib-injector/sessionserver/game/checkserver.jsp", sessionCheckServer)
e.POST("/authlib-injector/sessionserver/session/minecraft/join", sessionJoin)
e.GET("/authlib-injector/sessionserver/game/joinserver.jsp", sessionJoinServer)
e.GET("/authlib-injector/sessionserver/session/minecraft/profile/:id", SessionProfile(app, true))
e.GET("/authlib-injector/sessionserver/blockedservers", sessionBlockedServers)
// Services
servicesPlayerAttributes := ServicesPlayerAttributes(app)
servicesPlayerCertificates := ServicesPlayerCertificates(app)
@ -316,6 +328,21 @@ func (app *App) MakeServer() *echo.Echo {
e.GET("/services/publickeys", servicesPublicKeys)
e.POST("/services/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
e.GET("/authlib-injector/minecraftservices/privileges", servicesPlayerAttributes)
e.GET("/authlib-injector/minecraftservices/player/attributes", servicesPlayerAttributes)
e.POST("/authlib-injector/minecraftservices/player/certificates", servicesPlayerCertificates)
e.DELETE("/authlib-injector/minecraftservices/minecraft/profile/capes/active", servicesDeleteCape)
e.DELETE("/authlib-injector/minecraftservices/minecraft/profile/skins/active", servicesDeleteSkin)
e.GET("/authlib-injector/minecraftservices/minecraft/profile", servicesProfileInformation)
e.GET("/authlib-injector/minecraftservices/minecraft/profile/name/:playerName/available", servicesNameAvailability)
e.GET("/authlib-injector/minecraftservices/minecraft/profile/namechange", servicesNameChange)
e.GET("/authlib-injector/minecraftservices/privacy/blocklist", servicesBlocklist)
e.GET("/authlib-injector/minecraftservices/rollout/v1/msamigration", servicesMSAMigration)
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.POST("/authlib-injector/minecraftservices/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
return e
}

View File

@ -21,7 +21,7 @@ import (
// Authenticate a user using a bearer token, and call `f` with a reference to
// the player
func withBearerAuthentication(app *App, f func(c echo.Context, player *Player) error) func(c echo.Context) error {
func withBearerAuthentication(app *App, f func(c echo.Context, user *User, player *Player) error) func(c echo.Context) error {
bearerExp := regexp.MustCompile("^Bearer (.*)$")
return func(c echo.Context) error {
@ -45,7 +45,7 @@ func withBearerAuthentication(app *App, f func(c echo.Context, player *Player) e
return c.JSON(http.StatusBadRequest, ErrorResponse{Path: Ptr(c.Request().URL.Path), ErrorMessage: Ptr("Access token does not have a selected profile.")})
}
return f(c, player)
return f(c, &client.User, player)
}
}
@ -127,7 +127,7 @@ func getServicesProfile(app *App, player *Player) (ServicesProfile, error) {
// GET /minecraft/profile
// https://minecraft.wiki/w/Mojang_API#Query_player_profile
func ServicesProfileInformation(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
servicesProfile, err := getServicesProfile(app, player)
if err != nil {
return err
@ -162,7 +162,7 @@ type playerAttributesResponse struct {
// GET /player/attributes
// https://minecraft.wiki/w/Mojang_API#Query_player_attributes
func ServicesPlayerAttributes(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, _ *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, _ *Player) error {
res := playerAttributesResponse{
Privileges: playerAttributesPrivileges{
OnlineChat: playerAttributesToggle{Enabled: true},
@ -196,7 +196,7 @@ type playerCertificatesResponse struct {
// POST /player/certificates
// https://minecraft.wiki/w/Mojang_API#Get_keypair_for_signature
func ServicesPlayerCertificates(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
@ -325,7 +325,7 @@ func ServicesPlayerCertificates(app *App) func(c echo.Context) error {
// POST /minecraft/profile/skins
// https://minecraft.wiki/w/Mojang_API#Upload_skin
func ServicesUploadSkin(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
if !app.Config.AllowSkins {
return MakeErrorResponse(&c, http.StatusBadRequest, nil, Ptr("Changing your skin is not allowed."))
}
@ -364,7 +364,7 @@ func ServicesUploadSkin(app *App) func(c echo.Context) error {
// DELETE /minecraft/profile/skins/active
// https://minecraft.wiki/w/Mojang_API#Reset_skin
func ServicesResetSkin(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
err := app.SetSkinAndSave(player, nil)
if err != nil {
return err
@ -377,7 +377,7 @@ func ServicesResetSkin(app *App) func(c echo.Context) error {
// DELETE /minecraft/profile/capes/active
// https://minecraft.wiki/w/Mojang_API#Hide_cape
func ServicesHideCape(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
err := app.SetCapeAndSave(player, nil)
if err != nil {
return err
@ -396,7 +396,7 @@ type nameChangeResponse struct {
// GET /minecraft/profile/namechange
// https://minecraft.wiki/w/Mojang_API#Query_player's_name_change_information
func ServicesNameChange(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
changedAt := player.NameLastChangedAt.Format(time.RFC3339Nano)
createdAt := player.CreatedAt.Format(time.RFC3339Nano)
res := nameChangeResponse{
@ -414,7 +414,7 @@ func ServicesMSAMigration(app *App) func(c echo.Context) error {
Feature string `json:"feature"`
Rollout bool `json:"rollout"`
}
return withBearerAuthentication(app, func(c echo.Context, _ *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, _ *Player) error {
res := msaMigrationResponse{
Feature: "msamigration",
Rollout: false,
@ -430,7 +430,7 @@ type blocklistResponse struct {
// GET /privacy/blocklist
// https://minecraft.wiki/w/Mojang_API#Get_list_of_blocked_users
func ServicesBlocklist(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, _ *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, _ *Player) error {
res := blocklistResponse{
BlockedProfiles: []string{},
}
@ -445,7 +445,7 @@ type nameAvailabilityResponse struct {
// GET /minecraft/profile/name/:playerName/available
// https://minecraft.wiki/w/Mojang_API#Check_name_availability
func ServicesNameAvailability(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
playerName := c.Param("playerName")
if !app.Config.AllowChangingPlayerName {
return c.JSON(http.StatusOK, nameAvailabilityResponse{Status: "NOT_ALLOWED"})
@ -481,7 +481,7 @@ type changeNameErrorResponse struct {
// PUT /minecraft/profile/name/:playerName
// https://minecraft.wiki/w/Mojang_API#Change_name
func ServicesChangeName(app *App) func(c echo.Context) error {
return withBearerAuthentication(app, func(c echo.Context, player *Player) error {
return withBearerAuthentication(app, func(c echo.Context, _ *User, player *Player) error {
playerName := c.Param("playerName")
if err := app.ValidatePlayerName(playerName); err != nil {
return c.JSON(http.StatusBadRequest, changeNameErrorResponse{

View File

@ -97,7 +97,7 @@ func SessionJoinServer(app *App) func(c echo.Context) error {
}
}
func fullProfile(app *App, player *Player, uuid string, sign bool) (SessionProfileResponse, error) {
func fullProfile(app *App, user *User, player *Player, uuid string, sign bool, fromAuthlibInjector bool) (SessionProfileResponse, error) {
id, err := UUIDToID(uuid)
if err != nil {
return SessionProfileResponse{}, err
@ -108,16 +108,33 @@ func fullProfile(app *App, player *Player, uuid string, sign bool) (SessionProfi
return SessionProfileResponse{}, err
}
properties := []SessionProfileProperty{texturesProperty}
if fromAuthlibInjector {
var uploadableTextures []string
if app.Config.AllowSkins || user.IsAdmin {
uploadableTextures = append(uploadableTextures, "skin")
}
if app.Config.AllowCapes || user.IsAdmin {
uploadableTextures = append(uploadableTextures, "cape")
}
properties = append(properties, SessionProfileProperty{
Name: "uploadableTextures",
Value: strings.Join(uploadableTextures, ","),
})
}
return SessionProfileResponse{
ID: id,
Name: player.Name,
Properties: []SessionProfileProperty{texturesProperty},
Properties: properties,
}, nil
}
func (app *App) hasJoined(c *echo.Context, playerName string, serverID string, legacy bool) error {
var player Player
result := app.DB.First(&player, "name = ?", playerName)
result := app.DB.Preload("User").First(&player, "name = ?", playerName)
user := player.User
// If the error isn't "not found", throw.
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return result.Error
@ -169,7 +186,7 @@ func (app *App) hasJoined(c *echo.Context, playerName string, serverID string, l
return (*c).String(http.StatusOK, "YES")
}
profile, err := fullProfile(app, &player, player.UUID, true)
profile, err := fullProfile(app, &user, &player, player.UUID, true, false)
if err != nil {
return err
}
@ -198,7 +215,7 @@ func SessionCheckServer(app *App) func(c echo.Context) error {
// /session/minecraft/profile/:id
// https://minecraft.wiki/w/Mojang_API#Query_player's_skin_and_cape
func SessionProfile(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")
@ -214,31 +231,31 @@ func SessionProfile(app *App) func(c echo.Context) error {
uuid_ = id
}
findPlayer := func() (*Player, error) {
findPlayer := func() (*Player, *User, error) {
var player Player
result := app.DB.First(&player, "uuid = ?", uuid_)
result := app.DB.Preload("User").First(&player, "uuid = ?", uuid_)
if result.Error == nil {
return &player, nil
return &player, &player.User, nil
}
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, err
return nil, nil, err
}
// Could be an offline UUID
if app.Config.OfflineSkins {
result = app.DB.First(&player, "offline_uuid = ?", uuid_)
result = app.DB.Preload("User").First(&player, "offline_uuid = ?", uuid_)
if result.Error == nil {
return &player, nil
return &player, &player.User, nil
}
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, err
return nil, nil, err
}
}
return nil, nil
return nil, nil, nil
}
player, err := findPlayer()
player, user, err := findPlayer()
if err != nil {
return err
}
@ -264,7 +281,7 @@ func SessionProfile(app *App) func(c echo.Context) error {
}
sign := c.QueryParam("unsigned") == "false"
profile, err := fullProfile(app, player, uuid_, sign)
profile, err := fullProfile(app, user, player, uuid_, sign, fromAuthlibInjector)
if err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/labstack/echo/v4"
@ -11,6 +12,7 @@ import (
"github.com/stretchr/testify/suite"
"io"
"log"
"lukechampine.com/blake3"
"mime/multipart"
"net"
"net/http"
@ -35,10 +37,14 @@ const TEST_PASSWORD = "password"
const RED_SKIN_BASE64_STRING = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAE+SURBVHhe7ZtBDoMwDAST/z+6pdcgMXUXCXAn4mY74PV6E0VkDhivMbbn9zHH2J77Dvw4AZABtoAakEiYIugqcPNlMF3mkvb4xF7dIlMAwnVeBoQI2AIXrxJqgCL47yK4ahgxgkQrjSdNPXv+3XlA+oI0XgDCEypi6Dq9DCDKEiVXxGm+qj+9n+zEiHgfUE2o6k8Jkl0AYKcpA6hnqxSj+WyBhZIEGBWA7GqAGnB8JqkIpj1YFbWqP/U42dUANQA0gCjU3Y7/BwhAcwRkQPMCY3oyACFq7iADmhcY05MBCFFzBxnQvMCYngxAiJo7yICzC0xHbHRElcZX8zmdAWkCabwAFBGQAUXAdu5E2XR+iidN+SKeXI7tAvDw3+xiDZABMiC7VZYpUH7hwhZIK6AGqAFqQHSzNG1Bd4LhlZs3vSioQQnlCKsAAAAASUVORK5CYII="
var RED_SKIN []byte = Unwrap(base64.StdEncoding.DecodeString(RED_SKIN_BASE64_STRING))
var redSkinHashBytes = blake3.Sum256(RED_SKIN)
var RED_SKIN_HASH = hex.EncodeToString(redSkinHashBytes[:])
const BLUE_SKIN_BASE64_STRING = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAE+SURBVHhe7ZpBDoMwDATJ/x9NK/XUCGVtrVGoO73GDsl6PRTIOOTvPGXIMmAML//e7MDiEAAHeCakBQJt5knsZAcWBwNggGOx43g8A1yLe/LsFujNAAQwexwHmArsZQQtAAOA4N/fBWaGKUEUtNx8xdTa+S+eBdwLuPkIIBSoFRgH+LfBmQnZCql41RJqfM2sgj9CCDC1kapoVjBVYTWOA5ZvvWgBIGg/C2R7OhuvelyNwwAYsPIIEASCQFBRtPd44NsgArRWAAe0Lm9gczggIFLrEBzQuryBzeGAgEitQ3BA6/IGNocDAiK1DsEB9eXNfhmqPp+Q29ENDkAAce5w9wmTb4fggFzHXEUry/tXWM+gHCWy/eUhwE+fNS5gAA7AAT5HnBmAoNXGVvKnbjAABjgd7OfCAKuNreQODHgBFSioQeX4pUIAAAAASUVORK5CYII="
var BLUE_SKIN []byte = Unwrap(base64.StdEncoding.DecodeString(BLUE_SKIN_BASE64_STRING))
var blueSkinHashBytes = blake3.Sum256(BLUE_SKIN)
var BLUE_SKIN_HASH = hex.EncodeToString(blueSkinHashBytes[:])
const INVALID_SKIN_BASE64_STRING = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAABCAIAAAC+O+cgAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAD0lEQVQoz2NgGAWjYAQDAAMBAAGf4uJmAAAAAElFTkSuQmCC"
@ -47,10 +53,14 @@ var INVALID_SKIN []byte = Unwrap(base64.StdEncoding.DecodeString(INVALID_SKIN_BA
const RED_CAPE_BASE64_STRING = "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAABcGlDQ1BpY2MAACiRdZG9S8NAGMafthZFK0UUFHHIUEWwhaIgjlqHLkVKrWDVJbkmrZCk4ZIixVVwcSg4iC5+Df4HugquCoKgCCJu7n4tUuJ7TaFF2jsu748n97zcPQf4Uzoz7K44YJgOzyQT0mpuTep+RxADGKY5JTPbWkinU+g4fh7hE/UhJnp13td29OVVmwG+HuJZZnGHeJ44teVYgveIh1hRzhOfEEc5HZD4VuiKx2+CCx5/CebZzCLgFz2lQgsrLcyK3CCeJI4Yepk1ziNuElLNlWWqo7TGYCODJBKQoKCMTehwEKNqUmbtffG6bwkl8jD6WqiAk6OAInmjpJapq0pVI12lqaMicv+fp63NTHvdQwkg+Oq6n+NA9z5Qq7ru76nr1s6AwAtwbTb9Jcpp7pv0alOLHAPhHeDypqkpB8DVLjDybMlcrksBWn5NAz4ugP4cMHgP9K57WTX+4/wJyG7TE90Bh0fABO0Pb/wB/+FoCgeBR+AAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAA0SURBVFjD7c8xDQAACAMw5l8008BJ0jpodn6LgICAgICAgICAgICAgICAgICAgICAgMBVAR+SIAECIeUGAAAAAElFTkSuQmCC"
var RED_CAPE []byte = Unwrap(base64.StdEncoding.DecodeString(RED_CAPE_BASE64_STRING))
var redCapeHashBytes = blake3.Sum256(RED_CAPE)
var RED_CAPE_HASH = hex.EncodeToString(redCapeHashBytes[:])
const BLUE_CAPE_BASE64_STRING = "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAABcGlDQ1BpY2MAACiRdZG9S8NAGMafthZFK0UUFHHIUEWwhaIgjlqHLkVKrWDVJbkmrZCk4ZIixVVwcSg4iC5+Df4HugquCoKgCCJu7n4tUuJ7TaFF2jsu748n97zcPQf4Uzoz7K44YJgOzyQT0mpuTep+RxADGKY5JTPbWkinU+g4fh7hE/UhJnp13td29OVVmwG+HuJZZnGHeJ44teVYgveIh1hRzhOfEEc5HZD4VuiKx2+CCx5/CebZzCLgFz2lQgsrLcyK3CCeJI4Yepk1ziNuElLNlWWqo7TGYCODJBKQoKCMTehwEKNqUmbtffG6bwkl8jD6WqiAk6OAInmjpJapq0pVI12lqaMicv+fp63NTHvdQwkg+Oq6n+NA9z5Qq7ru76nr1s6AwAtwbTb9Jcpp7pv0alOLHAPhHeDypqkpB8DVLjDybMlcrksBWn5NAz4ugP4cMHgP9K57WTX+4/wJyG7TE90Bh0fABO0Pb/wB/+FoCgeBR+AAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAA0SURBVFjD7c8xDQAACAOwzb9o0MBJ0jpok8lnFRAQEBAQEBAQEBAQEBAQEBAQEBAQEBC4Wt/DIAGQrpeYAAAAAElFTkSuQmCC"
var BLUE_CAPE []byte = Unwrap(base64.StdEncoding.DecodeString(BLUE_CAPE_BASE64_STRING))
var blueCapeHashBytes = blake3.Sum256(BLUE_CAPE)
var BLUE_CAPE_HASH = hex.EncodeToString(blueCapeHashBytes[:])
var GOD User = User{IsAdmin: true}
@ -249,6 +259,22 @@ func (ts *TestSuite) PostMultipart(t *testing.T, server *echo.Echo, path string,
return rec
}
func (ts *TestSuite) PutMultipart(t *testing.T, server *echo.Echo, path string, body io.Reader, writer *multipart.Writer, cookies []http.Cookie, accessToken *string) *httptest.ResponseRecorder {
assert.Nil(t, writer.Close())
req := httptest.NewRequest(http.MethodPut, path, body)
for _, cookie := range cookies {
req.AddCookie(&cookie)
}
if accessToken != nil {
req.Header.Add("Authorization", "Bearer "+*accessToken)
}
req.Header.Add("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
ts.Server.ServeHTTP(rec, req)
ts.CheckAuthlibInjectorHeader(t, ts.App, rec)
return rec
}
func (ts *TestSuite) PostJSON(t *testing.T, server *echo.Echo, path string, payload interface{}, cookies []http.Cookie, accessToken *string) *httptest.ResponseRecorder {
body, err := json.Marshal(payload)
assert.Nil(t, err)