mirror of
https://github.com/elyby/chrly.git
synced 2025-05-31 14:11:51 +05:30
Remove profiles endpoint and textures signing mechanism
This commit is contained in:
@@ -165,68 +165,3 @@ paths:
|
|||||||
description: The profiles has been successfully deleted.
|
description: The profiles has been successfully deleted.
|
||||||
401:
|
401:
|
||||||
$ref: "#/components/responses/UnauthorizedError"
|
$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"
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ var serveCmd = &cobra.Command{
|
|||||||
Use: "serve",
|
Use: "serve",
|
||||||
Short: "Starts HTTP handler for the skins system",
|
Short: "Starts HTTP handler for the skins system",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return startServer(di.ModuleSkinsystem, di.ModuleProfiles, di.ModuleSigner)
|
return startServer(di.ModuleSkinsystem, di.ModuleProfiles)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ func New() (*di.Container, error) {
|
|||||||
loggerDiOptions,
|
loggerDiOptions,
|
||||||
mojangDiOptions,
|
mojangDiOptions,
|
||||||
profilesDiOptions,
|
profilesDiOptions,
|
||||||
securityDiOptions,
|
|
||||||
serverDiOptions,
|
serverDiOptions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ import (
|
|||||||
|
|
||||||
const ModuleSkinsystem = "skinsystem"
|
const ModuleSkinsystem = "skinsystem"
|
||||||
const ModuleProfiles = "profiles"
|
const ModuleProfiles = "profiles"
|
||||||
const ModuleSigner = "signer"
|
|
||||||
|
|
||||||
var handlersDiOptions = di.Options(
|
var handlersDiOptions = di.Options(
|
||||||
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
|
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
|
||||||
di.Provide(newSkinsystemHandler, di.WithName(ModuleSkinsystem)),
|
di.Provide(newSkinsystemHandler, di.WithName(ModuleSkinsystem)),
|
||||||
di.Provide(newProfilesApiHandler, di.WithName(ModuleProfiles)),
|
di.Provide(newProfilesApiHandler, di.WithName(ModuleProfiles)),
|
||||||
di.Provide(newSignerApiHandler, di.WithName(ModuleSigner)),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newHandlerFactory(
|
func newHandlerFactory(
|
||||||
@@ -65,26 +63,6 @@ func newHandlerFactory(
|
|||||||
mount(router, "/api/profiles", profilesApiRouter)
|
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
|
// 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
|
// must first be initialized and each of them can publish its own checkers
|
||||||
var healthCheckers []*namedHealthChecker
|
var healthCheckers []*namedHealthChecker
|
||||||
@@ -107,14 +85,12 @@ func newHandlerFactory(
|
|||||||
func newSkinsystemHandler(
|
func newSkinsystemHandler(
|
||||||
config *viper.Viper,
|
config *viper.Viper,
|
||||||
profilesProvider ProfilesProvider,
|
profilesProvider ProfilesProvider,
|
||||||
texturesSigner SignerService,
|
|
||||||
) (*mux.Router, error) {
|
) (*mux.Router, error) {
|
||||||
config.SetDefault("textures.extra_param_name", "chrly")
|
config.SetDefault("textures.extra_param_name", "chrly")
|
||||||
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
|
||||||
|
|
||||||
skinsystem, err := NewSkinsystemApi(
|
skinsystem, err := NewSkinsystemApi(
|
||||||
profilesProvider,
|
profilesProvider,
|
||||||
texturesSigner,
|
|
||||||
config.GetString("textures.extra_param_name"),
|
config.GetString("textures.extra_param_name"),
|
||||||
config.GetString("textures.extra_param_value"),
|
config.GetString("textures.extra_param_value"),
|
||||||
)
|
)
|
||||||
@@ -134,15 +110,6 @@ func newProfilesApiHandler(profilesManager ProfilesManager) (*mux.Router, error)
|
|||||||
return profilesApi.Handler(), nil
|
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) {
|
func mount(router *mux.Router, path string, handler http.Handler) {
|
||||||
router.PathPrefix(path).Handler(
|
router.PathPrefix(path).Handler(
|
||||||
http.StripPrefix(
|
http.StripPrefix(
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,10 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
@@ -17,24 +14,14 @@ import (
|
|||||||
"ely.by/chrly/internal/db"
|
"ely.by/chrly/internal/db"
|
||||||
"ely.by/chrly/internal/mojang"
|
"ely.by/chrly/internal/mojang"
|
||||||
"ely.by/chrly/internal/otel"
|
"ely.by/chrly/internal/otel"
|
||||||
"ely.by/chrly/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var timeNow = time.Now
|
|
||||||
|
|
||||||
type ProfilesProvider interface {
|
type ProfilesProvider interface {
|
||||||
FindProfileByUsername(ctx context.Context, username string, allowProxy bool) (*db.Profile, error)
|
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(
|
func NewSkinsystemApi(
|
||||||
profilesProvider ProfilesProvider,
|
profilesProvider ProfilesProvider,
|
||||||
signerService SignerService,
|
|
||||||
texturesExtraParamName string,
|
texturesExtraParamName string,
|
||||||
texturesExtraParamValue string,
|
texturesExtraParamValue string,
|
||||||
) (*Skinsystem, error) {
|
) (*Skinsystem, error) {
|
||||||
@@ -45,7 +32,6 @@ func NewSkinsystemApi(
|
|||||||
|
|
||||||
return &Skinsystem{
|
return &Skinsystem{
|
||||||
ProfilesProvider: profilesProvider,
|
ProfilesProvider: profilesProvider,
|
||||||
SignerService: signerService,
|
|
||||||
TexturesExtraParamName: texturesExtraParamName,
|
TexturesExtraParamName: texturesExtraParamName,
|
||||||
TexturesExtraParamValue: texturesExtraParamValue,
|
TexturesExtraParamValue: texturesExtraParamValue,
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
@@ -54,7 +40,6 @@ func NewSkinsystemApi(
|
|||||||
|
|
||||||
type Skinsystem struct {
|
type Skinsystem struct {
|
||||||
ProfilesProvider
|
ProfilesProvider
|
||||||
SignerService
|
|
||||||
TexturesExtraParamName string
|
TexturesExtraParamName string
|
||||||
TexturesExtraParamValue string
|
TexturesExtraParamValue string
|
||||||
metrics *skinsystemApiMetrics
|
metrics *skinsystemApiMetrics
|
||||||
@@ -68,12 +53,9 @@ func (s *Skinsystem) Handler() *mux.Router {
|
|||||||
// TODO: alias /capes/{username}?
|
// TODO: alias /capes/{username}?
|
||||||
router.HandleFunc("/textures/{username}", s.texturesHandler).Methods(http.MethodGet)
|
router.HandleFunc("/textures/{username}", s.texturesHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/textures/signed/{username}", s.signedTexturesHandler).Methods(http.MethodGet)
|
router.HandleFunc("/textures/signed/{username}", s.signedTexturesHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/profile/{username}", s.profileHandler).Methods(http.MethodGet)
|
|
||||||
// Legacy
|
// Legacy
|
||||||
router.HandleFunc("/skins", s.legacySkinHandler).Methods(http.MethodGet)
|
router.HandleFunc("/skins", s.legacySkinHandler).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/cloaks", s.legacyCapeHandler).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
|
return router
|
||||||
}
|
}
|
||||||
@@ -212,83 +194,6 @@ func (s *Skinsystem) signedTexturesHandler(resp http.ResponseWriter, req *http.R
|
|||||||
_, _ = resp.Write(responseJson)
|
_, _ = 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 {
|
func parseUsername(username string) string {
|
||||||
return strings.TrimSuffix(username, ".png")
|
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}"))
|
m.SignedTexturesRequest, err = meter.Int64Counter("chrly.app.skinsystem.signed_textures.request", metric.WithUnit("{request}"))
|
||||||
errors = multierr.Append(errors, err)
|
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
|
return m, errors
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +260,4 @@ type skinsystemApiMetrics struct {
|
|||||||
LegacyCapeRequest metric.Int64Counter
|
LegacyCapeRequest metric.Int64Counter
|
||||||
TexturesRequest metric.Int64Counter
|
TexturesRequest metric.Int64Counter
|
||||||
SignedTexturesRequest metric.Int64Counter
|
SignedTexturesRequest metric.Int64Counter
|
||||||
ProfileRequest metric.Int64Counter
|
|
||||||
SigningKeyRequest metric.Int64Counter
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
testify "github.com/stretchr/testify/require"
|
testify "github.com/stretchr/testify/require"
|
||||||
@@ -30,27 +29,12 @@ func (m *ProfilesProviderMock) FindProfileByUsername(ctx context.Context, userna
|
|||||||
return result, args.Error(1)
|
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 {
|
type SkinsystemTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
App *Skinsystem
|
App *Skinsystem
|
||||||
|
|
||||||
ProfilesProvider *ProfilesProviderMock
|
ProfilesProvider *ProfilesProviderMock
|
||||||
SignerService *SignerServiceMock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/********************
|
/********************
|
||||||
@@ -58,17 +42,10 @@ type SkinsystemTestSuite struct {
|
|||||||
********************/
|
********************/
|
||||||
|
|
||||||
func (t *SkinsystemTestSuite) SetupSubTest() {
|
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.ProfilesProvider = &ProfilesProviderMock{}
|
||||||
t.SignerService = &SignerServiceMock{}
|
|
||||||
|
|
||||||
t.App, _ = NewSkinsystemApi(
|
t.App, _ = NewSkinsystemApi(
|
||||||
t.ProfilesProvider,
|
t.ProfilesProvider,
|
||||||
t.SignerService,
|
|
||||||
"texturesParamName",
|
"texturesParamName",
|
||||||
"texturesParamValue",
|
"texturesParamValue",
|
||||||
)
|
)
|
||||||
@@ -76,7 +53,6 @@ func (t *SkinsystemTestSuite) SetupSubTest() {
|
|||||||
|
|
||||||
func (t *SkinsystemTestSuite) TearDownSubTest() {
|
func (t *SkinsystemTestSuite) TearDownSubTest() {
|
||||||
t.ProfilesProvider.AssertExpectations(t.T())
|
t.ProfilesProvider.AssertExpectations(t.T())
|
||||||
t.SignerService.AssertExpectations(t.T())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *SkinsystemTestSuite) TestSkinHandler() {
|
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) {
|
func TestSkinsystem(t *testing.T) {
|
||||||
suite.Run(t, new(SkinsystemTestSuite))
|
suite.Run(t, new(SkinsystemTestSuite))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user