github.com/aacfactory/fns@v1.2.86-0.20240310083819-80d667fc0a17/transports/middlewares/cachecontrol/middleware.go (about) 1 /* 2 * Copyright 2023 Wang Min Xiang 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package cachecontrol 19 20 import ( 21 "bytes" 22 "github.com/aacfactory/errors" 23 "github.com/aacfactory/fns/commons/bytex" 24 "github.com/aacfactory/fns/transports" 25 "github.com/aacfactory/logs" 26 "github.com/cespare/xxhash/v2" 27 "github.com/valyala/bytebufferpool" 28 "net/http" 29 "strconv" 30 "time" 31 ) 32 33 var ( 34 nilBodyETag = []byte(strconv.FormatUint(xxhash.Sum64([]byte("nil")), 16)) 35 contextEnableKey = []byte("@fns:cache-control:enable") 36 ) 37 38 func NewWithCache(cache Cache) transports.Middleware { 39 return &Middleware{ 40 cache: cache, 41 } 42 } 43 44 func New() transports.Middleware { 45 return NewWithCache(new(DefaultCache)) 46 } 47 48 var ( 49 getMethod = []byte("GET") 50 ) 51 52 // Middleware 53 // use @cache-control max-age=10 public=true must-revalidate=false proxy-revalidate=false 54 type Middleware struct { 55 log logs.Logger 56 cache Cache 57 enable bool 58 maxAge int 59 } 60 61 func (middleware *Middleware) Name() string { 62 return "cachecontrol" 63 } 64 65 func (middleware *Middleware) Construct(options transports.MiddlewareOptions) (err error) { 66 middleware.log = options.Log 67 config := Config{} 68 configErr := options.Config.As(&config) 69 if configErr != nil { 70 err = errors.Warning("fns: construct cache control middleware failed").WithCause(configErr) 71 return 72 } 73 if config.Enable { 74 middleware.enable = config.Enable 75 middleware.maxAge = config.MaxAge 76 if middleware.maxAge < 1 { 77 middleware.maxAge = 60 78 } 79 } 80 return 81 } 82 83 func (middleware *Middleware) Handler(next transports.Handler) transports.Handler { 84 if middleware.enable { 85 return transports.HandlerFunc(func(writer transports.ResponseWriter, request transports.Request) { 86 isGet := bytes.Equal(request.Method(), getMethod) 87 if !isGet { 88 next.Handle(writer, request) 89 return 90 } 91 rch := request.Header().Get(transports.CacheControlHeaderName) 92 if bytes.Equal(rch, transports.CacheControlHeaderNoStore) || bytes.Equal(rch, transports.CacheControlHeaderNoCache) { 93 next.Handle(writer, request) 94 return 95 } 96 // request key 97 var key []byte 98 // if-no-match 99 if inm := request.Header().Get(transports.CacheControlHeaderIfNonMatch); len(inm) > 0 { 100 key = hashRequest(request) 101 etag, hasEtag, getErr := middleware.cache.Get(request, key) 102 if getErr != nil { 103 if middleware.log.WarnEnabled() { 104 middleware.log.Warn(). 105 With("middleware", "cachecontrol"). 106 Cause(errors.Warning("fns: get etag from cache store failed").WithCause(getErr)). 107 Message("get etag from cache store failed") 108 } 109 next.Handle(writer, request) 110 return 111 } 112 if hasEtag && bytes.Equal(etag, inm) { 113 writer.SetStatus(http.StatusNotModified) 114 return 115 } 116 } 117 // set enable 118 request.SetLocalValue(contextEnableKey, true) 119 // next 120 next.Handle(writer, request) 121 // check response 122 cch := writer.Header().Get(transports.CacheControlHeaderName) 123 if len(cch) == 0 { 124 return 125 } 126 maxAgeValue := 0 127 hasMaxAge := false 128 if idx := bytes.Index(cch, maxAge); idx > -1 { 129 segment := cch[idx+8:] 130 commaIdx := bytes.IndexByte(segment, ',') 131 if commaIdx > 0 { 132 segment = segment[:commaIdx] 133 } 134 ma, parseErr := strconv.Atoi(bytex.ToString(segment)) 135 if parseErr == nil { 136 maxAgeValue = ma 137 hasMaxAge = true 138 } else { 139 // remove invalid max-age 140 if idx == 0 { 141 cch = cch[idx+8+commaIdx+1:] 142 } else { 143 cch = append(cch[:idx], cch[idx+8+commaIdx+1:]...) 144 } 145 } 146 } 147 if !hasMaxAge { 148 maxAgeValue = middleware.maxAge 149 cch = append(cch, ',', ' ') 150 cch = append(cch, maxAge...) 151 cch = append(cch, '=') 152 cch = append(cch, strconv.Itoa(maxAgeValue)...) 153 writer.Header().Set(transports.CacheControlHeaderName, cch) 154 } 155 if len(key) == 0 { 156 key = hashRequest(request) 157 } 158 // etag 159 var etag []byte 160 if bodyLen := writer.BodyLen(); bodyLen == 0 { 161 etag = nilBodyETag 162 } else { 163 body := writer.Body() 164 etag = bytex.FromString(strconv.FormatUint(xxhash.Sum64(body), 16)) 165 } 166 setErr := middleware.cache.Set(request, key, etag, time.Duration(maxAgeValue)*time.Second) 167 if setErr == nil { 168 writer.Header().Set(transports.ETagHeaderName, etag) 169 } else { 170 if middleware.log.WarnEnabled() { 171 middleware.log.Warn(). 172 With("middleware", "cachecontrol"). 173 Cause(errors.Warning("fns: set etag into cache store failed").WithCause(setErr)). 174 Message("set etag into cache store failed") 175 } 176 // use no-cache 177 writer.Header().Set(transports.CacheControlHeaderName, transports.CacheControlHeaderNoCache) 178 writer.Header().Set(pragma, transports.CacheControlHeaderNoCache) 179 } 180 }) 181 } 182 return next 183 } 184 185 func (middleware *Middleware) Close() (err error) { 186 middleware.cache.Close() 187 return 188 } 189 190 func hashRequest(r transports.Request) (p []byte) { 191 b := bytebufferpool.Get() 192 // device id 193 deviceId := r.Header().Get(transports.DeviceIdHeaderName) 194 _, _ = b.Write(deviceId) 195 // path 196 _, _ = b.Write(r.Path()) 197 // param 198 param := r.Params() 199 _, _ = b.Write(param.Encode()) 200 // authorization 201 if authorization := r.Header().Get(transports.AuthorizationHeaderName); len(authorization) > 0 { 202 _, _ = b.Write(authorization) 203 } 204 pp := b.Bytes() 205 p = bytex.FromString(strconv.FormatUint(xxhash.Sum64(pp), 16)) 206 bytebufferpool.Put(b) 207 return 208 }