summaryrefslogtreecommitdiff
path: root/app/cache.go
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-07 18:34:59 -0500
committerChristian Cleberg <[email protected]>2026-08-07 18:45:03 -0500
commitee9fc6db38dbbee7418f8a3be2ce25eaa76f90ec (patch)
treee89f137e9814106ba54f7c5735773e9bfa2f3b60 /app/cache.go
parentddb8aa297962379fa7731032140221ba35f84b50 (diff)
downloadskunky-art-ee9fc6db38dbbee7418f8a3be2ce25eaa76f90ec.tar.gz
skunky-art-ee9fc6db38dbbee7418f8a3be2ce25eaa76f90ec.tar.bz2
skunky-art-ee9fc6db38dbbee7418f8a3be2ce25eaa76f90ec.zip
fix: add blur op for mature deviations with blur-constrained tokens
Mature deviations are signed with a watermark-service token whose obj carries a "blur": ">=N" constraint. The composed wixmp /v1/fit transform had no blur operation, so wixmp rejected it with 403, which the proxy passed through as a broken image. buildMediaURL now decodes the token and, when it demands a blur, appends a matching blur_N op to the transform (e.g. w_1280,h_1920,blur_10). Unconstrained media and tokens that don't parse are left untouched, so only media that requires it is affected. Closes #14
Diffstat (limited to 'app/cache.go')
-rwxr-xr-xapp/cache.go73
1 files changed, 73 insertions, 0 deletions
diff --git a/app/cache.go b/app/cache.go
index e4822db..6142178 100755
--- a/app/cache.go
+++ b/app/cache.go
@@ -4,11 +4,14 @@ package app
import (
"crypto/sha1" //nolint:gosec // G505: SHA-1 is a cache-key hash here, not a security primitive
+ "encoding/base64"
"encoding/hex"
+ "encoding/json"
"io"
"net/url"
"os"
"regexp"
+ "strconv"
"strings"
"sync"
"syscall"
@@ -88,6 +91,69 @@ func ageMemCache() {
// than escaped, because this label is what selects the host to fetch from.
var mediaSubdomain = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
+// blurConstraint reports the minimum blur radius a wixmp media token demands, or
+// 0 if it demands none.
+//
+// DeviantArt signs mature-content media with a watermark-service token whose obj
+// carries a "blur": ">=N" constraint. wixmp then rejects a plain /v1/fit
+// transform with 403 unless it includes a matching blur_N operation, so this is
+// what tells buildMediaURL when to add one. A token it cannot parse yields 0,
+// leaving the URL untouched — the same behaviour as before this check existed.
+func blurConstraint(token string) int {
+ // A JWT is header.payload.signature; the claims are the middle segment,
+ // base64url-encoded without padding.
+ parts := strings.SplitN(token, ".", 3)
+ if len(parts) < 2 {
+ return 0
+ }
+ payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+ if err != nil {
+ return 0
+ }
+
+ var claims struct {
+ Obj [][]struct {
+ Blur string `json:"blur"`
+ } `json:"obj"`
+ }
+ if json.Unmarshal(payload, &claims) != nil ||
+ len(claims.Obj) == 0 || len(claims.Obj[0]) == 0 {
+ return 0
+ }
+
+ // The constraint reads like ">=10"; take its digits as the radius, which is
+ // the minimum the token accepts.
+ n := 0
+ for _, c := range claims.Obj[0][0].Blur {
+ if c >= '0' && c <= '9' {
+ n = n*10 + int(c-'0')
+ }
+ }
+ return n
+}
+
+// addBlurToTransform inserts a blur_n operation into a wixmp /v1/fit transform,
+// turning e.g. w_1280,h_1920 into w_1280,h_1920,blur_n. It returns path
+// unchanged when it carries no /v1/fit transform (GIFs and oversized originals
+// are served without one) or already blurs.
+func addBlurToTransform(path string, n int) string {
+ const marker = "/v1/fit/"
+ start := strings.Index(path, marker)
+ if start < 0 {
+ return path
+ }
+ ops := start + len(marker)
+ end := strings.IndexByte(path[ops:], '/')
+ if end < 0 {
+ return path
+ }
+ end += ops
+ if strings.Contains(path[ops:end], "blur_") {
+ return path
+ }
+ return path[:end] + ",blur_" + strconv.Itoa(n) + path[end:]
+}
+
// buildMediaURL returns the wixmp CDN URL for one media item, reporting false
// when subdomain is not a bare hostname label.
//
@@ -102,6 +168,13 @@ func buildMediaURL(subdomain, path, token string) (string, bool) {
return "", false
}
+ // Mature media is signed with a token that only authorizes a blurred render;
+ // without a matching blur op in the transform wixmp answers 403. Add the op
+ // the token demands, and only then, so unconstrained media is left as-is.
+ if n := blurConstraint(token); n > 0 {
+ path = addBlurToTransform(path, n)
+ }
+
// Fields rather than concatenation: String escapes the path, so a decoded
// "#" or "?" in it stays part of the path instead of ending it. The host is
// checked above rather than escaped, because url.URL passes it through