Skip to content

Commit c235f85

Browse files
authored
πŸ› bug: Respect Authorization when caching responses (#3905)
1 parent 74de487 commit c235f85

File tree

4 files changed

+215
-6
lines changed

4 files changed

+215
-6
lines changed

β€Žmiddleware/cache/cache.goβ€Ž

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ func New(config ...Config) fiber.Handler {
124124

125125
// Return new handler
126126
return func(c fiber.Ctx) error {
127+
hasAuthorization := len(c.Request().Header.Peek(fiber.HeaderAuthorization)) > 0
128+
127129
// Refrain from caching
128130
if hasRequestDirective(c, noStore) {
129131
return c.Next()
@@ -178,6 +180,15 @@ func New(config ...Config) fiber.Handler {
178180
storedBytes -= size
179181
}
180182
} else if e.exp != 0 && !hasRequestDirective(c, noCache) {
183+
if hasAuthorization && !e.shareable {
184+
if cfg.Storage != nil {
185+
manager.release(e)
186+
}
187+
mux.Unlock()
188+
c.Set(cfg.CacheHeader, cacheUnreachable)
189+
return c.Next()
190+
}
191+
181192
// Separate body value to avoid msgp serialization
182193
// We can store raw bytes with Storage πŸ‘
183194
if cfg.Storage != nil {
@@ -232,8 +243,16 @@ func New(config ...Config) fiber.Handler {
232243
return err
233244
}
234245

246+
cacheControl := string(c.Response().Header.Peek(fiber.HeaderCacheControl))
247+
235248
// Respect server cache-control: no-store
236-
if strings.Contains(utils.ToLower(string(c.Response().Header.Peek(fiber.HeaderCacheControl))), noStore) {
249+
if strings.Contains(utils.ToLower(cacheControl), noStore) {
250+
c.Set(cfg.CacheHeader, cacheUnreachable)
251+
return nil
252+
}
253+
254+
isSharedCacheAllowed := allowsSharedCache(cacheControl)
255+
if hasAuthorization && !isSharedCacheAllowed {
237256
c.Set(cfg.CacheHeader, cacheUnreachable)
238257
return nil
239258
}
@@ -288,6 +307,7 @@ func New(config ...Config) fiber.Handler {
288307
c.Response().Header.Set(fiber.HeaderAge, "0")
289308
}
290309
e.age = ageVal
310+
e.shareable = isSharedCacheAllowed
291311

292312
// Store all response headers
293313
// (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1)
@@ -415,3 +435,25 @@ func parseMaxAge(cc string) (time.Duration, bool) {
415435
}
416436
return 0, false
417437
}
438+
439+
func allowsSharedCache(cc string) bool {
440+
shareable := false
441+
442+
for part := range strings.SplitSeq(cc, ",") {
443+
part = strings.TrimSpace(utils.ToLower(part))
444+
switch {
445+
case part == "":
446+
continue
447+
case part == "private":
448+
return false
449+
case part == "public":
450+
shareable = true
451+
case strings.HasPrefix(part, "s-maxage="):
452+
shareable = true
453+
default:
454+
continue
455+
}
456+
}
457+
458+
return shareable
459+
}

β€Žmiddleware/cache/cache_test.goβ€Ž

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"net/http/httptest"
1414
"os"
1515
"strconv"
16+
"strings"
1617
"testing"
1718
"time"
1819

@@ -1599,6 +1600,145 @@ func Test_ParseMaxAge(t *testing.T) {
15991600
}
16001601
}
16011602

1603+
func Test_AllowsSharedCache(t *testing.T) {
1604+
t.Parallel()
1605+
1606+
tests := []struct {
1607+
directives string
1608+
expect bool
1609+
}{
1610+
{"public", true},
1611+
{"private", false},
1612+
{"s-maxage=60", true},
1613+
{"public, max-age=60", true},
1614+
{"public, must-revalidate", true},
1615+
{"max-age=60", false},
1616+
{"no-cache", false},
1617+
{"no-cache, s-maxage=60", true},
1618+
{"", false},
1619+
}
1620+
1621+
for _, tt := range tests {
1622+
t.Run(tt.directives, func(t *testing.T) {
1623+
t.Parallel()
1624+
1625+
got := allowsSharedCache(tt.directives)
1626+
require.Equal(t, tt.expect, got, "directives: %q", tt.directives)
1627+
})
1628+
}
1629+
1630+
t.Run("private overrules public", func(t *testing.T) {
1631+
t.Parallel()
1632+
1633+
got := allowsSharedCache(strings.ToUpper("private, public"))
1634+
require.False(t, got)
1635+
})
1636+
}
1637+
1638+
func TestCacheSkipsAuthorizationByDefault(t *testing.T) {
1639+
t.Parallel()
1640+
1641+
app := fiber.New()
1642+
app.Use(New())
1643+
1644+
var count int
1645+
app.Get("/", func(c fiber.Ctx) error {
1646+
count++
1647+
return c.SendString(strconv.Itoa(count))
1648+
})
1649+
1650+
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1651+
req.Header.Set(fiber.HeaderAuthorization, "Bearer token")
1652+
1653+
resp, err := app.Test(req)
1654+
require.NoError(t, err)
1655+
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
1656+
body, err := io.ReadAll(resp.Body)
1657+
require.NoError(t, err)
1658+
require.Equal(t, "1", string(body))
1659+
1660+
req = httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1661+
req.Header.Set(fiber.HeaderAuthorization, "Bearer token")
1662+
1663+
resp, err = app.Test(req)
1664+
require.NoError(t, err)
1665+
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
1666+
body, err = io.ReadAll(resp.Body)
1667+
require.NoError(t, err)
1668+
require.Equal(t, "2", string(body))
1669+
}
1670+
1671+
func TestCacheBypassesExistingEntryForAuthorization(t *testing.T) {
1672+
t.Parallel()
1673+
1674+
app := fiber.New()
1675+
app.Use(New())
1676+
1677+
var count int
1678+
app.Get("/", func(c fiber.Ctx) error {
1679+
count++
1680+
return c.SendString(strconv.Itoa(count))
1681+
})
1682+
1683+
nonAuthReq := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1684+
1685+
resp, err := app.Test(nonAuthReq)
1686+
require.NoError(t, err)
1687+
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
1688+
body, err := io.ReadAll(resp.Body)
1689+
require.NoError(t, err)
1690+
require.Equal(t, "1", string(body))
1691+
1692+
authReq := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1693+
authReq.Header.Set(fiber.HeaderAuthorization, "Bearer token")
1694+
1695+
resp, err = app.Test(authReq)
1696+
require.NoError(t, err)
1697+
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
1698+
body, err = io.ReadAll(resp.Body)
1699+
require.NoError(t, err)
1700+
require.Equal(t, "2", string(body))
1701+
1702+
resp, err = app.Test(nonAuthReq)
1703+
require.NoError(t, err)
1704+
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
1705+
body, err = io.ReadAll(resp.Body)
1706+
require.NoError(t, err)
1707+
require.Equal(t, "1", string(body))
1708+
}
1709+
1710+
func TestCacheAllowsSharedCacheWithAuthorization(t *testing.T) {
1711+
t.Parallel()
1712+
1713+
app := fiber.New()
1714+
app.Use(New(Config{Expiration: 10 * time.Second}))
1715+
1716+
var count int
1717+
app.Get("/", func(c fiber.Ctx) error {
1718+
count++
1719+
c.Set(fiber.HeaderCacheControl, "public, max-age=60")
1720+
return c.SendString("ok")
1721+
})
1722+
1723+
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1724+
req.Header.Set(fiber.HeaderAuthorization, "Bearer token")
1725+
1726+
resp, err := app.Test(req)
1727+
require.NoError(t, err)
1728+
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
1729+
1730+
req = httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
1731+
req.Header.Set(fiber.HeaderAuthorization, "Bearer token")
1732+
1733+
resp, err = app.Test(req)
1734+
require.NoError(t, err)
1735+
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
1736+
body, err := io.ReadAll(resp.Body)
1737+
require.NoError(t, err)
1738+
require.Equal(t, "ok", string(body))
1739+
require.Equal(t, 1, count)
1740+
}
1741+
16021742
// go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4
16031743
func Benchmark_Cache(b *testing.B) {
16041744
app := fiber.New()

β€Žmiddleware/cache/manager.goβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type item struct {
2323
age uint64
2424
exp uint64
2525
ttl uint64
26+
shareable bool
2627
// used for finding the item in an indexed heap
2728
heapidx int
2829
}
@@ -77,6 +78,7 @@ func (m *manager) release(e *item) {
7778
e.exp = 0
7879
e.ttl = 0
7980
e.headers = nil
81+
e.shareable = false
8082
m.pool.Put(e)
8183
}
8284

β€Žmiddleware/cache/manager_msgp.goβ€Ž

Lines changed: 30 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
Β (0)