feat!: proxy images with JWT auth

This commit is contained in:
httpjamesm 2023-02-02 19:05:08 -05:00
parent 7494b2df33
commit 5f11bcd6a2
No known key found for this signature in database
8 changed files with 151 additions and 2 deletions

1
env/checks.go vendored
View File

@ -10,6 +10,7 @@ import (
func RunChecks() {
godotenv.Load(".env")
checkEnv("APP_URL")
checkEnv("JWT_SIGNING_SECRET")
}
func checkEnv(key string) {

1
go.mod
View File

@ -17,6 +17,7 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect

2
go.sum
View File

@ -27,6 +27,8 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPr
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=

View File

@ -52,5 +52,7 @@ func main() {
r.GET("/questions/:id/:title", routes.ViewQuestion)
r.GET("/proxy", routes.GetImage)
r.Run(fmt.Sprintf("%s:%s", host, port))
}

77
src/routes/image.go Normal file
View File

@ -0,0 +1,77 @@
package routes
import (
"anonymousoverflow/src/types"
"fmt"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2"
"github.com/golang-jwt/jwt/v4"
)
func GetImage(c *gin.Context) {
authorization := c.Query("auth")
if authorization == "" {
c.String(400, "Missing auth token")
return
}
url := c.Query("url")
if url == "" {
c.String(400, "Missing url")
return
}
// validate the auth token
token, err := jwt.ParseWithClaims(authorization, &types.ImageProxyClaims{}, func(token *jwt.Token) (interface{}, error) {
// validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SIGNING_SECRET")), nil
})
if err != nil {
c.String(400, err.Error())
return
}
claims, ok := token.Claims.(*types.ImageProxyClaims)
if !ok || !token.Valid {
c.String(400, "Invalid token")
return
}
if claims.Action != "imageProxy" {
c.String(400, "Invalid action")
return
}
if claims.ImageURL != url {
c.String(400, "Request & token mismatch")
return
}
if claims.Exp < time.Now().Unix() {
c.String(400, "Token expired")
return
}
// download the image
client := resty.New()
resp, err := client.R().Get(url)
if err != nil {
c.AbortWithStatus(500)
return
}
// set the content type
c.Header("Content-Type", resp.Header().Get("Content-Type"))
// write the image to the response
c.Writer.Write(resp.Body())
}

View File

@ -93,7 +93,7 @@ func ViewQuestion(c *gin.Context) {
return
}
newFilteredQuestion.Body = template.HTML(questionBodyParentHTML)
newFilteredQuestion.Body = template.HTML(utils.ReplaceImgTags(questionBodyParentHTML))
questionBodyText := questionBodyParent.Text()
@ -234,7 +234,7 @@ func ViewQuestion(c *gin.Context) {
comments = utils.FindAndReturnComments(answerBodyHTML, postLayout)
newFilteredAnswer.Comments = comments
newFilteredAnswer.Body = template.HTML(answerBodyHTML)
newFilteredAnswer.Body = template.HTML(utils.ReplaceImgTags(answerBodyHTML))
answers = append(answers, newFilteredAnswer)
})

14
src/types/imageProxy.go Normal file
View File

@ -0,0 +1,14 @@
package types
import "github.com/golang-jwt/jwt/v4"
type ImageProxyClaims struct {
Action string `json:"action"`
ImageURL string `json:"image_url"`
Iss int64 `json:"iss"`
Exp int64 `json:"exp"`
jwt.RegisteredClaims
}

52
src/utils/images.go Normal file
View File

@ -0,0 +1,52 @@
package utils
import (
"anonymousoverflow/src/types"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
)
var imgTagRegex = regexp.MustCompile(`<img[^>]*\s+src\s*=\s*"(.*?)"[^>]*>`)
func ReplaceImgTags(inHtml string) string {
// find all img tags
imgTags := imgTagRegex.FindAllString(inHtml, -1)
for _, imgTag := range imgTags {
// parse the src="" attribute
srcRegex := regexp.MustCompile(`src\s*=\s*"(.*?)"`)
src := srcRegex.FindStringSubmatch(imgTag)[1]
authToken, _ := generateImageProxyAuth(src)
// replace the img tag with a proxied url
inHtml = strings.Replace(inHtml, imgTag, fmt.Sprintf(`<img src="%s/proxy?url=%s&auth=%s">`, os.Getenv("APP_URL"), src, authToken), 1)
}
return inHtml
}
func generateImageProxyAuth(url string) (string, error) {
// generate a jwt with types.ImageProxyClaims
claims := types.ImageProxyClaims{
Action: "imageProxy",
ImageURL: url,
Iss: time.Now().Unix(),
Exp: time.Now().Add(time.Minute).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
// sign the token
ss, err := token.SignedString([]byte(os.Getenv("JWT_SIGNING_SECRET")))
if err != nil {
return "", err
}
return ss, nil
}