Remove profiles endpoint and textures signing mechanism

This commit is contained in:
ErickSkrauch 2024-06-11 04:35:46 +02:00
parent 62b6ac8083
commit 716ec8bd37
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
11 changed files with 1 additions and 823 deletions

View File

@ -165,68 +165,3 @@ paths:
description: The profiles has been successfully deleted.
401:
$ref: "#/components/responses/UnauthorizedError"
/api/signer:
post:
operationId: signData
summary: Signs the sent data.
tags:
- signer
- api
security:
- BearerAuth: [ sign ]
requestBody:
content:
"*":
schema:
description: Accepts data in any format and generates a signature for it.
type: string
responses:
200:
description: Successfully signed data.
content:
application/octet-stream+base64:
schema:
description: A base64 encoded signature for the passed data.
type: string
401:
$ref: "#/components/responses/UnauthorizedError"
/api/signer/public-key.pem:
get:
operationId: signerPublicKeyPem
summary: Get signer's public key in PEM format.
tags:
- signer
- api
security:
- BearerAuth: [ sign ]
responses:
200:
description: The public file in PEM format.
content:
application/x-pem-file:
schema:
type: string
401:
$ref: "#/components/responses/UnauthorizedError"
/api/signer/public-key.der:
get:
operationId: signerPublicKeyPem
summary: Get signer's public key in DER format.
tags:
- signer
- api
security:
- BearerAuth: [ sign ]
responses:
200:
description: The public file in PEM format.
content:
application/octet-stream:
schema:
type: string
format: binary
401:
$ref: "#/components/responses/UnauthorizedError"

View File

@ -1,34 +0,0 @@
package signer
import (
"context"
"io"
"strings"
)
type Signer interface {
Sign(data io.Reader) ([]byte, error)
GetPublicKey(format string) ([]byte, error)
}
type LocalSigner struct {
Signer
}
func (s *LocalSigner) Sign(ctx context.Context, data string) (string, error) {
signed, err := s.Signer.Sign(strings.NewReader(data))
if err != nil {
return "", err
}
return string(signed), nil
}
func (s *LocalSigner) GetPublicKey(ctx context.Context, format string) (string, error) {
publicKey, err := s.Signer.GetPublicKey(format)
if err != nil {
return "", err
}
return string(publicKey), nil
}

View File

@ -1,104 +0,0 @@
package signer
import (
"context"
"errors"
"io"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type SignerMock struct {
mock.Mock
}
func (m *SignerMock) Sign(data io.Reader) ([]byte, error) {
args := m.Called(data)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}
return result, args.Error(1)
}
func (m *SignerMock) GetPublicKey(format string) ([]byte, error) {
args := m.Called(format)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}
return result, args.Error(1)
}
type LocalSignerServiceTestSuite struct {
suite.Suite
Service *LocalSigner
Signer *SignerMock
}
func (t *LocalSignerServiceTestSuite) SetupSubTest() {
t.Signer = &SignerMock{}
t.Service = &LocalSigner{
Signer: t.Signer,
}
}
func (t *LocalSignerServiceTestSuite) TearDownSubTest() {
t.Signer.AssertExpectations(t.T())
}
func (t *LocalSignerServiceTestSuite) TestSign() {
t.Run("successfully sign", func() {
signature := []byte("mock signature")
t.Signer.On("Sign", mock.Anything).Return(signature, nil).Run(func(args mock.Arguments) {
r, _ := io.ReadAll(args.Get(0).(io.Reader))
t.Equal([]byte("mock body to sign"), r)
})
result, err := t.Service.Sign(context.Background(), "mock body to sign")
t.NoError(err)
t.Equal(string(signature), result)
})
t.Run("handle error during sign", func() {
expectedErr := errors.New("mock error")
t.Signer.On("Sign", mock.Anything).Return(nil, expectedErr)
result, err := t.Service.Sign(context.Background(), "mock body to sign")
t.Error(err)
t.Same(expectedErr, err)
t.Empty(result)
})
}
func (t *LocalSignerServiceTestSuite) TestGetPublicKey() {
t.Run("successfully get", func() {
publicKey := []byte("mock public key")
t.Signer.On("GetPublicKey", "pem").Return(publicKey, nil)
result, err := t.Service.GetPublicKey(context.Background(), "pem")
t.NoError(err)
t.Equal(string(publicKey), result)
})
t.Run("handle error", func() {
expectedErr := errors.New("mock error")
t.Signer.On("GetPublicKey", "pem").Return(nil, expectedErr)
result, err := t.Service.GetPublicKey(context.Background(), "pem")
t.Error(err)
t.Same(expectedErr, err)
t.Empty(result)
})
}
func TestLocalSignerService(t *testing.T) {
suite.Run(t, new(LocalSignerServiceTestSuite))
}

View File

@ -16,7 +16,7 @@ var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts HTTP handler for the skins system",
RunE: func(cmd *cobra.Command, args []string) error {
return startServer(di.ModuleSkinsystem, di.ModuleProfiles, di.ModuleSigner)
return startServer(di.ModuleSkinsystem, di.ModuleProfiles)
},
}

View File

@ -12,7 +12,6 @@ func New() (*di.Container, error) {
loggerDiOptions,
mojangDiOptions,
profilesDiOptions,
securityDiOptions,
serverDiOptions,
)
}

View File

@ -17,13 +17,11 @@ import (
const ModuleSkinsystem = "skinsystem"
const ModuleProfiles = "profiles"
const ModuleSigner = "signer"
var handlersDiOptions = di.Options(
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
di.Provide(newSkinsystemHandler, di.WithName(ModuleSkinsystem)),
di.Provide(newProfilesApiHandler, di.WithName(ModuleProfiles)),
di.Provide(newSignerApiHandler, di.WithName(ModuleSigner)),
)
func newHandlerFactory(
@ -65,26 +63,6 @@ func newHandlerFactory(
mount(router, "/api/profiles", profilesApiRouter)
}
if slices.Contains(enabledModules, ModuleSigner) {
var signerApiRouter *mux.Router
if err := container.Resolve(&signerApiRouter, di.Name(ModuleSigner)); err != nil {
return nil, err
}
var authenticator Authenticator
if err := container.Resolve(&authenticator); err != nil {
return nil, err
}
authMiddleware := NewAuthenticationMiddleware(authenticator, security.SignScope)
conditionalAuth := NewConditionalMiddleware(func(req *http.Request) bool {
return req.Method != "GET"
}, authMiddleware)
signerApiRouter.Use(conditionalAuth)
mount(router, "/api/signer", signerApiRouter)
}
// Resolve health checkers last, because all the services required by the application
// must first be initialized and each of them can publish its own checkers
var healthCheckers []*namedHealthChecker
@ -107,14 +85,12 @@ func newHandlerFactory(
func newSkinsystemHandler(
config *viper.Viper,
profilesProvider ProfilesProvider,
texturesSigner SignerService,
) (*mux.Router, error) {
config.SetDefault("textures.extra_param_name", "chrly")
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
skinsystem, err := NewSkinsystemApi(
profilesProvider,
texturesSigner,
config.GetString("textures.extra_param_name"),
config.GetString("textures.extra_param_value"),
)
@ -134,15 +110,6 @@ func newProfilesApiHandler(profilesManager ProfilesManager) (*mux.Router, error)
return profilesApi.Handler(), nil
}
func newSignerApiHandler(signer Signer) (*mux.Router, error) {
signerApi, err := NewSignerApi(signer)
if err != nil {
return nil, err
}
return signerApi.Handler(), nil
}
func mount(router *mux.Router, path string, handler http.Handler) {
router.PathPrefix(path).Handler(
http.StripPrefix(

View File

@ -1,59 +0,0 @@
package di
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"log/slog"
"ely.by/chrly/internal/client/signer"
"ely.by/chrly/internal/http"
"ely.by/chrly/internal/security"
"github.com/defval/di"
"github.com/spf13/viper"
)
var securityDiOptions = di.Options(
di.Provide(newSigner,
di.As(new(http.Signer)),
di.As(new(signer.Signer)),
),
di.Provide(newSignerService),
)
func newSigner(config *viper.Viper) (*security.Signer, error) {
var privateKey *rsa.PrivateKey
var err error
keyStr := config.GetString("chrly.signing.key")
if keyStr == "" {
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
slog.Warn("A private signing key has been generated. To make it permanent, specify the valid RSA private key in the config parameter chrly.signing.key")
} else {
keyBytes := []byte(keyStr)
rawPem, _ := pem.Decode(keyBytes)
if rawPem == nil {
return nil, errors.New("unable to decode pem key")
}
privateKey, err = x509.ParsePKCS1PrivateKey(rawPem.Bytes)
if err != nil {
return nil, err
}
}
return security.NewSigner(privateKey), nil
}
func newSignerService(s signer.Signer) http.SignerService {
return &signer.LocalSigner{
Signer: s,
}
}

View File

@ -1,96 +0,0 @@
package http
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/metric"
"go.uber.org/multierr"
"ely.by/chrly/internal/otel"
)
type Signer interface {
Sign(data io.Reader) ([]byte, error)
GetPublicKey(format string) ([]byte, error)
}
func NewSignerApi(signer Signer) (*SignerApi, error) {
metrics, err := newSignerApiMetrics(otel.GetMeter())
if err != nil {
return nil, err
}
return &SignerApi{
Signer: signer,
metrics: metrics,
}, nil
}
type SignerApi struct {
Signer
metrics *signerApiMetrics
}
func (s *SignerApi) Handler() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/", s.signHandler).Methods(http.MethodPost)
router.HandleFunc("/public-key.{format:(?:pem|der)}", s.getPublicKeyHandler).Methods(http.MethodGet)
return router
}
func (s *SignerApi) signHandler(resp http.ResponseWriter, req *http.Request) {
signature, err := s.Signer.Sign(req.Body)
if err != nil {
apiServerError(resp, req, fmt.Errorf("unable to sign the value: %w", err))
return
}
resp.Header().Set("Content-Type", "application/octet-stream+base64")
buf := make([]byte, base64.StdEncoding.EncodedLen(len(signature)))
base64.StdEncoding.Encode(buf, signature)
_, _ = resp.Write(buf)
}
func (s *SignerApi) getPublicKeyHandler(resp http.ResponseWriter, req *http.Request) {
format := mux.Vars(req)["format"]
publicKey, err := s.Signer.GetPublicKey(format)
if err != nil {
apiServerError(resp, req, fmt.Errorf("unable to retrieve public key: %w", err))
return
}
if format == "pem" {
resp.Header().Set("Content-Type", "application/x-pem-file")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.pem"`)
} else {
resp.Header().Set("Content-Type", "application/octet-stream")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.der"`)
}
_, _ = resp.Write(publicKey)
}
func newSignerApiMetrics(meter metric.Meter) (*signerApiMetrics, error) {
m := &signerApiMetrics{}
var errors, err error
m.SignRequest, err = meter.Int64Counter("chrly.app.signer.sign.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.GetPublicKeyRequest, err = meter.Int64Counter("chrly.app.signer.get_public_key.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
return m, errors
}
type signerApiMetrics struct {
SignRequest metric.Int64Counter
GetPublicKeyRequest metric.Int64Counter
}

View File

@ -1,144 +0,0 @@
package http
import (
"bytes"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type SignerMock struct {
mock.Mock
}
func (m *SignerMock) Sign(data io.Reader) ([]byte, error) {
args := m.Called(data)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}
return result, args.Error(1)
}
func (m *SignerMock) GetPublicKey(format string) ([]byte, error) {
args := m.Called(format)
var result []byte
if casted, ok := args.Get(0).([]byte); ok {
result = casted
}
return result, args.Error(1)
}
type SignerApiTestSuite struct {
suite.Suite
App *SignerApi
Signer *SignerMock
}
func (t *SignerApiTestSuite) SetupSubTest() {
t.Signer = &SignerMock{}
t.App, _ = NewSignerApi(t.Signer)
}
func (t *SignerApiTestSuite) TearDownSubTest() {
t.Signer.AssertExpectations(t.T())
}
func (t *SignerApiTestSuite) TestSign() {
t.Run("successfully sign", func() {
signature := []byte("mock signature")
t.Signer.On("Sign", mock.Anything).Return(signature, nil).Run(func(args mock.Arguments) {
buf := &bytes.Buffer{}
_, _ = io.Copy(buf, args.Get(0).(io.Reader))
r, _ := io.ReadAll(buf)
t.Equal([]byte("mock body to sign"), r)
})
req := httptest.NewRequest("POST", "http://chrly/", strings.NewReader("mock body to sign"))
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/octet-stream+base64", result.Header.Get("Content-Type"))
body, _ := io.ReadAll(result.Body)
t.Equal([]byte{0x62, 0x57, 0x39, 0x6a, 0x61, 0x79, 0x42, 0x7a, 0x61, 0x57, 0x64, 0x75, 0x59, 0x58, 0x52, 0x31, 0x63, 0x6d, 0x55, 0x3d}, body)
})
t.Run("handle error during sign", func() {
t.Signer.On("Sign", mock.Anything).Return(nil, errors.New("mock error"))
req := httptest.NewRequest("POST", "http://chrly/", strings.NewReader("mock body to sign"))
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusInternalServerError, result.StatusCode)
})
}
func (t *SignerApiTestSuite) TestGetPublicKey() {
t.Run("in pem format", func() {
publicKey := []byte("mock public key in pem format")
t.Signer.On("GetPublicKey", "pem").Return(publicKey, nil)
req := httptest.NewRequest("GET", "http://chrly/public-key.pem", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/x-pem-file", result.Header.Get("Content-Type"))
t.Equal(`attachment; filename="yggdrasil_session_pubkey.pem"`, result.Header.Get("Content-Disposition"))
body, _ := io.ReadAll(result.Body)
t.Equal(publicKey, body)
})
t.Run("in der format", func() {
publicKey := []byte("mock public key in der format")
t.Signer.On("GetPublicKey", "der").Return(publicKey, nil)
req := httptest.NewRequest("GET", "http://chrly/public-key.der", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/octet-stream", result.Header.Get("Content-Type"))
t.Equal(`attachment; filename="yggdrasil_session_pubkey.der"`, result.Header.Get("Content-Disposition"))
body, _ := io.ReadAll(result.Body)
t.Equal(publicKey, body)
})
t.Run("handle error", func() {
t.Signer.On("GetPublicKey", "pem").Return(nil, errors.New("mock error"))
req := httptest.NewRequest("GET", "http://chrly/public-key.pem", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusInternalServerError, result.StatusCode)
})
}
func TestSignerApi(t *testing.T) {
suite.Run(t, new(SignerApiTestSuite))
}

View File

@ -2,13 +2,10 @@ package http
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/metric"
@ -17,24 +14,14 @@ import (
"ely.by/chrly/internal/db"
"ely.by/chrly/internal/mojang"
"ely.by/chrly/internal/otel"
"ely.by/chrly/internal/utils"
)
var timeNow = time.Now
type ProfilesProvider interface {
FindProfileByUsername(ctx context.Context, username string, allowProxy bool) (*db.Profile, error)
}
// SignerService uses context because in the future we may separate this logic as an external microservice
type SignerService interface {
Sign(ctx context.Context, data string) (string, error)
GetPublicKey(ctx context.Context, format string) (string, error)
}
func NewSkinsystemApi(
profilesProvider ProfilesProvider,
signerService SignerService,
texturesExtraParamName string,
texturesExtraParamValue string,
) (*Skinsystem, error) {
@ -45,7 +32,6 @@ func NewSkinsystemApi(
return &Skinsystem{
ProfilesProvider: profilesProvider,
SignerService: signerService,
TexturesExtraParamName: texturesExtraParamName,
TexturesExtraParamValue: texturesExtraParamValue,
metrics: metrics,
@ -54,7 +40,6 @@ func NewSkinsystemApi(
type Skinsystem struct {
ProfilesProvider
SignerService
TexturesExtraParamName string
TexturesExtraParamValue string
metrics *skinsystemApiMetrics
@ -68,12 +53,9 @@ func (s *Skinsystem) Handler() *mux.Router {
// TODO: alias /capes/{username}?
router.HandleFunc("/textures/{username}", s.texturesHandler).Methods(http.MethodGet)
router.HandleFunc("/textures/signed/{username}", s.signedTexturesHandler).Methods(http.MethodGet)
router.HandleFunc("/profile/{username}", s.profileHandler).Methods(http.MethodGet)
// Legacy
router.HandleFunc("/skins", s.legacySkinHandler).Methods(http.MethodGet)
router.HandleFunc("/cloaks", s.legacyCapeHandler).Methods(http.MethodGet)
// Utils
router.HandleFunc("/signature-verification-key.{format:(?:pem|der)}", s.signatureVerificationKeyHandler).Methods(http.MethodGet)
return router
}
@ -212,83 +194,6 @@ func (s *Skinsystem) signedTexturesHandler(resp http.ResponseWriter, req *http.R
_, _ = resp.Write(responseJson)
}
func (s *Skinsystem) profileHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.ProfileRequest.Add(req.Context(), 1)
profile, err := s.ProfilesProvider.FindProfileByUsername(req.Context(), mux.Vars(req)["username"], true)
if err != nil {
apiServerError(resp, req, fmt.Errorf("unable to retrieve a profile: %w", err))
return
}
if profile == nil {
resp.WriteHeader(http.StatusNotFound)
return
}
texturesPropContent := &mojang.TexturesProp{
Timestamp: utils.UnixMillisecond(timeNow()),
ProfileID: profile.Uuid,
ProfileName: profile.Username,
Textures: texturesFromProfile(profile),
}
texturesPropValueJson, _ := json.Marshal(texturesPropContent)
texturesPropEncodedValue := base64.StdEncoding.EncodeToString(texturesPropValueJson)
texturesProp := &mojang.Property{
Name: "textures",
Value: texturesPropEncodedValue,
}
if req.URL.Query().Has("unsigned") && !getToBool(req.URL.Query().Get("unsigned")) {
signature, err := s.SignerService.Sign(req.Context(), texturesProp.Value)
if err != nil {
apiServerError(resp, req, fmt.Errorf("unable to sign textures: %w", err))
return
}
texturesProp.Signature = signature
}
profileResponse := &mojang.ProfileResponse{
Id: profile.Uuid,
Name: profile.Username,
Props: []*mojang.Property{
texturesProp,
{
Name: s.TexturesExtraParamName,
Value: s.TexturesExtraParamValue,
},
},
}
responseJson, _ := json.Marshal(profileResponse)
resp.Header().Set("Content-Type", "application/json")
_, _ = resp.Write(responseJson)
}
func (s *Skinsystem) signatureVerificationKeyHandler(resp http.ResponseWriter, req *http.Request) {
s.metrics.SigningKeyRequest.Add(req.Context(), 1)
format := mux.Vars(req)["format"]
publicKey, err := s.SignerService.GetPublicKey(req.Context(), format)
if err != nil {
apiServerError(resp, req, fmt.Errorf("unable to retrieve public key: %w", err))
return
}
if format == "pem" {
resp.Header().Set("Content-Type", "application/x-pem-file")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.pem"`)
} else {
resp.Header().Set("Content-Type", "application/octet-stream")
resp.Header().Set("Content-Disposition", `attachment; filename="yggdrasil_session_pubkey.der"`)
}
_, _ = io.WriteString(resp, publicKey)
}
func parseUsername(username string) string {
return strings.TrimSuffix(username, ".png")
}
@ -345,12 +250,6 @@ func newSkinsystemMetrics(meter metric.Meter) (*skinsystemApiMetrics, error) {
m.SignedTexturesRequest, err = meter.Int64Counter("chrly.app.skinsystem.signed_textures.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.ProfileRequest, err = meter.Int64Counter("chrly.app.skinsystem.profile.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
m.SigningKeyRequest, err = meter.Int64Counter("chrly.app.skinsystem.signing_key.request", metric.WithUnit("{request}"))
errors = multierr.Append(errors, err)
return m, errors
}
@ -361,6 +260,4 @@ type skinsystemApiMetrics struct {
LegacyCapeRequest metric.Int64Counter
TexturesRequest metric.Int64Counter
SignedTexturesRequest metric.Int64Counter
ProfileRequest metric.Int64Counter
SigningKeyRequest metric.Int64Counter
}

View File

@ -7,7 +7,6 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/mock"
testify "github.com/stretchr/testify/require"
@ -30,27 +29,12 @@ func (m *ProfilesProviderMock) FindProfileByUsername(ctx context.Context, userna
return result, args.Error(1)
}
type SignerServiceMock struct {
mock.Mock
}
func (m *SignerServiceMock) Sign(ctx context.Context, data string) (string, error) {
args := m.Called(ctx, data)
return args.String(0), args.Error(1)
}
func (m *SignerServiceMock) GetPublicKey(ctx context.Context, format string) (string, error) {
args := m.Called(ctx, format)
return args.String(0), args.Error(1)
}
type SkinsystemTestSuite struct {
suite.Suite
App *Skinsystem
ProfilesProvider *ProfilesProviderMock
SignerService *SignerServiceMock
}
/********************
@ -58,17 +42,10 @@ type SkinsystemTestSuite struct {
********************/
func (t *SkinsystemTestSuite) SetupSubTest() {
timeNow = func() time.Time {
CET, _ := time.LoadLocation("CET")
return time.Date(2021, 02, 25, 01, 50, 23, 0, CET)
}
t.ProfilesProvider = &ProfilesProviderMock{}
t.SignerService = &SignerServiceMock{}
t.App, _ = NewSkinsystemApi(
t.ProfilesProvider,
t.SignerService,
"texturesParamName",
"texturesParamValue",
)
@ -76,7 +53,6 @@ func (t *SkinsystemTestSuite) SetupSubTest() {
func (t *SkinsystemTestSuite) TearDownSubTest() {
t.ProfilesProvider.AssertExpectations(t.T())
t.SignerService.AssertExpectations(t.T())
}
func (t *SkinsystemTestSuite) TestSkinHandler() {
@ -415,165 +391,6 @@ func (t *SkinsystemTestSuite) TestSignedTextures() {
})
}
func (t *SkinsystemTestSuite) TestProfile() {
t.Run("exists profile with skin and cape", func() {
req := httptest.NewRequest("GET", "http://chrly/profile/mock_username", nil)
w := httptest.NewRecorder()
// TODO: see the TODO about context above
t.ProfilesProvider.On("FindProfileByUsername", mock.Anything, "mock_username", true).Return(&db.Profile{
Uuid: "mock-uuid",
Username: "mock_username",
SkinUrl: "https://example.com/skin.png",
SkinModel: "slim",
CapeUrl: "https://example.com/cape.png",
}, nil)
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/json", result.Header.Get("Content-Type"))
body, _ := io.ReadAll(result.Body)
t.JSONEq(`{
"id": "mock-uuid",
"name": "mock_username",
"properties": [
{
"name": "textures",
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6Im1vY2stdXVpZCIsInByb2ZpbGVOYW1lIjoibW9ja191c2VybmFtZSIsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9za2luLnBuZyIsIm1ldGFkYXRhIjp7Im1vZGVsIjoic2xpbSJ9fSwiQ0FQRSI6eyJ1cmwiOiJodHRwczovL2V4YW1wbGUuY29tL2NhcGUucG5nIn19fQ=="
},
{
"name": "texturesParamName",
"value": "texturesParamValue"
}
]
}`, string(body))
})
t.Run("exists signed profile with skin", func() {
req := httptest.NewRequest("GET", "http://chrly/profile/mock_username?unsigned=false", nil)
w := httptest.NewRecorder()
t.ProfilesProvider.On("FindProfileByUsername", mock.Anything, "mock_username", true).Return(&db.Profile{
Uuid: "mock-uuid",
Username: "mock_username",
SkinUrl: "https://example.com/skin.png",
SkinModel: "slim",
}, nil)
t.SignerService.On("Sign", mock.Anything, "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6Im1vY2stdXVpZCIsInByb2ZpbGVOYW1lIjoibW9ja191c2VybmFtZSIsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9za2luLnBuZyIsIm1ldGFkYXRhIjp7Im1vZGVsIjoic2xpbSJ9fX19").Return("mock signature", nil)
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/json", result.Header.Get("Content-Type"))
body, _ := io.ReadAll(result.Body)
t.JSONEq(`{
"id": "mock-uuid",
"name": "mock_username",
"properties": [
{
"name": "textures",
"signature": "mock signature",
"value": "eyJ0aW1lc3RhbXAiOjE2MTQyMTQyMjMwMDAsInByb2ZpbGVJZCI6Im1vY2stdXVpZCIsInByb2ZpbGVOYW1lIjoibW9ja191c2VybmFtZSIsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9za2luLnBuZyIsIm1ldGFkYXRhIjp7Im1vZGVsIjoic2xpbSJ9fX19"
},
{
"name": "texturesParamName",
"value": "texturesParamValue"
}
]
}`, string(body))
})
t.Run("not exists profile", func() {
req := httptest.NewRequest("GET", "http://chrly/profile/mock_username", nil)
w := httptest.NewRecorder()
t.ProfilesProvider.On("FindProfileByUsername", mock.Anything, "mock_username", true).Return(nil, nil)
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusNotFound, result.StatusCode)
body, _ := io.ReadAll(result.Body)
t.Empty(body)
})
t.Run("err from profiles provider", func() {
req := httptest.NewRequest("GET", "http://chrly/profile/mock_username", nil)
w := httptest.NewRecorder()
t.ProfilesProvider.On("FindProfileByUsername", mock.Anything, "mock_username", true).Return(nil, errors.New("mock error"))
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusInternalServerError, result.StatusCode)
})
t.Run("err from textures signer", func() {
req := httptest.NewRequest("GET", "http://chrly/profile/mock_username?unsigned=false", nil)
w := httptest.NewRecorder()
t.ProfilesProvider.On("FindProfileByUsername", mock.Anything, "mock_username", true).Return(&db.Profile{}, nil)
t.SignerService.On("Sign", mock.Anything, mock.Anything).Return("", errors.New("mock error"))
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusInternalServerError, result.StatusCode)
})
}
func (t *SkinsystemTestSuite) TestSignatureVerificationKey() {
t.Run("in pem format", func() {
publicKey := "mock public key in pem format"
t.SignerService.On("GetPublicKey", mock.Anything, "pem").Return(publicKey, nil)
req := httptest.NewRequest("GET", "http://chrly/signature-verification-key.pem", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/x-pem-file", result.Header.Get("Content-Type"))
t.Equal(`attachment; filename="yggdrasil_session_pubkey.pem"`, result.Header.Get("Content-Disposition"))
body, _ := io.ReadAll(result.Body)
t.Equal(publicKey, string(body))
})
t.Run("in der format", func() {
publicKey := "mock public key in der format"
t.SignerService.On("GetPublicKey", mock.Anything, "der").Return(publicKey, nil)
req := httptest.NewRequest("GET", "http://chrly/signature-verification-key.der", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusOK, result.StatusCode)
t.Equal("application/octet-stream", result.Header.Get("Content-Type"))
t.Equal(`attachment; filename="yggdrasil_session_pubkey.der"`, result.Header.Get("Content-Disposition"))
body, _ := io.ReadAll(result.Body)
t.Equal(publicKey, string(body))
})
t.Run("handle error", func() {
t.SignerService.On("GetPublicKey", mock.Anything, "pem").Return("", errors.New("mock error"))
req := httptest.NewRequest("GET", "http://chrly/signature-verification-key.pem", nil)
w := httptest.NewRecorder()
t.App.Handler().ServeHTTP(w, req)
result := w.Result()
t.Equal(http.StatusInternalServerError, result.StatusCode)
})
}
func TestSkinsystem(t *testing.T) {
suite.Run(t, new(SkinsystemTestSuite))
}