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  }