Skip to content

Commit

Permalink
feat(chore): add mode to bypass RFC (#348)
Browse files Browse the repository at this point in the history
* feat(chore): add mode to bypass RFC

* fix(e2e): update JSON configuration

* fix: souin. prefix on context keys and throw error on caddy directive error

* fix(plugins): retrieve caddy mode argument, support inmemory badger configuration option

* Update documentation
  • Loading branch information
darkweak authored Jul 17, 2023
1 parent b470be2 commit 6b17da1
Show file tree
Hide file tree
Showing 37 changed files with 1,629 additions and 420 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ default_cache:
- etcd-1:2379 # First node
- etcd-2:2379 # Second node
- etcd-3:2379 # Third node
mode: bypass # Override the RFC respect.
olric: # If distributed is set to true, you'll have to define either the etcd or olric section
url: 'olric:3320' # Olric server
regex:
Expand Down Expand Up @@ -197,6 +198,7 @@ surrogate_keys:
| `default_cache.key.disable_query` | Disable the query string part in the key | `true`<br/><br/>`(default: false)` |
| `default_cache.key.headers` | Add headers to the key matching the regexp | `- Authorization`<br/><br/>`- Content-Type`<br/><br/>`- X-Additional-Header` |
| `default_cache.key.hide` | Prevent the key from being exposed in the `Cache-Status` HTTP response header | `true`<br/><br/>`(default: false)` |
| `default_cache.mode` | RFC respect tweaking | One of `bypass` `bypass_request` `bypass_response` `strict` (default `strict`) |
| `default_cache.nuts` | Configure the Nuts cache storage | |
| `default_cache.nuts.path` | Set the Nuts file path storage | `/anywhere/nuts/storage` |
| `default_cache.nuts.configuration` | Configure Nuts directly in the Caddyfile or your JSON caddy configuration | [See the Nuts configuration for the options](https://github.com/nutsdb/nutsdb#default-options) |
Expand Down
7 changes: 7 additions & 0 deletions configurationtypes/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ type DefaultCache struct {
Headers []string `json:"headers" yaml:"headers"`
Key Key `json:"key" yaml:"key"`
Etcd CacheProvider `json:"etcd" yaml:"etcd"`
Mode string `json:"mode" yaml:"mode"`
Nuts CacheProvider `json:"nuts" yaml:"nuts"`
Olric CacheProvider `json:"olric" yaml:"olric"`
Redis CacheProvider `json:"redis" yaml:"redis"`
Expand Down Expand Up @@ -273,6 +274,11 @@ func (d *DefaultCache) GetEtcd() CacheProvider {
return d.Etcd
}

// GetMode returns mode configuration
func (d *DefaultCache) GetMode() string {
return d.Mode
}

// GetNuts returns nuts configuration
func (d *DefaultCache) GetNuts() CacheProvider {
return d.Nuts
Expand Down Expand Up @@ -321,6 +327,7 @@ type DefaultCacheInterface interface {
GetCDN() CDN
GetDistributed() bool
GetEtcd() CacheProvider
GetMode() string
GetNuts() CacheProvider
GetOlric() CacheProvider
GetRedis() CacheProvider
Expand Down
4 changes: 2 additions & 2 deletions context/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
)

const (
CacheName ctxKey = "CACHE_NAME"
RequestCacheControl ctxKey = "REQUEST_CACHE_CONTROL"
CacheName ctxKey = "souin_ctx.CACHE_NAME"
RequestCacheControl ctxKey = "souin_ctx.REQUEST_CACHE_CONTROL"
)

var defaultCacheName string = "Souin"
Expand Down
6 changes: 3 additions & 3 deletions context/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
)

const (
GraphQL ctxKey = "GRAPHQL"
HashBody ctxKey = "HASH_BODY"
IsMutationRequest ctxKey = "IS_MUTATION_REQUEST"
GraphQL ctxKey = "souin_ctx.GRAPHQL"
HashBody ctxKey = "souin_ctx.HASH_BODY"
IsMutationRequest ctxKey = "souin_ctx.IS_MUTATION_REQUEST"
)

type graphQLContext struct {
Expand Down
6 changes: 3 additions & 3 deletions context/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
)

const (
Key ctxKey = "CACHE_KEY"
DisplayableKey ctxKey = "DISPLAYABLE_KEY"
IgnoredHeaders ctxKey = "IGNORE_HEADERS"
Key ctxKey = "souin_ctx.CACHE_KEY"
DisplayableKey ctxKey = "souin_ctx.DISPLAYABLE_KEY"
IgnoredHeaders ctxKey = "souin_ctx.IGNORE_HEADERS"
)

type keyContext struct {
Expand Down
2 changes: 1 addition & 1 deletion context/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/darkweak/souin/configurationtypes"
)

const SupportedMethod ctxKey = "SUPPORTED_METHOD"
const SupportedMethod ctxKey = "souin_ctx.SUPPORTED_METHOD"

var defaultVerbs []string = []string{http.MethodGet, http.MethodHead}

Expand Down
28 changes: 28 additions & 0 deletions context/mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package context

import (
"context"
"net/http"

"github.com/darkweak/souin/configurationtypes"
)

const Mode ctxKey = "souin_ctx.MODE"

type ModeContext struct {
Strict, Bypass_request, Bypass_response bool
}

func (mc *ModeContext) SetupContext(c configurationtypes.AbstractConfigurationInterface) {
mode := c.GetDefaultCache().GetMode()
mc.Bypass_request = mode == "bypass" || mode == "bypass_request"
mc.Bypass_response = mode == "bypass" || mode == "bypass_response"
mc.Strict = !mc.Bypass_request && !mc.Bypass_response
c.GetLogger().Sugar().Debugf("The cache logic will run as %s: %+v", mode, mc)
}

func (mc *ModeContext) SetContext(req *http.Request) *http.Request {
return req.WithContext(context.WithValue(req.Context(), Mode, mc))
}

var _ ctx = (*cacheContext)(nil)
58 changes: 58 additions & 0 deletions context/mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package context

import (
"testing"

"github.com/darkweak/souin/configurationtypes"
"github.com/darkweak/souin/plugins/souin/configuration"
"go.uber.org/zap"
)

func Test_ModeContext_SetupContext(t *testing.T) {
dc := configurationtypes.DefaultCache{}
c := configuration.Configuration{
DefaultCache: &dc,
}
c.SetLogger(zap.NewNop())
ctx := ModeContext{}

ctx.SetupContext(&c)
if ctx.Bypass_request || ctx.Bypass_response || !ctx.Strict {
t.Error("The context must be strict and must not bypass either response or request.")
}

c.DefaultCache.Mode = "bypass"
ctx = ModeContext{}
ctx.SetupContext(&c)
if !ctx.Bypass_request || !ctx.Bypass_response || ctx.Strict {
t.Error("The context must bypass either response and request.")
}

c.DefaultCache.Mode = "bypass_request"
ctx = ModeContext{}
ctx.SetupContext(&c)
if !ctx.Bypass_request || ctx.Bypass_response || ctx.Strict {
t.Error("The context must bypass request only.")
}

c.DefaultCache.Mode = "bypass_response"
ctx = ModeContext{}
ctx.SetupContext(&c)
if ctx.Bypass_request || !ctx.Bypass_response || ctx.Strict {
t.Error("The context must bypass response only.")
}

c.DefaultCache.Mode = "strict"
ctx = ModeContext{}
ctx.SetupContext(&c)
if ctx.Bypass_request || ctx.Bypass_response || !ctx.Strict {
t.Error("The context must be strict.")
}

c.DefaultCache.Mode = "default_value"
ctx = ModeContext{}
ctx.SetupContext(&c)
if ctx.Bypass_request || ctx.Bypass_response || !ctx.Strict {
t.Error("The context must be strict.")
}
}
2 changes: 1 addition & 1 deletion context/now.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/darkweak/souin/configurationtypes"
)

const Now ctxKey = "NOW"
const Now ctxKey = "souin_ctx.NOW"

type nowContext struct{}

Expand Down
4 changes: 2 additions & 2 deletions context/timeout.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
)

const (
TimeoutCache ctxKey = "TIMEOUT_CACHE"
TimeoutCancel ctxKey = "TIMEOUT_CANCEL"
TimeoutCache ctxKey = "souin_ctx.TIMEOUT_CACHE"
TimeoutCancel ctxKey = "souin_ctx.TIMEOUT_CANCEL"
)

const (
Expand Down
7 changes: 5 additions & 2 deletions context/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ type (
GraphQL ctx
Key ctx
Method ctx
Mode ctx
Now ctx
Timeout ctx
}
)

const CacheControlCtx ctxKey = "CACHE-CONTROL-CTX"
const CacheControlCtx ctxKey = "souin_ctx.CACHE-CONTROL-CTX"

func GetContext() *Context {
return &Context{
CacheName: &cacheContext{},
GraphQL: &graphQLContext{},
Key: &keyContext{},
Method: &methodContext{},
Mode: &ModeContext{},
Now: &nowContext{},
Timeout: &timeoutContext{},
}
Expand All @@ -42,12 +44,13 @@ func (c *Context) Init(co configurationtypes.AbstractConfigurationInterface) {
c.GraphQL.SetupContext(co)
c.Key.SetupContext(co)
c.Method.SetupContext(co)
c.Mode.SetupContext(co)
c.Now.SetupContext(co)
c.Timeout.SetupContext(co)
}

func (c *Context) SetBaseContext(req *http.Request) *http.Request {
return c.Timeout.SetContext(c.Method.SetContext(c.CacheName.SetContext(c.Now.SetContext(req))))
return c.Mode.SetContext(c.Timeout.SetContext(c.Method.SetContext(c.CacheName.SetContext(c.Now.SetContext(req)))))
}

func (c *Context) SetContext(req *http.Request) *http.Request {
Expand Down
145 changes: 144 additions & 1 deletion docs/e2e/Souin E2E.postman_collection.json

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions pkg/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ func (s *SouinBaseHandler) Store(
customWriter.Headers.Set("Cache-Status", fmt.Sprintf("%s; fwd=uri-miss; key=%s; detail=INVALID-RESPONSE-CACHE-CONTROL", rq.Context().Value(context.CacheName), rfc.GetCacheKeyFromCtx(rq.Context())))
return nil
}
if (responseCc.PrivatePresent || rq.Header.Get("Authorization") != "") && !canBypassAuthorizationRestriction(customWriter.Header(), rq.Context().Value(context.IgnoredHeaders).([]string)) {

modeContext := rq.Context().Value(context.Mode).(*context.ModeContext)
if !modeContext.Bypass_request && (responseCc.PrivatePresent || rq.Header.Get("Authorization") != "") && !canBypassAuthorizationRestriction(customWriter.Header(), rq.Context().Value(context.IgnoredHeaders).([]string)) {
customWriter.Headers.Set("Cache-Status", fmt.Sprintf("%s; fwd=uri-miss; key=%s; detail=PRIVATE-OR-AUTHENTICATED-RESPONSE", rq.Context().Value(context.CacheName), rfc.GetCacheKeyFromCtx(rq.Context())))
return nil
}
Expand Down Expand Up @@ -207,7 +209,8 @@ func (s *SouinBaseHandler) Store(
}

status := fmt.Sprintf("%s; fwd=uri-miss", rq.Context().Value(context.CacheName))
if !requestCc.NoStore && !responseCc.NoStore {
if (modeContext.Bypass_request || !requestCc.NoStore) &&
(modeContext.Bypass_response || !responseCc.NoStore) {
headers := customWriter.Headers.Clone()
for hname, shouldDelete := range responseCc.NoCache {
if shouldDelete {
Expand Down Expand Up @@ -359,7 +362,8 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n

requestCc, coErr := cacheobject.ParseRequestCacheControl(rq.Header.Get("Cache-Control"))

if coErr != nil || requestCc == nil {
modeContext := rq.Context().Value(context.Mode).(*context.ModeContext)
if !modeContext.Bypass_request && (coErr != nil || requestCc == nil) {
rw.Header().Set("Cache-Status", cacheName+"; fwd=bypass; detail=CACHE-CONTROL-EXTRACTION-ERROR")

return next(rw, rq)
Expand All @@ -384,11 +388,11 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n
crw.mutex.Unlock()
}(rq, customWriter)
s.Configuration.GetLogger().Sugar().Debugf("Request cache-control %+v", requestCc)
if !requestCc.NoCache {
if modeContext.Bypass_request || !requestCc.NoCache {
validator := rfc.ParseRequest(rq)
response := s.Storer.Prefix(cachedKey, rq, validator)

if response != nil && rfc.ValidateCacheControl(response, requestCc) {
if response != nil && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) {
if validator.NeedRevalidation {
err := s.Revalidate(validator, next, customWriter, rq, requestCc, cachedKey)
_, _ = io.Copy(customWriter.Buf, response.Body)
Expand All @@ -397,7 +401,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n
return err
}
rfc.SetCacheStatusHeader(response)
if rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil {
if !modeContext.Strict || rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil {
customWriter.Headers = response.Header
customWriter.statusCode = response.StatusCode
s.Configuration.GetLogger().Sugar().Debugf("Serve from cache %+v", rq)
Expand All @@ -408,7 +412,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n
}
} else if response == nil && !requestCc.OnlyIfCached && (requestCc.MaxStaleSet || requestCc.MaxStale > -1) {
response = s.Storer.Prefix(storage.StalePrefix+cachedKey, rq, validator)
if nil != response && rfc.ValidateCacheControl(response, requestCc) {
if nil != response && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) {
addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader))
rfc.SetCacheStatusHeader(response)

Expand Down
12 changes: 10 additions & 2 deletions pkg/storage/badgerProvider.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,16 @@ func BadgerConnectionFactory(c t.AbstractConfigurationInterface) (Storer, error)
if err := mergo.Merge(&badgerOptions, parsedBadger, mergo.WithOverride); err != nil {
c.GetLogger().Sugar().Error("An error occurred during the badgerOptions merge from the default options with your configuration.")
}
if badgerOptions.Dir == "" {
badgerOptions.Dir = "souin_dir"
if badgerOptions.InMemory {
badgerOptions.Dir = ""
badgerOptions.ValueDir = ""
} else {
if badgerOptions.Dir == "" {
badgerOptions.Dir = "souin_dir"
}
if badgerOptions.ValueDir == "" {
badgerOptions.ValueDir = badgerOptions.Dir
}
}
} else if badgerConfiguration.Path == "" {
badgerOptions = badgerOptions.WithInMemory(true)
Expand Down
43 changes: 43 additions & 0 deletions plugins/caddy/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,49 @@ route /cache-authorization {
respond "Hello to the authenticated user."
}

route /bypass {
cache {
mode bypass
}

header Cache-Control "no-store"
respond "Hello bypass"
}

route /bypass_request {
cache {
mode bypass_request
}

respond "Hello bypass_request"
}

route /bypass_response {
cache {
mode bypass_response
}

header Cache-Control "no-cache, no-store"
respond "Hello bypass_response"
}

route /strict_request {
cache {
mode strict
}

respond "Hello strict"
}

route /strict_response {
cache {
mode strict
}

header Cache-Control "no-cache, no-store"
respond "Hello strict"
}

cache @souin-api {
}

Expand Down
2 changes: 2 additions & 0 deletions plugins/caddy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Here are all the available options for the global options
headers Content-Type Authorization
}
log_level debug
mode bypass
nuts {
path /path/to/the/storage
}
Expand Down Expand Up @@ -380,6 +381,7 @@ What does these directives mean?
| `key.disable_method` | Disable the method part in the key | `true`<br/><br/>`(default: false)` |
| `key.headers` | Add headers to the key matching the regexp | `Authorization Content-Type X-Additional-Header` |
| `key.hide` | Prevent the key from being exposed in the `Cache-Status` HTTP response header | `true`<br/><br/>`(default: false)` |
| `mode` | Bypass the RFC respect | One of `bypass` `bypass_request` `bypass_response` `strict` (default `strict`) |
| `nuts` | Configure the Nuts cache storage | |
| `nuts.path` | Set the Nuts file path storage | `/anywhere/nuts/storage` |
| `nuts.configuration` | Configure Nuts directly in the Caddyfile or your JSON caddy configuration | [See the Nuts configuration for the options](https://github.com/nutsdb/nutsdb#default-options) |
Expand Down
Loading

0 comments on commit 6b17da1

Please sign in to comment.