github.com/vicanso/pike@v1.0.1-0.20210630235453-9099e041f6ec/server/proxy.go (about)

     1  // MIT License
     2  
     3  // Copyright (c) 2020 Tree Xie
     4  
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  
    23  package server
    24  
    25  import (
    26  	"net/http"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"github.com/vicanso/elton"
    32  	"github.com/vicanso/hes"
    33  	"github.com/vicanso/pike/cache"
    34  	"github.com/vicanso/pike/location"
    35  	"github.com/vicanso/pike/upstream"
    36  	"github.com/vicanso/pike/util"
    37  	"golang.org/x/net/context"
    38  )
    39  
    40  var (
    41  	noCacheReg = regexp.MustCompile(`no-cache|no-store|private`)
    42  	sMaxAgeReg = regexp.MustCompile(`s-maxage=(\d+)`)
    43  	maxAgeReg  = regexp.MustCompile(`max-age=(\d+)`)
    44  )
    45  
    46  // 根据Cache-Control的信息,获取s-maxage 或者max-age的值
    47  func getCacheMaxAge(header http.Header) int {
    48  	// 如果有设置cookie,则为不可缓存
    49  	if header.Get(elton.HeaderSetCookie) != "" {
    50  		return 0
    51  	}
    52  	// 如果没有设置cache-control,则不可缓存
    53  	cc := strings.Join(header.Values(elton.HeaderCacheControl), ",")
    54  	if cc == "" {
    55  		return 0
    56  	}
    57  
    58  	// 如果设置不可缓存,返回0
    59  	if noCacheReg.MatchString(cc) {
    60  		return 0
    61  	}
    62  	// 优先从s-maxage中获取
    63  	var maxAge = 0
    64  	result := sMaxAgeReg.FindStringSubmatch(cc)
    65  	if len(result) == 2 {
    66  		maxAge, _ = strconv.Atoi(result[1])
    67  	} else {
    68  		// 从max-age中获取缓存时间
    69  		result = maxAgeReg.FindStringSubmatch(cc)
    70  		if len(result) == 2 {
    71  			maxAge, _ = strconv.Atoi(result[1])
    72  		}
    73  	}
    74  
    75  	// 如果有设置了 age 字段,则最大缓存时长减少
    76  	if age := header.Get(headerAge); age != "" {
    77  		v, _ := strconv.Atoi(age)
    78  		maxAge -= v
    79  	}
    80  
    81  	return maxAge
    82  }
    83  
    84  // NewProxy create proxy middleware
    85  func NewProxy(s *server) elton.Handler {
    86  	return func(c *elton.Context) (err error) {
    87  		originalNext := c.Next
    88  		// 由于proxy中间件会调用next,因此直接覆盖,
    89  		// 避免导致先执行了后续的中间件(保证在函数调用next前是已完成此中间件处理)
    90  		c.Next = func() error {
    91  			return nil
    92  		}
    93  
    94  		l := location.Get(c.Request.Host, c.Request.RequestURI, s.GetLocations()...)
    95  		if l == nil {
    96  			err = ErrLocationNotFound
    97  			return
    98  		}
    99  
   100  		upstream := upstream.Get(l.Upstream)
   101  		if upstream == nil {
   102  			err = ErrUpstreamNotFound
   103  			return
   104  		}
   105  
   106  		reqHeader := c.Request.Header
   107  		var ifModifiedSince, ifNoneMatch string
   108  		status := getCacheStatus(c)
   109  		// 针对fetching的请求,由于其最终状态未知,因此需要删除有可能导致304的请求,避免无法生成缓存
   110  		if status == cache.StatusFetching {
   111  			ifModifiedSince = reqHeader.Get(elton.HeaderIfModifiedSince)
   112  			ifNoneMatch = reqHeader.Get(elton.HeaderIfNoneMatch)
   113  			if ifModifiedSince != "" {
   114  				reqHeader.Del(elton.HeaderIfModifiedSince)
   115  			}
   116  			if ifNoneMatch != "" {
   117  				reqHeader.Del(elton.HeaderIfNoneMatch)
   118  			}
   119  		}
   120  
   121  		// url rewrite
   122  		var originalPath string
   123  		if l.URLRewriter != nil {
   124  			originalPath = c.Request.URL.Path
   125  			l.URLRewriter(c.Request)
   126  		}
   127  		// 添加额外的请求头
   128  		l.AddRequestHeader(reqHeader)
   129  
   130  		// 添加query string
   131  		var originRawQuery string
   132  		if l.ShouldModifyQuery() {
   133  			originRawQuery = c.Request.URL.RawQuery
   134  			l.AddQuery(c.Request)
   135  		}
   136  
   137  		var acceptEncoding string
   138  
   139  		// 根据upstream设置可接受压缩编码调整
   140  		acceptEncodingChanged := upstream.Option.AcceptEncoding != ""
   141  		if acceptEncodingChanged {
   142  			acceptEncoding = reqHeader.Get(elton.HeaderAcceptEncoding)
   143  			reqHeader.Set(elton.HeaderAcceptEncoding, upstream.Option.AcceptEncoding)
   144  		}
   145  
   146  		if l.ProxyTimeout != 0 {
   147  			ctx, cancel := context.WithTimeout(c.Context(), l.ProxyTimeout)
   148  			defer cancel()
   149  			c.WithContext(ctx)
   150  		}
   151  
   152  		// clone当前header,用于后续恢复
   153  		originalHeader := c.Header().Clone()
   154  		c.ResetHeader()
   155  		err = upstream.Proxy(c)
   156  		// 如果出错超时,则转换为504 timeout,category:pike
   157  		if err != nil {
   158  			if he, ok := err.(*hes.Error); ok {
   159  				if he.Err == context.DeadlineExceeded {
   160  					err = util.NewError("Timeout", http.StatusGatewayTimeout)
   161  				}
   162  			}
   163  		}
   164  
   165  		// 恢复请求头
   166  		if ifModifiedSince != "" {
   167  			reqHeader.Set(elton.HeaderIfModifiedSince, ifModifiedSince)
   168  		}
   169  		if ifNoneMatch != "" {
   170  			reqHeader.Set(elton.HeaderIfNoneMatch, ifNoneMatch)
   171  		}
   172  		if acceptEncodingChanged {
   173  			reqHeader.Set(elton.HeaderAcceptEncoding, acceptEncoding)
   174  		}
   175  
   176  		// 恢复query
   177  		if originRawQuery != "" {
   178  			c.Request.URL.RawQuery = originRawQuery
   179  		}
   180  
   181  		header := c.Header()
   182  		// 添加额外的响应头
   183  		l.AddResponseHeader(header)
   184  		// 恢复原始url path
   185  		if originalPath != "" {
   186  			c.Request.URL.Path = originalPath
   187  		}
   188  
   189  		if err != nil {
   190  			return
   191  		}
   192  
   193  		var data []byte
   194  		if c.BodyBuffer != nil {
   195  			data = c.BodyBuffer.Bytes()
   196  		}
   197  
   198  		// 对于fetching的请求,从响应头中判断该请求缓存的有效期
   199  		if status == cache.StatusFetching {
   200  			maxAge := getCacheMaxAge(header)
   201  			if maxAge > 0 {
   202  				setHTTPCacheMaxAge(c, maxAge)
   203  			}
   204  		}
   205  
   206  		// 初始化http response时,如果已压缩,而且非gzip br,则会解压
   207  		httpResp, err := cache.NewHTTPResponse(c.StatusCode, header, header.Get(elton.HeaderContentEncoding), data)
   208  		if err != nil {
   209  			return
   210  		}
   211  
   212  		compressSrv, minLength, filter := s.GetCompress()
   213  		httpResp.CompressSrv = compressSrv
   214  		httpResp.CompressMinLength = minLength
   215  		httpResp.CompressContentTypeFilter = filter
   216  		setHTTPResp(c, httpResp)
   217  
   218  		// 重置context中由于proxy中间件影响的状态 statusCode, header, body
   219  		// 因为最终响应会从http response中生成,该响应会包括http响应头,
   220  		// 因此清除现在的header并恢复原来的header
   221  		c.ResetHeader()
   222  		c.MergeHeader(originalHeader)
   223  		c.BodyBuffer = nil
   224  		c.StatusCode = 0
   225  		c.Next = originalNext
   226  		return c.Next()
   227  	}
   228  }