Rework project's structure

This commit is contained in:
ErickSkrauch
2024-02-01 07:58:26 +01:00
parent dac3ca9001
commit 77e466cc0d
69 changed files with 130 additions and 161 deletions

14
internal/di/config.go Normal file
View File

@@ -0,0 +1,14 @@
package di
import (
"github.com/defval/di"
"github.com/spf13/viper"
)
var config = di.Options(
di.Provide(newConfig),
)
func newConfig() *viper.Viper {
return viper.GetViper()
}

55
internal/di/db.go Normal file
View File

@@ -0,0 +1,55 @@
package di
import (
"context"
"fmt"
"github.com/defval/di"
"github.com/spf13/viper"
db2 "github.com/elyby/chrly/internal/db"
"github.com/elyby/chrly/internal/db/redis"
es "github.com/elyby/chrly/internal/eventsubscribers"
"github.com/elyby/chrly/internal/mojang"
"github.com/elyby/chrly/internal/profiles"
)
// v4 had the idea that it would be possible to separate backends for storing skins and capes.
// But in v5 the storage will be unified, so this is just temporary constructors before large reworking.
//
// Since there are no options for selecting target backends,
// all constants in this case point to static specific implementations.
var db = di.Options(
di.Provide(newRedis,
di.As(new(profiles.ProfilesRepository)),
di.As(new(profiles.ProfilesFinder)),
di.As(new(mojang.MojangUuidsStorage)),
),
)
func newRedis(container *di.Container, config *viper.Viper) (*redis.Redis, error) {
config.SetDefault("storage.redis.host", "localhost")
config.SetDefault("storage.redis.port", 6379)
config.SetDefault("storage.redis.poolSize", 10)
conn, err := redis.New(
context.Background(),
db2.NewZlibEncoder(&db2.JsonSerializer{}),
fmt.Sprintf("%s:%d", config.GetString("storage.redis.host"), config.GetInt("storage.redis.port")),
config.GetInt("storage.redis.poolSize"),
)
if err != nil {
return nil, err
}
if err := container.Provide(func() *namedHealthChecker {
return &namedHealthChecker{
Name: "redis",
Checker: es.DatabaseChecker(conn),
}
}); err != nil {
return nil, err
}
return conn, nil
}

22
internal/di/di.go Normal file
View File

@@ -0,0 +1,22 @@
package di
import "github.com/defval/di"
func New() (*di.Container, error) {
container, err := di.New(
config,
dispatcher,
logger,
db,
mojangTextures,
handlers,
profilesDi,
server,
signer,
)
if err != nil {
return nil, err
}
return container, nil
}

34
internal/di/dispatcher.go Normal file
View File

@@ -0,0 +1,34 @@
package di
import (
"github.com/defval/di"
"github.com/mono83/slf"
d "github.com/elyby/chrly/internal/dispatcher"
"github.com/elyby/chrly/internal/eventsubscribers"
"github.com/elyby/chrly/internal/http"
)
var dispatcher = di.Options(
di.Provide(newDispatcher,
di.As(new(d.Emitter)),
di.As(new(d.Subscriber)),
di.As(new(http.Emitter)),
di.As(new(eventsubscribers.Subscriber)),
),
di.Invoke(enableEventsHandlers),
)
func newDispatcher() d.Dispatcher {
return d.New()
}
func enableEventsHandlers(
dispatcher d.Subscriber,
logger slf.Logger,
statsReporter slf.StatsReporter,
) {
// TODO: use idea from https://github.com/defval/di/issues/10#issuecomment-615869852
(&eventsubscribers.Logger{Logger: logger}).ConfigureWithDispatcher(dispatcher)
(&eventsubscribers.StatsReporter{StatsReporter: statsReporter}).ConfigureWithDispatcher(dispatcher)
}

133
internal/di/handlers.go Normal file
View File

@@ -0,0 +1,133 @@
package di
import (
"errors"
"net/http"
"strings"
"github.com/defval/di"
"github.com/etherlabsio/healthcheck/v2"
"github.com/gorilla/mux"
"github.com/spf13/viper"
. "github.com/elyby/chrly/internal/http"
)
var handlers = di.Options(
di.Provide(newHandlerFactory, di.As(new(http.Handler))),
di.Provide(newSkinsystemHandler, di.WithName("skinsystem")),
di.Provide(newApiHandler, di.WithName("api")),
)
func newHandlerFactory(
container *di.Container,
config *viper.Viper,
emitter Emitter,
) (*mux.Router, error) {
enabledModules := config.GetStringSlice("modules")
// gorilla.mux has no native way to combine multiple routers.
// The hack used later in the code works for prefixes in addresses, but leads to misbehavior
// if you set an empty prefix. Since the main application should be mounted at the root prefix,
// we use it as the base router
var router *mux.Router
if hasValue(enabledModules, "skinsystem") {
if err := container.Resolve(&router, di.Name("skinsystem")); err != nil {
return nil, err
}
} else {
router = mux.NewRouter()
}
router.StrictSlash(true)
requestEventsMiddleware := CreateRequestEventsMiddleware(emitter, "skinsystem")
router.Use(requestEventsMiddleware)
// NotFoundHandler doesn't call for registered middlewares, so we must wrap it manually.
// See https://github.com/gorilla/mux/issues/416#issuecomment-600079279
router.NotFoundHandler = requestEventsMiddleware(http.HandlerFunc(NotFoundHandler))
if hasValue(enabledModules, "api") {
var apiRouter *mux.Router
if err := container.Resolve(&apiRouter, di.Name("api")); err != nil {
return nil, err
}
var authenticator Authenticator
if err := container.Resolve(&authenticator); err != nil {
return nil, err
}
apiRouter.Use(CreateAuthenticationMiddleware(authenticator))
mount(router, "/api", apiRouter)
}
err := container.Invoke(enableReporters)
if err != nil && !errors.Is(err, di.ErrTypeNotExists) {
return nil, err
}
// 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
if has, _ := container.Has(&healthCheckers); has {
if err := container.Resolve(&healthCheckers); err != nil {
return nil, err
}
checkersOptions := make([]healthcheck.Option, len(healthCheckers))
for i, checker := range healthCheckers {
checkersOptions[i] = healthcheck.WithChecker(checker.Name, checker.Checker)
}
router.Handle("/healthcheck", healthcheck.Handler(checkersOptions...)).Methods("GET")
}
return router, nil
}
func newSkinsystemHandler(
config *viper.Viper,
profilesProvider ProfilesProvider,
texturesSigner TexturesSigner,
) *mux.Router {
config.SetDefault("textures.extra_param_name", "chrly")
config.SetDefault("textures.extra_param_value", "how do you tame a horse in Minecraft?")
return (&Skinsystem{
ProfilesProvider: profilesProvider,
TexturesSigner: texturesSigner,
TexturesExtraParamName: config.GetString("textures.extra_param_name"),
TexturesExtraParamValue: config.GetString("textures.extra_param_value"),
}).Handler()
}
func newApiHandler(profilesManager ProfilesManager) *mux.Router {
return (&Api{
ProfilesManager: profilesManager,
}).Handler()
}
func hasValue(slice []string, needle string) bool {
for _, value := range slice {
if value == needle {
return true
}
}
return false
}
func mount(router *mux.Router, path string, handler http.Handler) {
router.PathPrefix(path).Handler(
http.StripPrefix(
strings.TrimSuffix(path, "/"),
handler,
),
)
}
type namedHealthChecker struct {
Name string
Checker healthcheck.Checker
}

104
internal/di/logger.go Normal file
View File

@@ -0,0 +1,104 @@
package di
import (
"os"
"github.com/defval/di"
"github.com/getsentry/raven-go"
"github.com/mono83/slf"
"github.com/mono83/slf/rays"
"github.com/mono83/slf/recievers/sentry"
"github.com/mono83/slf/recievers/statsd"
"github.com/mono83/slf/recievers/writer"
"github.com/mono83/slf/wd"
"github.com/spf13/viper"
"github.com/elyby/chrly/internal/eventsubscribers"
"github.com/elyby/chrly/internal/version"
)
var logger = di.Options(
di.Provide(newLogger),
di.Provide(newSentry),
di.Provide(newStatsReporter),
)
type loggerParams struct {
di.Inject
SentryRaven *raven.Client `di:"" optional:"true"`
}
func newLogger(params loggerParams) slf.Logger {
dispatcher := &slf.Dispatcher{}
dispatcher.AddReceiver(writer.New(writer.Options{
Marker: false,
TimeFormat: "15:04:05.000",
}))
if params.SentryRaven != nil {
sentryReceiver, _ := sentry.NewReceiverWithCustomRaven(
params.SentryRaven,
&sentry.Config{
MinLevel: "warn",
},
)
dispatcher.AddReceiver(sentryReceiver)
}
logger := wd.Custom("", "", dispatcher)
logger.WithParams(rays.Host)
return logger
}
func newSentry(config *viper.Viper) (*raven.Client, error) {
sentryAddr := config.GetString("sentry.dsn")
if sentryAddr == "" {
return nil, nil
}
ravenClient, err := raven.New(sentryAddr)
if err != nil {
return nil, err
}
ravenClient.SetEnvironment("production")
ravenClient.SetDefaultLoggerName("sentry-watchdog-receiver")
ravenClient.SetRelease(version.Version())
raven.DefaultClient = ravenClient
return ravenClient, nil
}
func newStatsReporter(config *viper.Viper) (slf.StatsReporter, error) {
dispatcher := &slf.Dispatcher{}
statsdAddr := config.GetString("statsd.addr")
if statsdAddr != "" {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
statsdReceiver, err := statsd.NewReceiver(statsd.Config{
Address: statsdAddr,
Prefix: "ely.skinsystem." + hostname + ".app.",
FlushEvery: 1,
})
if err != nil {
return nil, err
}
dispatcher.AddReceiver(statsdReceiver)
}
return wd.Custom("", "", dispatcher), nil
}
func enableReporters(reporter slf.StatsReporter, factories []eventsubscribers.Reporter) {
for _, factory := range factories {
factory.Enable(reporter)
}
}

View File

@@ -0,0 +1,108 @@
package di
import (
"net/http"
"net/url"
"time"
"github.com/defval/di"
"github.com/spf13/viper"
"github.com/elyby/chrly/internal/mojang"
"github.com/elyby/chrly/internal/profiles"
)
var mojangTextures = di.Options(
di.Provide(newMojangApi),
di.Provide(newMojangTexturesProviderFactory),
di.Provide(newMojangTexturesProvider),
di.Provide(newMojangTexturesUuidsProviderFactory),
di.Provide(newMojangTexturesBatchUUIDsProvider),
di.Provide(newMojangSignedTexturesProvider),
)
func newMojangApi(config *viper.Viper) (*mojang.MojangApi, error) {
batchUuidsUrl := config.GetString("mojang.batch_uuids_url")
if batchUuidsUrl != "" {
if _, err := url.ParseRequestURI(batchUuidsUrl); err != nil {
return nil, err
}
}
profileUrl := config.GetString("mojang.profile_url")
if profileUrl != "" {
if _, err := url.ParseRequestURI(batchUuidsUrl); err != nil {
return nil, err
}
}
httpClient := &http.Client{} // TODO: extract to the singleton dependency
return mojang.NewMojangApi(httpClient, batchUuidsUrl, profileUrl), nil
}
func newMojangTexturesProviderFactory(
container *di.Container,
config *viper.Viper,
) (profiles.MojangProfilesProvider, error) {
config.SetDefault("mojang_textures.enabled", true)
if !config.GetBool("mojang_textures.enabled") {
return &mojang.NilProvider{}, nil
}
var provider *mojang.MojangTexturesProvider
err := container.Resolve(&provider)
if err != nil {
return nil, err
}
return provider, nil
}
func newMojangTexturesProvider(
uuidsProvider mojang.UuidsProvider,
texturesProvider mojang.TexturesProvider,
) *mojang.MojangTexturesProvider {
return &mojang.MojangTexturesProvider{
UuidsProvider: uuidsProvider,
TexturesProvider: texturesProvider,
}
}
func newMojangTexturesUuidsProviderFactory(
batchProvider *mojang.BatchUuidsProvider,
uuidsStorage mojang.MojangUuidsStorage,
) mojang.UuidsProvider {
return &mojang.UuidsProviderWithCache{
Provider: batchProvider,
Storage: uuidsStorage,
}
}
func newMojangTexturesBatchUUIDsProvider(
mojangApi *mojang.MojangApi,
config *viper.Viper,
) (*mojang.BatchUuidsProvider, error) {
config.SetDefault("queue.loop_delay", 2*time.Second+500*time.Millisecond)
config.SetDefault("queue.batch_size", 10)
config.SetDefault("queue.strategy", "periodic")
// TODO: healthcheck is broken
uuidsProvider := mojang.NewBatchUuidsProvider(
mojangApi.UsernamesToUuids,
config.GetInt("queue.batch_size"),
config.GetDuration("queue.loop_delay"),
config.GetString("queue.strategy") == "full-bus",
)
return uuidsProvider, nil
}
func newMojangSignedTexturesProvider(mojangApi *mojang.MojangApi) mojang.TexturesProvider {
return mojang.NewTexturesProviderWithInMemoryCache(
&mojang.MojangApiTexturesProvider{
MojangApiTexturesEndpoint: mojangApi.UuidToTextures,
},
)
}

27
internal/di/profiles.go Normal file
View File

@@ -0,0 +1,27 @@
package di
import (
"github.com/defval/di"
. "github.com/elyby/chrly/internal/http"
"github.com/elyby/chrly/internal/profiles"
)
var profilesDi = di.Options(
di.Provide(newProfilesManager, di.As(new(ProfilesManager))),
di.Provide(newProfilesProvider, di.As(new(ProfilesProvider))),
)
func newProfilesManager(r profiles.ProfilesRepository) *profiles.Manager {
return profiles.NewManager(r)
}
func newProfilesProvider(
finder profiles.ProfilesFinder,
mojangProfilesProvider profiles.MojangProfilesProvider,
) *profiles.Provider {
return &profiles.Provider{
ProfilesFinder: finder,
MojangProfilesProvider: mojangProfilesProvider,
}
}

79
internal/di/server.go Normal file
View File

@@ -0,0 +1,79 @@
package di
import (
"errors"
"fmt"
"net/http"
"runtime/debug"
"time"
"github.com/defval/di"
"github.com/getsentry/raven-go"
"github.com/spf13/viper"
. "github.com/elyby/chrly/internal/http"
)
var server = di.Options(
di.Provide(newAuthenticator, di.As(new(Authenticator))),
di.Provide(newServer),
)
func newAuthenticator(config *viper.Viper, emitter Emitter) (*JwtAuth, error) {
key := config.GetString("chrly.secret")
if key == "" {
return nil, errors.New("chrly.secret must be set in order to use authenticator")
}
return &JwtAuth{
Key: []byte(key),
Emitter: emitter,
}, nil
}
type serverParams struct {
di.Inject
Config *viper.Viper `di:""`
Handler http.Handler `di:""`
Sentry *raven.Client `di:"" optional:"true"`
}
func newServer(params serverParams) *http.Server {
params.Config.SetDefault("server.host", "")
params.Config.SetDefault("server.port", 80)
var handler http.Handler
if params.Sentry != nil {
// raven.Recoverer uses DefaultClient and nothing can be done about it
// To avoid code duplication, if the Sentry service is successfully initiated,
// it will also replace DefaultClient, so raven.Recoverer will work with the instance
// created in the application constructor
handler = raven.Recoverer(params.Handler)
} else {
// Raven's Recoverer is prints the stacktrace and sets the corresponding status itself.
// But there is no magic and if you don't define a panic handler, Mux will just reset the connection
handler = http.HandlerFunc(func(request http.ResponseWriter, response *http.Request) {
defer func() {
if recovered := recover(); recovered != nil {
debug.PrintStack() // TODO: colorize output
request.WriteHeader(http.StatusInternalServerError)
}
}()
params.Handler.ServeHTTP(request, response)
})
}
address := fmt.Sprintf("%s:%d", params.Config.GetString("server.host"), params.Config.GetInt("server.port"))
server := &http.Server{
Addr: address,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 16,
Handler: handler,
}
return server
}

49
internal/di/signer.go Normal file
View File

@@ -0,0 +1,49 @@
package di
import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"strings"
"github.com/elyby/chrly/internal/http"
. "github.com/elyby/chrly/internal/signer"
"github.com/defval/di"
"github.com/spf13/viper"
)
var signer = di.Options(
di.Provide(newTexturesSigner,
di.As(new(http.TexturesSigner)),
),
)
func newTexturesSigner(config *viper.Viper) (*Signer, error) {
keyStr := config.GetString("chrly.signing.key")
if keyStr == "" {
return nil, errors.New("chrly.signing.key must be set in order to sign textures")
}
var keyBytes []byte
if strings.HasPrefix(keyStr, "base64:") {
base64Value := keyStr[7:]
decodedKey, err := base64.URLEncoding.DecodeString(base64Value)
if err != nil {
return nil, err
}
keyBytes = decodedKey
} else {
keyBytes = []byte(keyStr)
}
rawPem, _ := pem.Decode(keyBytes)
key, err := x509.ParsePKCS1PrivateKey(rawPem.Bytes)
if err != nil {
return nil, err
}
return &Signer{Key: key}, nil
}