mirror of
https://github.com/unmojang/drasl.git
synced 2025-09-08 22:56:49 -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
|
prefix ?= /usr
|
||||||
.DEFAULT_GOAL := build
|
.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:
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
swag:
|
swag:
|
||||||
swag init --generalInfo api.go --output . --outputTypes json
|
$(SWAG) init --generalInfo api.go --output . --outputTypes json
|
||||||
|
|
||||||
prebuild: npm-install swag
|
prebuild: npm-install swag
|
||||||
node esbuild.config.js
|
node esbuild.config.js
|
||||||
|
@ -5,7 +5,10 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
@ -83,3 +86,113 @@ func AuthlibInjectorRoot(app *App) func(c echo.Context) error {
|
|||||||
return c.JSONBlob(http.StatusOK, responseBlob)
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
@ -13,7 +15,6 @@ const FALLBACK_SKIN_DOMAIN_B = "b.example.com"
|
|||||||
|
|
||||||
func TestAuthlibInjector(t *testing.T) {
|
func TestAuthlibInjector(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Just check that AuthlibInjectorRoot works.
|
|
||||||
// authlib-injector also expects a X-Authlib-Injector-API-Location header
|
// authlib-injector also expects a X-Authlib-Injector-API-Location header
|
||||||
// on the authserver and sessionserver routes that it uses; those are
|
// on the authserver and sessionserver routes that it uses; those are
|
||||||
// tested in auth_test.go and session_test.go.
|
// 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)
|
ts.CreateTestUser(t, ts.App, ts.Server, TEST_USERNAME)
|
||||||
|
|
||||||
t.Run("Test /authlib-injector", ts.testAuthlibInjectorRoot)
|
t.Run("Test /authlib-injector", ts.testAuthlibInjectorRoot)
|
||||||
|
t.Run("Test /authlib-injector/api/user/profile/:playerUUID/:textureType", ts.testAuthlibInjectorTextureUploadDelete)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ts := &TestSuite{}
|
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)
|
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
|
return result.Error
|
||||||
}
|
}
|
||||||
|
playerUser := player.User
|
||||||
|
|
||||||
if !user.IsAdmin && (player.User.UUID != user.UUID) {
|
if !user.IsAdmin && (player.User.UUID != user.UUID) {
|
||||||
return NewWebError(app.FrontEndURL, "You don't own that player.")
|
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)
|
skinURL, err := app.GetSkinURL(&player)
|
||||||
if err != nil {
|
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.HidePort = app.Config.TestMode
|
||||||
e.HTTPErrorHandler = app.HandleError
|
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 {
|
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
c.Response().Header().Set("X-Authlib-Injector-API-Location", app.AuthlibInjectorURL)
|
c.Response().Header().Set("X-Authlib-Injector-API-Location", app.AuthlibInjectorURL)
|
||||||
@ -217,6 +211,10 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
// authlib-injector
|
// authlib-injector
|
||||||
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
|
e.GET("/authlib-injector", AuthlibInjectorRoot(app))
|
||||||
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
|
// Auth
|
||||||
authAuthenticate := AuthAuthenticate(app)
|
authAuthenticate := AuthAuthenticate(app)
|
||||||
@ -238,6 +236,13 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
e.POST("/auth/signout", authSignout)
|
e.POST("/auth/signout", authSignout)
|
||||||
e.POST("/auth/validate", authValidate)
|
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
|
// Account
|
||||||
accountVerifySecurityLocation := AccountVerifySecurityLocation(app)
|
accountVerifySecurityLocation := AccountVerifySecurityLocation(app)
|
||||||
accountPlayerNameToID := AccountPlayerNameToID(app)
|
accountPlayerNameToID := AccountPlayerNameToID(app)
|
||||||
@ -256,7 +261,7 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
sessionCheckServer := SessionCheckServer(app)
|
sessionCheckServer := SessionCheckServer(app)
|
||||||
sessionJoin := SessionJoin(app)
|
sessionJoin := SessionJoin(app)
|
||||||
sessionJoinServer := SessionJoinServer(app)
|
sessionJoinServer := SessionJoinServer(app)
|
||||||
sessionProfile := SessionProfile(app)
|
sessionProfile := SessionProfile(app, false)
|
||||||
sessionBlockedServers := SessionBlockedServers(app)
|
sessionBlockedServers := SessionBlockedServers(app)
|
||||||
e.GET("/session/minecraft/hasJoined", sessionHasJoined)
|
e.GET("/session/minecraft/hasJoined", sessionHasJoined)
|
||||||
e.GET("/game/checkserver.jsp", sessionCheckServer)
|
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/session/minecraft/profile/:id", sessionProfile)
|
||||||
e.GET("/session/blockedservers", sessionBlockedServers)
|
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
|
// Services
|
||||||
servicesPlayerAttributes := ServicesPlayerAttributes(app)
|
servicesPlayerAttributes := ServicesPlayerAttributes(app)
|
||||||
servicesPlayerCertificates := ServicesPlayerCertificates(app)
|
servicesPlayerCertificates := ServicesPlayerCertificates(app)
|
||||||
@ -316,6 +328,21 @@ func (app *App) MakeServer() *echo.Echo {
|
|||||||
e.GET("/services/publickeys", servicesPublicKeys)
|
e.GET("/services/publickeys", servicesPublicKeys)
|
||||||
e.POST("/services/minecraft/profile/lookup/bulk/byname", accountPlayerNamesToIDs)
|
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
|
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
|
// Authenticate a user using a bearer token, and call `f` with a reference to
|
||||||
// the player
|
// 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 (.*)$")
|
bearerExp := regexp.MustCompile("^Bearer (.*)$")
|
||||||
|
|
||||||
return func(c echo.Context) error {
|
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 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
|
// GET /minecraft/profile
|
||||||
// https://minecraft.wiki/w/Mojang_API#Query_player_profile
|
// https://minecraft.wiki/w/Mojang_API#Query_player_profile
|
||||||
func ServicesProfileInformation(app *App) func(c echo.Context) error {
|
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)
|
servicesProfile, err := getServicesProfile(app, player)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -162,7 +162,7 @@ type playerAttributesResponse struct {
|
|||||||
// GET /player/attributes
|
// GET /player/attributes
|
||||||
// https://minecraft.wiki/w/Mojang_API#Query_player_attributes
|
// https://minecraft.wiki/w/Mojang_API#Query_player_attributes
|
||||||
func ServicesPlayerAttributes(app *App) func(c echo.Context) error {
|
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{
|
res := playerAttributesResponse{
|
||||||
Privileges: playerAttributesPrivileges{
|
Privileges: playerAttributesPrivileges{
|
||||||
OnlineChat: playerAttributesToggle{Enabled: true},
|
OnlineChat: playerAttributesToggle{Enabled: true},
|
||||||
@ -196,7 +196,7 @@ type playerCertificatesResponse struct {
|
|||||||
// POST /player/certificates
|
// POST /player/certificates
|
||||||
// https://minecraft.wiki/w/Mojang_API#Get_keypair_for_signature
|
// https://minecraft.wiki/w/Mojang_API#Get_keypair_for_signature
|
||||||
func ServicesPlayerCertificates(app *App) func(c echo.Context) error {
|
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)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -325,7 +325,7 @@ func ServicesPlayerCertificates(app *App) func(c echo.Context) error {
|
|||||||
// POST /minecraft/profile/skins
|
// POST /minecraft/profile/skins
|
||||||
// https://minecraft.wiki/w/Mojang_API#Upload_skin
|
// https://minecraft.wiki/w/Mojang_API#Upload_skin
|
||||||
func ServicesUploadSkin(app *App) func(c echo.Context) error {
|
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 {
|
if !app.Config.AllowSkins {
|
||||||
return MakeErrorResponse(&c, http.StatusBadRequest, nil, Ptr("Changing your skin is not allowed."))
|
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
|
// DELETE /minecraft/profile/skins/active
|
||||||
// https://minecraft.wiki/w/Mojang_API#Reset_skin
|
// https://minecraft.wiki/w/Mojang_API#Reset_skin
|
||||||
func ServicesResetSkin(app *App) func(c echo.Context) error {
|
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)
|
err := app.SetSkinAndSave(player, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -377,7 +377,7 @@ func ServicesResetSkin(app *App) func(c echo.Context) error {
|
|||||||
// DELETE /minecraft/profile/capes/active
|
// DELETE /minecraft/profile/capes/active
|
||||||
// https://minecraft.wiki/w/Mojang_API#Hide_cape
|
// https://minecraft.wiki/w/Mojang_API#Hide_cape
|
||||||
func ServicesHideCape(app *App) func(c echo.Context) error {
|
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)
|
err := app.SetCapeAndSave(player, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -396,7 +396,7 @@ type nameChangeResponse struct {
|
|||||||
// GET /minecraft/profile/namechange
|
// GET /minecraft/profile/namechange
|
||||||
// https://minecraft.wiki/w/Mojang_API#Query_player's_name_change_information
|
// https://minecraft.wiki/w/Mojang_API#Query_player's_name_change_information
|
||||||
func ServicesNameChange(app *App) func(c echo.Context) error {
|
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)
|
changedAt := player.NameLastChangedAt.Format(time.RFC3339Nano)
|
||||||
createdAt := player.CreatedAt.Format(time.RFC3339Nano)
|
createdAt := player.CreatedAt.Format(time.RFC3339Nano)
|
||||||
res := nameChangeResponse{
|
res := nameChangeResponse{
|
||||||
@ -414,7 +414,7 @@ func ServicesMSAMigration(app *App) func(c echo.Context) error {
|
|||||||
Feature string `json:"feature"`
|
Feature string `json:"feature"`
|
||||||
Rollout bool `json:"rollout"`
|
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{
|
res := msaMigrationResponse{
|
||||||
Feature: "msamigration",
|
Feature: "msamigration",
|
||||||
Rollout: false,
|
Rollout: false,
|
||||||
@ -430,7 +430,7 @@ type blocklistResponse struct {
|
|||||||
// GET /privacy/blocklist
|
// GET /privacy/blocklist
|
||||||
// https://minecraft.wiki/w/Mojang_API#Get_list_of_blocked_users
|
// https://minecraft.wiki/w/Mojang_API#Get_list_of_blocked_users
|
||||||
func ServicesBlocklist(app *App) func(c echo.Context) error {
|
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{
|
res := blocklistResponse{
|
||||||
BlockedProfiles: []string{},
|
BlockedProfiles: []string{},
|
||||||
}
|
}
|
||||||
@ -445,7 +445,7 @@ type nameAvailabilityResponse struct {
|
|||||||
// GET /minecraft/profile/name/:playerName/available
|
// GET /minecraft/profile/name/:playerName/available
|
||||||
// https://minecraft.wiki/w/Mojang_API#Check_name_availability
|
// https://minecraft.wiki/w/Mojang_API#Check_name_availability
|
||||||
func ServicesNameAvailability(app *App) func(c echo.Context) error {
|
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")
|
playerName := c.Param("playerName")
|
||||||
if !app.Config.AllowChangingPlayerName {
|
if !app.Config.AllowChangingPlayerName {
|
||||||
return c.JSON(http.StatusOK, nameAvailabilityResponse{Status: "NOT_ALLOWED"})
|
return c.JSON(http.StatusOK, nameAvailabilityResponse{Status: "NOT_ALLOWED"})
|
||||||
@ -481,7 +481,7 @@ type changeNameErrorResponse struct {
|
|||||||
// PUT /minecraft/profile/name/:playerName
|
// PUT /minecraft/profile/name/:playerName
|
||||||
// https://minecraft.wiki/w/Mojang_API#Change_name
|
// https://minecraft.wiki/w/Mojang_API#Change_name
|
||||||
func ServicesChangeName(app *App) func(c echo.Context) error {
|
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")
|
playerName := c.Param("playerName")
|
||||||
if err := app.ValidatePlayerName(playerName); err != nil {
|
if err := app.ValidatePlayerName(playerName); err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, changeNameErrorResponse{
|
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)
|
id, err := UUIDToID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SessionProfileResponse{}, err
|
return SessionProfileResponse{}, err
|
||||||
@ -108,16 +108,33 @@ func fullProfile(app *App, player *Player, uuid string, sign bool) (SessionProfi
|
|||||||
return SessionProfileResponse{}, err
|
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{
|
return SessionProfileResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: player.Name,
|
Name: player.Name,
|
||||||
Properties: []SessionProfileProperty{texturesProperty},
|
Properties: properties,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) hasJoined(c *echo.Context, playerName string, serverID string, legacy bool) error {
|
func (app *App) hasJoined(c *echo.Context, playerName string, serverID string, legacy bool) error {
|
||||||
var player Player
|
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 the error isn't "not found", throw.
|
||||||
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return result.Error
|
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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -198,7 +215,7 @@ func SessionCheckServer(app *App) func(c echo.Context) error {
|
|||||||
|
|
||||||
// /session/minecraft/profile/:id
|
// /session/minecraft/profile/:id
|
||||||
// https://minecraft.wiki/w/Mojang_API#Query_player's_skin_and_cape
|
// 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 {
|
return func(c echo.Context) error {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
@ -214,31 +231,31 @@ func SessionProfile(app *App) func(c echo.Context) error {
|
|||||||
uuid_ = id
|
uuid_ = id
|
||||||
}
|
}
|
||||||
|
|
||||||
findPlayer := func() (*Player, error) {
|
findPlayer := func() (*Player, *User, error) {
|
||||||
var player Player
|
var player Player
|
||||||
result := app.DB.First(&player, "uuid = ?", uuid_)
|
result := app.DB.Preload("User").First(&player, "uuid = ?", uuid_)
|
||||||
if result.Error == nil {
|
if result.Error == nil {
|
||||||
return &player, nil
|
return &player, &player.User, nil
|
||||||
}
|
}
|
||||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Could be an offline UUID
|
// Could be an offline UUID
|
||||||
if app.Config.OfflineSkins {
|
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 {
|
if result.Error == nil {
|
||||||
return &player, nil
|
return &player, &player.User, nil
|
||||||
}
|
}
|
||||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -264,7 +281,7 @@ func SessionProfile(app *App) func(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sign := c.QueryParam("unsigned") == "false"
|
sign := c.QueryParam("unsigned") == "false"
|
||||||
profile, err := fullProfile(app, player, uuid_, sign)
|
profile, err := fullProfile(app, user, player, uuid_, sign, fromAuthlibInjector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"lukechampine.com/blake3"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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="
|
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 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="
|
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 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"
|
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"
|
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 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"
|
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 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}
|
var GOD User = User{IsAdmin: true}
|
||||||
|
|
||||||
@ -249,6 +259,22 @@ func (ts *TestSuite) PostMultipart(t *testing.T, server *echo.Echo, path string,
|
|||||||
return rec
|
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 {
|
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)
|
body, err := json.Marshal(payload)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user