mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 10:56:06 -04:00
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:
parent
0b7264230c
commit
09c9192cca
5
Makefile
5
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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
4
front.go
4
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 {
|
||||
|
41
main.go
41
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
|
||||
}
|
||||
|
||||
|
26
services.go
26
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{
|
||||
|
47
session.go
47
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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user