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
+}