From 59abebd7e2f7e4919d9e64ef88cf9f7a4aed4339 Mon Sep 17 00:00:00 2001 From: Nicolas Carlier Date: Tue, 31 Oct 2023 09:52:52 +0000 Subject: [PATCH] chore(image-proxy): switch to imgproxy service --- docker-compose.dev.yml | 10 +++++----- docker-compose.yml | 10 +++++----- pkg/config/config.go | 9 +++++++-- pkg/config/defaults.toml | 9 ++++++--- pkg/config/test/config_test.go | 5 +++-- pkg/config/test/test.toml | 4 ++-- pkg/config/types.go | 3 ++- pkg/config/unmarshal.go | 15 +++++++++++++- pkg/helper/hashid.go | 4 ++-- pkg/schema/article/queries.go | 3 +++ pkg/service/articles.go | 14 ------------- pkg/service/articles_thumbnail.go | 22 +++++++++++++++++++++ pkg/service/registry.go | 2 +- ui/src/articles/components/ArticleImage.tsx | 8 +++----- 14 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 pkg/service/articles_thumbnail.go diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ec3d7e946..fae06b8f1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -16,13 +16,13 @@ services: ####################################### # Imagor: Image proxy ####################################### - imagor: - image: shumc/imagor - command: -debug -imagor-auto-webp + imgproxy: + image: darthsim/imgproxy environment: - IMAGOR_SECRET: ${READFLOW_HASH_SECRET_SALT:-pepper} + IMGPROXY_KEY: ${READFLOW_HASH_SECRET_KEY:-736563726574} + IMGPROXY_SALT: ${READFLOW_HASH_SECRET_SALT:-706570706572} ports: - - "${IMAGOR_PORT:-8081}:8000" + - "${IMAGOR_PORT:-8081}:8080" ####################################### # Goenberg: PDF generator ####################################### diff --git a/docker-compose.yml b/docker-compose.yml index 7f1224dde..f9a0dbd34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,13 +15,13 @@ services: - db-data:/var/lib/postgresql/data ####################################### - # Imagor: Image proxy + # Imgproxy: Image proxy ####################################### - imagor: - image: shumc/imagor - command: -imagor-auto-webp + imgproxy: + image: darthsim/imgproxy environment: - IMAGOR_SECRET: ${READFLOW_HASH_SECRET_SALT:-pepper} + IMGPROXY_KEY: ${READFLOW_HASH_SECRET_KEY:-736563726574} + IMGPROXY_SALT: ${READFLOW_HASH_SECRET_SALT:-706570706572} logging: driver: "json-file" options: diff --git a/pkg/config/config.go b/pkg/config/config.go index 7e1d49874..270a6703c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -41,13 +41,18 @@ func NewConfig() *Config { PublicURL: "http://localhost:8080", }, Hash: HashConfig{ - SecretSalt: "pepper", + SecretKey: hex_string{ + Value: []byte("secret"), + }, + SecretSalt: hex_string{ + Value: []byte("pepper"), + }, }, Avatar: AvatarConfig{ ServiceProvider: "https://robohash.org/{seed}?set=set4&size=48x48", }, Image: ImageConfig{ - ProxySizes: "320x200,768x576", + ProxySizes: "320,768", }, RateLimiting: RateLimitingConfig{ Notification: RateLimiting{ diff --git a/pkg/config/defaults.toml b/pkg/config/defaults.toml index 9dab2362b..ab21b65bd 100644 --- a/pkg/config/defaults.toml +++ b/pkg/config/defaults.toml @@ -81,8 +81,11 @@ directory = "${READFLOW_UI_DIRECTORY}" public_url = "${READFLOW_UI_PUBLIC_URL}" [hash] -## Secret salt used by hash algorythms -# Default: "pepper" +## Secret key used by hash algorythms (hex-encoded) +# Default: "736563726574" (aka "secret") +secret_key = "${READFLOW_HASH_SECRET_KEY}" +## Secret salt used by hash algorythms (hex-encoded) +# Default: "706570706572" (aka "pepper") secret_salt = "${READFLOW_HASH_SECRET_SALT}" [scraping] @@ -111,7 +114,7 @@ service_provider = "${READFLOW_AVATAR_SERVICE_PROVIDER}" proxy_url = "${READFLOW_IMAGE_PROXY_URL}" ## Image proxy supported sizes # Comma separated list of image size -# Default: "320x200,768x576" +# Default: "320,768" sizes = "${READFLOW_IMAGE_PROXY_SIZES}" [pdf] diff --git a/pkg/config/test/config_test.go b/pkg/config/test/config_test.go index f73136fbb..7f54db2d2 100644 --- a/pkg/config/test/config_test.go +++ b/pkg/config/test/config_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/ncarlier/readflow/pkg/config" ) @@ -18,11 +19,11 @@ func TestDefaultConfig(t *testing.T) { func TestLaodConfigFromFile(t *testing.T) { conf := config.NewConfig() err := conf.LoadFile("test.toml") - assert.Nil(t, err) + require.Nil(t, err) // Default overide assert.Equal(t, "localhost:8081", conf.HTTP.ListenAddr) // Env variable substitution - assert.NotEqual(t, "${USER}", conf.Hash.SecretSalt) + assert.Equal(t, []byte("test"), conf.Hash.SecretSalt.Value) // Default if empty assert.Equal(t, "http://localhost:8080", conf.HTTP.PublicURL) // Sub attribute diff --git a/pkg/config/test/test.toml b/pkg/config/test/test.toml index c3033bdd4..6b7eeb28a 100644 --- a/pkg/config/test/test.toml +++ b/pkg/config/test/test.toml @@ -4,8 +4,8 @@ listen_addr = "localhost:8081" public_url = "${AN_EMPTY_VARIABLE}" -[hsah] -secret_salt = "${USER}" +[hash] +secret_salt = "74657374" [integration.sentry] dsn_url = "https://1..9:1..9@sentry.io/1..9" diff --git a/pkg/config/types.go b/pkg/config/types.go index 8a4722812..9d7e7f939 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -81,7 +81,8 @@ type UIConfig struct { // HashConfig for hash configuration section type HashConfig struct { - SecretSalt string `toml:"secret_salt"` + SecretKey hex_string `toml:"secret_key"` + SecretSalt hex_string `toml:"secret_salt"` } // ScrapingConfig for scraping configuration section diff --git a/pkg/config/unmarshal.go b/pkg/config/unmarshal.go index aa8b45bef..e7c79a80a 100644 --- a/pkg/config/unmarshal.go +++ b/pkg/config/unmarshal.go @@ -1,6 +1,9 @@ package config -import "time" +import ( + "encoding/hex" + "time" +) type duration struct { time.Duration @@ -11,3 +14,13 @@ func (d *duration) UnmarshalText(text []byte) error { d.Duration, err = time.ParseDuration(string(text)) return err } + +type hex_string struct { + Value []byte +} + +func (hs *hex_string) UnmarshalText(text []byte) error { + var err error + hs.Value, err = hex.DecodeString(string(text)) + return err +} diff --git a/pkg/helper/hashid.go b/pkg/helper/hashid.go index 54f1e8687..4f0bac2f7 100644 --- a/pkg/helper/hashid.go +++ b/pkg/helper/hashid.go @@ -10,9 +10,9 @@ type HashIDHandler struct { } // NewHashIDHandler creates hashid handler -func NewHashIDHandler(salt string) (*HashIDHandler, error) { +func NewHashIDHandler(salt []byte) (*HashIDHandler, error) { hd := hashids.NewData() - hd.Salt = salt + hd.Salt = string(salt) provider, err := hashids.NewWithData(hd) if err != nil { return nil, err diff --git a/pkg/schema/article/queries.go b/pkg/schema/article/queries.go index 8fdd35927..97a4a2eac 100644 --- a/pkg/schema/article/queries.go +++ b/pkg/schema/article/queries.go @@ -93,6 +93,9 @@ func thumbnailsResolver(p graphql.ResolveParams) (interface{}, error) { if !ok { return nil, errors.New("thumbnails resolver is expecting an article") } + if article.Image == nil || *article.Image == "" { + return nil, nil + } if service.Lookup().GetConfig().Image.ProxyURL == "" { return nil, nil } diff --git a/pkg/service/articles.go b/pkg/service/articles.go index 82a3d1258..897658801 100644 --- a/pkg/service/articles.go +++ b/pkg/service/articles.go @@ -2,11 +2,8 @@ package service import ( "context" - "crypto/sha1" "errors" - "strings" - "github.com/ncarlier/readflow/pkg/helper" "github.com/ncarlier/readflow/pkg/model" ) @@ -95,14 +92,3 @@ func (reg *Registry) CleanHistory(ctx context.Context) (int64, error) { return nb, nil } - -// GetArticleThumbnail return article thumbnail URL -func (reg *Registry) GetArticleThumbnailHash(article *model.Article, size string) string { - if article.Image == nil || *article.Image == "" { - return "" - } - size += "/top" - path := size + "/" + strings.TrimPrefix(strings.TrimPrefix(*article.Image, "https://"), "http://") - hash := helper.Sign(sha1.New, path, reg.conf.Hash.SecretSalt, 0) - return hash + "/" + size -} diff --git a/pkg/service/articles_thumbnail.go b/pkg/service/articles_thumbnail.go new file mode 100644 index 000000000..1ba0dd1b0 --- /dev/null +++ b/pkg/service/articles_thumbnail.go @@ -0,0 +1,22 @@ +package service + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + + "github.com/ncarlier/readflow/pkg/model" +) + +// GetArticleThumbnail return article thumbnail URL +func (reg *Registry) GetArticleThumbnailHash(article *model.Article, size string) string { + if article.Image == nil || *article.Image == "" { + return "" + } + path := "/resize:fit:" + size + "/" + base64.StdEncoding.EncodeToString([]byte(*article.Image)) + + mac := hmac.New(sha256.New, reg.conf.Hash.SecretKey.Value) + mac.Write(reg.conf.Hash.SecretSalt.Value) + mac.Write([]byte(path)) + return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) +} diff --git a/pkg/service/registry.go b/pkg/service/registry.go index 9ae3aa653..dba363296 100644 --- a/pkg/service/registry.go +++ b/pkg/service/registry.go @@ -50,7 +50,7 @@ func Configure(conf config.Config, database db.DB, downloadCache cache.Cache) er if err != nil { return err } - hashid, err := helper.NewHashIDHandler(conf.Hash.SecretSalt) + hashid, err := helper.NewHashIDHandler(conf.Hash.SecretSalt.Value) if err != nil { return err } diff --git a/ui/src/articles/components/ArticleImage.tsx b/ui/src/articles/components/ArticleImage.tsx index 4292141fb..73203d21c 100644 --- a/ui/src/articles/components/ArticleImage.tsx +++ b/ui/src/articles/components/ArticleImage.tsx @@ -3,18 +3,16 @@ import React, { FC, ImgHTMLAttributes } from 'react' import { Article, ArticleThumbnail } from '../models' import { getAPIURL } from '../../helpers' -const imgResToWidth = (res: string) => res.split('x')[0] + 'w' -const getThumbnailURL = (thumbnail: ArticleThumbnail, src: string) => `${getAPIURL()}/img/${thumbnail.hash}/${src.replace(/^https?:\/\//, '')}` - +const getThumbnailURL = (thumbnail: ArticleThumbnail, src: string) => `${getAPIURL()}/img/${thumbnail.hash}/resize:fit:${thumbnail.size}/${btoa(src)}` const getThumbnailAttributes = ({thumbnails, image}: Article) => { const attrs :ImgHTMLAttributes = {} if (!thumbnails || thumbnails.length == 0) { return attrs } - const sizes = thumbnails.reverse().map(thumb => thumb.size.split('x')[0] + 'px') + const sizes = thumbnails.reverse().map(thumb => `${thumb.size}px`) attrs.sizes = `(max-width: ${sizes[0]}) ${sizes.join(', ')}` - attrs.srcSet = thumbnails?.map(thumb => `${getThumbnailURL(thumb, image)} ${imgResToWidth(thumb.size)}`).join(',') + attrs.srcSet = thumbnails?.map(thumb => `${getThumbnailURL(thumb, image)} ${thumb.size}w`).join(',') return attrs }