From 5f11bcd6a23c0eefcf05294d7de80fe09d59bf4b Mon Sep 17 00:00:00 2001 From: httpjamesm Date: Thu, 2 Feb 2023 19:05:08 -0500 Subject: [PATCH] feat!: proxy images with JWT auth --- env/checks.go | 1 + go.mod | 1 + go.sum | 2 ++ main.go | 2 ++ src/routes/image.go | 77 +++++++++++++++++++++++++++++++++++++++++ src/routes/question.go | 4 +-- src/types/imageProxy.go | 14 ++++++++ src/utils/images.go | 52 ++++++++++++++++++++++++++++ 8 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/routes/image.go create mode 100644 src/types/imageProxy.go create mode 100644 src/utils/images.go diff --git a/env/checks.go b/env/checks.go index 74b583f..5408d94 100644 --- a/env/checks.go +++ b/env/checks.go @@ -10,6 +10,7 @@ import ( func RunChecks() { godotenv.Load(".env") checkEnv("APP_URL") + checkEnv("JWT_SIGNING_SECRET") } func checkEnv(key string) { diff --git a/go.mod b/go.mod index 1a2e400..8a43e4e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e210ac8..6dc90ff 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index bb155a0..a0a7682 100644 --- a/main.go +++ b/main.go @@ -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)) } diff --git a/src/routes/image.go b/src/routes/image.go new file mode 100644 index 0000000..fe42a82 --- /dev/null +++ b/src/routes/image.go @@ -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()) +} diff --git a/src/routes/question.go b/src/routes/question.go index e07331f..36d8fdf 100644 --- a/src/routes/question.go +++ b/src/routes/question.go @@ -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) }) diff --git a/src/types/imageProxy.go b/src/types/imageProxy.go new file mode 100644 index 0000000..2d9bdc4 --- /dev/null +++ b/src/types/imageProxy.go @@ -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 +} diff --git a/src/utils/images.go b/src/utils/images.go new file mode 100644 index 0000000..b8c84a0 --- /dev/null +++ b/src/utils/images.go @@ -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(`]*\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(``, 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 +}