diff --git a/Makefile b/Makefile index de15f36..1903772 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/authlib_injector.go b/authlib_injector.go index 50b93ea..825d92e 100644 --- a/authlib_injector.go +++ b/authlib_injector.go @@ -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) + }) +} diff --git a/authlib_injector_test.go b/authlib_injector_test.go index 82a0e59..7208ae2 100644 --- a/authlib_injector_test.go +++ b/authlib_injector_test.go @@ -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)) + } +} diff --git a/front.go b/front.go index 70ef4ab..700a476 100644 --- a/front.go +++ b/front.go @@ -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 { diff --git a/main.go b/main.go index 44894b0..e89ffc8 100644 --- a/main.go +++ b/main.go @@ -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 } diff --git a/services.go b/services.go index d165f17..f3c7a3c 100644 --- a/services.go +++ b/services.go @@ -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{ diff --git a/session.go b/session.go index a6bca76..5d0af75 100644 --- a/session.go +++ b/session.go @@ -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 } diff --git a/test_suite_test.go b/test_suite_test.go index 778ed1c..c1dcefd 100644 --- a/test_suite_test.go +++ b/test_suite_test.go @@ -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)