mirror of
https://github.com/mmahdium/TGSS.git
synced 2026-08-12 08:32:48 +03:30
feat: implement image HMAC generator and integrate into image handling and RSS generation
This commit is contained in:
+4
-1
@@ -1,4 +1,7 @@
|
||||
.env
|
||||
.vscode/launch.json
|
||||
main
|
||||
data/
|
||||
data/
|
||||
*.tar.xz
|
||||
buildcmd
|
||||
TGSS
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user