diff --git a/config.example.yml b/config.example.yml index 5bc8020..a435576 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,12 +1,7 @@ environment: development host: 127.0.0.1 port: 3000 -redis: - host: 127.0.0.1 - port: 6379 - user: - password: - database: 0 +redis: redis://127.0.0.1:6379/0 routes: face: default_overlay: true diff --git a/src/cache.go b/src/cache.go index b82eb67..b7df904 100644 --- a/src/cache.go +++ b/src/cache.go @@ -1,12 +1,9 @@ package main import ( - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" "image" - "log" ) type ResultCacheKey struct { @@ -31,9 +28,7 @@ func GetResultCacheKey(uuid, renderType string, opts *QueryParams) (string, erro return "", err } - hash := sha256.Sum256(rawKeyData) - - return fmt.Sprintf("result:%s", hex.EncodeToString(hash[:])), nil + return fmt.Sprintf("result:%s", SHA256(rawKeyData)), nil } // GetCachedRenderResult returns the render result from Redis cache, or nil if it does not exist or cache is disabled. @@ -81,12 +76,26 @@ func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) { return nil, false, err } - if config.Environment == "development" { - log.Printf("Retrieved player skin from cache (uuid=%s, slim=%v)\n", uuid, slim) - } - return cache, slim, nil } return nil, false, nil } + +func SetCachedSkin(uuid string, value []byte, isSlim bool) error { + if err := r.Set(fmt.Sprintf("skin:%s", uuid), value, *config.Cache.SkinCacheDuration); err != nil { + return err + } + + if isSlim { + if err := r.Set(fmt.Sprintf("slim:%s", uuid), "true", *config.Cache.SkinCacheDuration); err != nil { + return err + } + } else { + if err := r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil { + return err + } + } + + return nil +} diff --git a/src/config.go b/src/config.go index 4445674..5627554 100644 --- a/src/config.go +++ b/src/config.go @@ -13,14 +13,8 @@ var ( Environment: "development", Host: "127.0.0.1", Port: 3001, - Redis: RedisConfig{ - Host: "127.0.0.1", - Port: 6379, - User: "", - Password: "", - Database: 0, - }, - Routes: RoutesConfig{ + Redis: "redis://127.0.0.1:6379/0", + Routes: Routes{ Face: RouteConfig{ DefaultOverlay: true, DefaultDownload: false, @@ -84,16 +78,16 @@ var ( // Config is the root configuration object for the application. type Config struct { - Environment string `yaml:"environment"` - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - Redis RedisConfig `yaml:"redis"` - Routes RoutesConfig `yaml:"routes"` - Cache CacheConfig `yaml:"cache"` + Environment string `yaml:"environment"` + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + Redis string `yaml:"redis"` + Routes Routes `yaml:"routes"` + Cache CacheConfig `yaml:"cache"` } -// RoutesConfig is the configuration data of all API routes. -type RoutesConfig struct { +// Routes is the configuration data of all API routes. +type Routes struct { Face RouteConfig `yaml:"face"` Head RouteConfig `yaml:"head"` FullBody RouteConfig `yaml:"full_body"` @@ -113,15 +107,6 @@ type RouteConfig struct { MaxScale int `yaml:"max_scale"` } -// RedisConfig is the configuration data used to connect to Redis. -type RedisConfig struct { - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - User string `yaml:"user"` - Password string `yaml:"password"` - Database int `yaml:"database"` -} - // CacheConfig is the configuration data used to set TTL values for Redis keys. type CacheConfig struct { SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"` diff --git a/src/mojang.go b/src/mojang.go index cd60321..6156538 100644 --- a/src/mojang.go +++ b/src/mojang.go @@ -20,8 +20,8 @@ type MinecraftProfile struct { } `json:"properties"` } -// MinecraftDecodedTextures is the decoded object of the base64-encoded values property in a MinecraftProfile properties value. -type MinecraftDecodedTextures struct { +// DecodedTextures is the decoded object of the base64-encoded values property in a MinecraftProfile properties value. +type DecodedTextures struct { Timestamp int64 `json:"timestamp"` UUID string `json:"uuid"` Username string `json:"username"` @@ -47,7 +47,7 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) { return nil, err } - req.Header.Set("User-Agent", "mineatar.io Skin Render API") + req.Header.Set("User-Agent", "mineatar.io") resp, err := http.DefaultClient.Do(req) @@ -71,7 +71,7 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) { return nil, err } - response := MinecraftProfile{} + var response MinecraftProfile if err = json.Unmarshal(body, &response); err != nil { return nil, err @@ -80,15 +80,15 @@ func GetMinecraftProfile(uuid string) (*MinecraftProfile, error) { return &response, nil } -// GetDecodedTexturesValue decodes the values from a MinecraftProfileTextures texture value. -func GetDecodedTexturesValue(value string) (*MinecraftDecodedTextures, error) { +// DecodeTexturesValue decodes the value from a MinecraftProfile texture property. +func DecodeTexturesValue(value string) (*DecodedTextures, error) { rawResult, err := base64.StdEncoding.DecodeString(value) if err != nil { return nil, err } - result := MinecraftDecodedTextures{} + var result DecodedTextures if err = json.Unmarshal(rawResult, &result); err != nil { return nil, err diff --git a/src/redis.go b/src/redis.go index c83e251..5c04a7a 100644 --- a/src/redis.go +++ b/src/redis.go @@ -3,7 +3,6 @@ package main import ( "bytes" "context" - "fmt" "image" "image/draw" "time" @@ -24,17 +23,18 @@ type Redis struct { } // Connect connects to the Redis server using the configuration values provided. -func (r *Redis) Connect(conf RedisConfig) error { +func (r *Redis) Connect(url string) error { ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) defer cancel() - r.Client = redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%d", conf.Host, conf.Port), - Username: conf.User, - Password: conf.Password, - DB: conf.Database, - }) + opts, err := redis.ParseURL(url) + + if err != nil { + return err + } + + r.Client = redis.NewClient(opts) if err := r.Client.Ping(ctx).Err(); err != nil { return err diff --git a/src/renderer.go b/src/renderer.go new file mode 100644 index 0000000..01d8295 --- /dev/null +++ b/src/renderer.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "image" + + "github.com/mineatar-io/skin-render" +) + +var ( + RenderTypeFullBody = "fullbody" + RenderTypeFrontBody = "frontbody" + RenderTypeBackBody = "backbody" + RenderTypeLeftBody = "leftbody" + RenderTypeRightBody = "rightbody" + RenderTypeFace = "face" + RenderTypeHead = "head" +) + +// Render will render the image using the specified details and return the result. +func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) { + if config.Cache.EnableLocks { + mutex := r.NewMutex(fmt.Sprintf("render-lock:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid)) + mutex.Lock() + + defer mutex.Unlock() + } + + // Fetch the existing render from cache if it exists + { + cache, err := GetCachedRenderResult(renderType, uuid, opts) + + if err != nil { + return nil, false, err + } + + if cache != nil { + return cache, true, nil + } + } + + var ( + result *image.NRGBA + renderOpts skin.Options = skin.Options{ + Overlay: opts.Overlay, + Slim: isSlim, + Scale: opts.Scale, + } + ) + + // Render the image based on the type provided + { + switch renderType { + case RenderTypeFullBody: + { + result = skin.RenderBody(rawSkin, renderOpts) + + break + } + case RenderTypeFrontBody: + { + result = skin.RenderFrontBody(rawSkin, renderOpts) + + break + } + case RenderTypeBackBody: + { + result = skin.RenderBackBody(rawSkin, renderOpts) + + break + } + case RenderTypeLeftBody: + { + result = skin.RenderLeftBody(rawSkin, renderOpts) + + break + } + case RenderTypeRightBody: + { + result = skin.RenderRightBody(rawSkin, renderOpts) + + break + } + case RenderTypeHead: + { + result = skin.RenderHead(rawSkin, renderOpts) + + break + } + case RenderTypeFace: + { + result = skin.RenderFace(rawSkin, renderOpts) + + break + } + default: + panic(fmt.Errorf("unknown render type: %s", renderType)) + } + } + + var ( + data []byte + err error + ) + + // Encode the image into a PNG in byte-array format + { + data, err = EncodePNG(result) + + if err != nil { + return nil, false, err + } + } + + // Put the result into the cache for later use + { + if err = SetCachedRenderResult(renderType, uuid, opts, data); err != nil { + return nil, false, err + } + } + + return data, false, nil +} diff --git a/src/util.go b/src/util.go index 6f42c19..da6b44b 100644 --- a/src/util.go +++ b/src/util.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "crypto/sha256" _ "embed" + "encoding/hex" "fmt" "image" "image/draw" @@ -20,14 +22,6 @@ import ( var ( //go:embed favicon.ico favicon []byte - - RenderTypeFullBody = "fullbody" - RenderTypeFrontBody = "frontbody" - RenderTypeBackBody = "backbody" - RenderTypeLeftBody = "leftbody" - RenderTypeRightBody = "rightbody" - RenderTypeFace = "face" - RenderTypeHead = "head" ) // QueryParams is used by most all API routes as options for how the image should be rendered, or how errors should be handled. @@ -56,102 +50,6 @@ func Clamp[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 return value } -// Render will render the image using the specified details and return the result. -func Render(renderType, uuid string, rawSkin *image.NRGBA, isSlim bool, opts *QueryParams) ([]byte, bool, error) { - if config.Cache.EnableLocks { - mutex := r.NewMutex(fmt.Sprintf("render-lock:%s-%d-%t-%s", renderType, opts.Scale, opts.Overlay, uuid)) - mutex.Lock() - - defer mutex.Unlock() - } - - cache, err := GetCachedRenderResult(renderType, uuid, opts) - - if err != nil { - return nil, false, err - } - - if cache != nil { - if config.Environment == "development" { - log.Printf("Retrieved render from cache (type=%s, uuid=%s, slim=%v, scale=%d)\n", renderType, uuid, isSlim, opts.Scale) - } - - return cache, true, nil - } - - var ( - result *image.NRGBA - renderOpts skin.Options = skin.Options{ - Overlay: opts.Overlay, - Slim: isSlim, - Scale: opts.Scale, - } - ) - - switch renderType { - case RenderTypeFullBody: - { - result = skin.RenderBody(rawSkin, renderOpts) - - break - } - case RenderTypeFrontBody: - { - result = skin.RenderFrontBody(rawSkin, renderOpts) - - break - } - case RenderTypeBackBody: - { - result = skin.RenderBackBody(rawSkin, renderOpts) - - break - } - case RenderTypeLeftBody: - { - result = skin.RenderLeftBody(rawSkin, renderOpts) - - break - } - case RenderTypeRightBody: - { - result = skin.RenderRightBody(rawSkin, renderOpts) - - break - } - case RenderTypeHead: - { - result = skin.RenderHead(rawSkin, renderOpts) - - break - } - case RenderTypeFace: - { - result = skin.RenderFace(rawSkin, renderOpts) - - break - } - default: - panic(fmt.Errorf("unknown render type: %s", renderType)) - } - - data, err := EncodePNG(result) - - if err != nil { - return nil, false, err - } - - if err = SetCachedRenderResult(renderType, uuid, opts, data); err != nil { - return nil, false, err - } - - if config.Environment == "development" { - log.Printf("Rendered image (type=%s, uuid=%s, slim=%v, scale=%d)\n", renderType, uuid, isSlim, opts.Scale) - } - - return data, false, nil -} - // ExtractUUID returns the UUID from the route param, allowing values such as ".png" to be returned as "". func ExtractUUID(ctx *fiber.Ctx) string { return strings.Split(ctx.Params("uuid"), ".")[0] @@ -218,13 +116,13 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) { } var ( - err error = nil - skinImage *image.NRGBA = nil - rawSkin []byte = nil - isSlim bool = skin.IsSlimFromUUID(uuid) - profile *MinecraftProfile = nil - rawTextures string = "" - texturesProperty *MinecraftDecodedTextures = nil + err error = nil + skinImage *image.NRGBA = nil + rawSkin []byte = nil + isSlim bool = skin.IsSlimFromUUID(uuid) + profile *MinecraftProfile = nil + rawTextures string = "" + texturesProperty *DecodedTextures = nil ) // Get the textures metadata from Mojang about the Minecraft player @@ -259,7 +157,7 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) { // Decode the raw textures value returned from the player's properties { - if texturesProperty, err = GetDecodedTexturesValue(rawTextures); err != nil { + if texturesProperty, err = DecodeTexturesValue(rawTextures); err != nil { return nil, false, err } @@ -283,23 +181,9 @@ func GetPlayerSkin(uuid string) (*image.NRGBA, bool, error) { // Put the skin into cache so it can be used for future requests if config.Cache.SkinCacheDuration != nil { - if err = r.Set(fmt.Sprintf("skin:%s", uuid), rawSkin, *config.Cache.SkinCacheDuration); err != nil { + if err = SetCachedSkin(uuid, rawSkin, isSlim); err != nil { return nil, false, err } - - if isSlim { - if err = r.Set(fmt.Sprintf("slim:%s", uuid), "true", *config.Cache.SkinCacheDuration); err != nil { - return nil, false, err - } - } else { - if err = r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil { - return nil, false, err - } - } - } - - if config.Environment == "development" { - log.Printf("Fetched player skin from Mojang (uuid=%s, slim=%v)\n", uuid, isSlim) } return skinImage, isSlim, nil @@ -357,3 +241,10 @@ func GetInstanceID() (uint16, error) { return 0, nil } + +// SHA256 computes the SHA-256 hash of the input byte-array. +func SHA256(value []byte) string { + hash := sha256.Sum256(value) + + return hex.EncodeToString(hash[:]) +}