feat: implement image HMAC generator and integrate into image handling and RSS generation

This commit is contained in:
2026-03-13 23:34:59 +03:30
parent e210acb810
commit 940cfa8626
9 changed files with 131 additions and 24 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
.env
.vscode/launch.json
main
data/
data/
*.tar.xz
buildcmd
TGSS
+2
View File
@@ -7,6 +7,7 @@ import (
"tgss/internal/rss"
"tgss/internal/server"
"tgss/internal/telegram"
"tgss/internal/utils"
"go.uber.org/fx"
)
@@ -18,6 +19,7 @@ func main() {
config.Load,
server.NewGin,
server.NewRateLimiter,
utils.NewImageHMACGenerator,
handlers.NewChannelHandler,
handlers.NewAuthHandler,
+19 -5
View File
@@ -16,11 +16,13 @@ type Config struct {
TgAppId int
TgAppHash string
SessionPath string
ProxyURL string
AppHost string
AppPort int
BaseURL string
SessionPath string
ProxyURL string
AppHost string
AppPort int
BaseURL string
ImageSigSecret string
AppEnv string
}
func Load(logger *zap.Logger) *Config {
@@ -83,6 +85,18 @@ func Load(logger *zap.Logger) *Config {
baseURL = baseURL[:len(baseURL)-1]
}
imageSigSecret := os.Getenv("IMAGE_SIGNATURE_SECRET")
if imageSigSecret == "" {
logger.Warn("IMAGE_SIGNATURE_SECRET is not set, which leaves a backlink vulnerability on images")
imageSigSecret = "notAsafeSecreT"
}
appEnv := os.Getenv("APP_ENV")
if appEnv == "" {
appEnv = "development"
}
// TODO: logging and gin
return &Config{
TgAppId: func() int { i, _ := strconv.Atoi(appId); return i }(),
TgAppHash: strings.TrimSpace(appHash),
+24 -4
View File
@@ -7,6 +7,7 @@ import (
"strconv"
"tgss/internal/config"
"tgss/internal/telegram"
"tgss/internal/utils"
"time"
"github.com/gin-gonic/gin"
@@ -14,17 +15,20 @@ import (
)
type ImageHandler struct {
logger *zap.Logger
imageService *telegram.ImageService
logger *zap.Logger
imageService *telegram.ImageService
hmacGenerator *utils.ImageHMACGenerator
}
func NewImageHandler(
imageService *telegram.ImageService,
logger *zap.Logger,
hmacGenerator *utils.ImageHMACGenerator,
) *ImageHandler {
return &ImageHandler{
logger: logger,
imageService: imageService,
logger: logger,
imageService: imageService,
hmacGenerator: hmacGenerator,
}
}
@@ -46,6 +50,22 @@ func (ih *ImageHandler) GetImage(c *gin.Context) {
return
}
expStr := c.Query("exp")
sig := c.Query("sig")
expUnix, err := strconv.ParseInt(expStr, 10, 64)
if err != nil {
c.AbortWithStatus(400)
return
}
expiresAt := time.Unix(expUnix, 0)
if !ih.hmacGenerator.VerifyMAC(msgId, expiresAt, sig) {
c.AbortWithStatus(403)
return
}
reader, err := ih.imageService.GetChannelsMessageImageById(ctx, channelId, msgId)
if err != nil {
ih.logger.Error("failed to get image", zap.Error(err))
+13 -2
View File
@@ -1,8 +1,15 @@
package infra
import "go.uber.org/zap"
import (
"os"
"go.uber.org/zap"
)
func NewLogger() (*zap.Logger, error) {
if os.Getenv("APP_ENV") == "production" {
return zap.NewProduction()
}
return zap.NewDevelopment()
}
@@ -12,10 +19,14 @@ type FXLogger struct {
}
func NewFXLogger() *FXLogger {
if os.Getenv("APP_ENV") == "production" {
logger, _ := zap.NewProduction()
return &FXLogger{logger: logger}
}
logger, _ := zap.NewDevelopment()
return &FXLogger{logger: logger}
}
func (l *FXLogger) Printf(str string, args ...any) {
l.logger.Sugar().Infof(str, args...)
}
}
+21 -8
View File
@@ -7,6 +7,7 @@ import (
"net/url"
"strconv"
"tgss/internal/config"
"tgss/internal/utils"
"time"
"github.com/gotd/td/tg"
@@ -18,13 +19,17 @@ type RSSItem struct {
Link string `xml:"link"`
Enclosure *RSSEnclosure `xml:"enclosure,omitempty"`
PubDate string `xml:"pubDate,omitempty"`
Description string `xml:"description"`
Description CDATA `xml:"description"`
Guid string `xml:"guid,omitempty"`
}
type CDATA struct {
Text string `xml:",cdata"`
}
type RSSEnclosure struct {
URL string `xml:"url,attr"`
Length string `xml:"lenghth,omitempty,attr"`
Length string `xml:"length,omitempty,attr"`
Type string `xml:"type,attr"`
}
@@ -48,12 +53,13 @@ type RSSFeed struct {
}
type RSSGenerator struct {
logger *zap.Logger
config *config.Config
logger *zap.Logger
config *config.Config
hmacGenerator *utils.ImageHMACGenerator
}
func NewRSSGenerator(logger *zap.Logger, config *config.Config) *RSSGenerator {
return &RSSGenerator{logger: logger, config: config}
func NewRSSGenerator(logger *zap.Logger, config *config.Config, hmacGenerator *utils.ImageHMACGenerator) *RSSGenerator {
return &RSSGenerator{logger: logger, config: config, hmacGenerator: hmacGenerator}
}
func (r *RSSGenerator) GenerateFeed(items []tg.MessageClass, channelId string) *RSSFeed {
@@ -130,12 +136,19 @@ func (r *RSSGenerator) messageToItem(msg tg.MessageClass, channelId string) (*RS
Title: "Post by @" + channelId + " on Telegram",
Link: messageURL.String(),
PubDate: time.Unix(int64(message.Date), 0).Format("Mon, 02 Jan 2006 15:04 MST"),
Description: description,
Description: CDATA{Text: description},
Guid: messageURL.String(),
}
if err = r.messageHasPhoto(message); err == nil {
if enclosureURL, err := url.ParseRequestURI(r.config.BaseURL + "/image/" + channelId + "/" + strconv.Itoa(msg.GetID())); err == nil {
enclosurePhotoExpiresAt := time.Now().Add(10 * time.Minute)
enclosurePhotoSignature := r.hmacGenerator.GenerateMAC(message.GetID(), enclosurePhotoExpiresAt)
urlParams := fmt.Sprintf(
"?exp=%d&sig=%s",
enclosurePhotoExpiresAt.Unix(),
enclosurePhotoSignature,
)
if enclosureURL, err := url.ParseRequestURI(r.config.BaseURL + "/image/" + channelId + "/" + strconv.Itoa(msg.GetID()) + urlParams); err == nil {
rssItem.Enclosure = &RSSEnclosure{
URL: enclosureURL.String(),
Length: "0",
+7 -3
View File
@@ -28,7 +28,7 @@ type RouterParams struct {
ImageHandler *handlers.ImageHandler
}
func NewGin() *gin.Engine {
func NewGin(config *config.Config) *gin.Engine {
g := gin.New()
g.Use(gin.Recovery())
g.Use(cors.New(cors.Config{
@@ -36,6 +36,11 @@ func NewGin() *gin.Engine {
AllowMethods: []string{"GET"},
AllowHeaders: []string{"Origin", "Content-Type"},
}))
if config.AppEnv == "production" {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.DebugMode)
}
return g
}
@@ -48,13 +53,12 @@ func RegisterRoutes(params RouterParams) {
})
})
params.GinEngine.GET("/feed/:id/json", params.ChannelHandler.GetMessagesJson)
// params.GinEngine.GET("/feed/:id/json", params.ChannelHandler.GetMessagesJson)
params.GinEngine.GET("/feed/:id", params.RateLimiter.FeedRateLimit(), params.ChannelHandler.GetMessagesRSS)
params.GinEngine.GET("/image/:channelId/:messageId", params.RateLimiter.ImageRateLimit(), params.ImageHandler.GetImage)
// TODO: improve accurecy in rss channel fields
// TODO: add ui with templates under /setup with fetch
// TODO: image endpoint + HMAC
authStatCtx, cancel := context.WithTimeout(context.Background(), config.AuthStatusTimeout)
authStat, err := params.TgService.AuthStatus(authStatCtx)
cancel()
-1
View File
@@ -11,7 +11,6 @@ import (
"golang.org/x/time/rate"
)
// TODO: another limiter rate for images
type ClientLimiter struct {
feedLimiter *rate.Limiter
imageLimiter *rate.Limiter
+41
View File
@@ -0,0 +1,41 @@
package utils
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"tgss/internal/config"
"time"
"go.uber.org/zap"
)
type ImageHMACGenerator struct {
config *config.Config
logger *zap.Logger
}
func NewImageHMACGenerator(logger *zap.Logger, config *config.Config) *ImageHMACGenerator {
return &ImageHMACGenerator{logger: logger, config: config}
}
func (h *ImageHMACGenerator) GenerateMAC(messageId int, expiresAt time.Time) string {
mac := hmac.New(sha256.New, []byte(h.config.ImageSigSecret))
msg := strconv.Itoa(messageId) + ":" + strconv.FormatInt(expiresAt.Unix(), 10)
mac.Write([]byte(msg))
return hex.EncodeToString(mac.Sum(nil))
}
func (h *ImageHMACGenerator) VerifyMAC(messageID int, expiresAt time.Time, sig string) bool {
if time.Now().After(expiresAt) {
return false
}
expected := h.GenerateMAC(messageID, expiresAt)
return hmac.Equal([]byte(expected), []byte(sig))
}