object.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. /**
  2. * Copyright 2015 Paul Querna
  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. package cacheobject
  18. import (
  19. "net/http"
  20. "time"
  21. )
  22. // LOW LEVEL API: Repersents a potentially cachable HTTP object.
  23. //
  24. // This struct is designed to be serialized efficiently, so in a high
  25. // performance caching server, things like Date-Strings don't need to be
  26. // parsed for every use of a cached object.
  27. type Object struct {
  28. CacheIsPrivate bool
  29. RespDirectives *ResponseCacheDirectives
  30. RespHeaders http.Header
  31. RespStatusCode int
  32. RespExpiresHeader time.Time
  33. RespDateHeader time.Time
  34. RespLastModifiedHeader time.Time
  35. ReqDirectives *RequestCacheDirectives
  36. ReqHeaders http.Header
  37. ReqMethod string
  38. NowUTC time.Time
  39. }
  40. // LOW LEVEL API: Repersents the results of examinig an Object with
  41. // CachableObject and ExpirationObject.
  42. //
  43. // TODO(pquerna): decide if this is a good idea or bad
  44. type ObjectResults struct {
  45. OutReasons []Reason
  46. OutWarnings []Warning
  47. OutExpirationTime time.Time
  48. OutErr error
  49. }
  50. // LOW LEVEL API: Check if a object is cachable.
  51. func CachableObject(obj *Object, rv *ObjectResults) {
  52. rv.OutReasons = nil
  53. rv.OutWarnings = nil
  54. rv.OutErr = nil
  55. switch obj.ReqMethod {
  56. case "GET":
  57. break
  58. case "HEAD":
  59. break
  60. case "POST":
  61. /**
  62. POST: http://tools.ietf.org/html/rfc7231#section-4.3.3
  63. Responses to POST requests are only cacheable when they include
  64. explicit freshness information (see Section 4.2.1 of [RFC7234]).
  65. However, POST caching is not widely implemented. For cases where an
  66. origin server wishes the client to be able to cache the result of a
  67. POST in a way that can be reused by a later GET, the origin server
  68. MAY send a 200 (OK) response containing the result and a
  69. Content-Location header field that has the same value as the POST's
  70. effective request URI (Section 3.1.4.2).
  71. */
  72. if !hasFreshness(obj.ReqDirectives, obj.RespDirectives, obj.RespHeaders, obj.RespExpiresHeader, obj.CacheIsPrivate) {
  73. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPOST)
  74. }
  75. case "PUT":
  76. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodPUT)
  77. case "DELETE":
  78. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodDELETE)
  79. case "CONNECT":
  80. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodCONNECT)
  81. case "OPTIONS":
  82. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodOPTIONS)
  83. case "TRACE":
  84. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodTRACE)
  85. // HTTP Extension Methods: http://www.iana.org/assignments/http-methods/http-methods.xhtml
  86. //
  87. // To my knowledge, none of them are cachable. Please open a ticket if this is not the case!
  88. //
  89. default:
  90. rv.OutReasons = append(rv.OutReasons, ReasonRequestMethodUnkown)
  91. }
  92. if obj.ReqDirectives.NoStore {
  93. rv.OutReasons = append(rv.OutReasons, ReasonRequestNoStore)
  94. }
  95. // Storing Responses to Authenticated Requests: http://tools.ietf.org/html/rfc7234#section-3.2
  96. authz := obj.ReqHeaders.Get("Authorization")
  97. if authz != "" {
  98. if obj.RespDirectives.MustRevalidate ||
  99. obj.RespDirectives.Public ||
  100. obj.RespDirectives.SMaxAge != -1 {
  101. // Expires of some kind present, this is potentially OK.
  102. } else {
  103. rv.OutReasons = append(rv.OutReasons, ReasonRequestAuthorizationHeader)
  104. }
  105. }
  106. if obj.RespDirectives.PrivatePresent && !obj.CacheIsPrivate {
  107. rv.OutReasons = append(rv.OutReasons, ReasonResponsePrivate)
  108. }
  109. if obj.RespDirectives.NoStore {
  110. rv.OutReasons = append(rv.OutReasons, ReasonResponseNoStore)
  111. }
  112. /*
  113. the response either:
  114. * contains an Expires header field (see Section 5.3), or
  115. * contains a max-age response directive (see Section 5.2.2.8), or
  116. * contains a s-maxage response directive (see Section 5.2.2.9)
  117. and the cache is shared, or
  118. * contains a Cache Control Extension (see Section 5.2.3) that
  119. allows it to be cached, or
  120. * has a status code that is defined as cacheable by default (see
  121. Section 4.2.2), or
  122. * contains a public response directive (see Section 5.2.2.5).
  123. */
  124. expires := obj.RespHeaders.Get("Expires") != ""
  125. statusCachable := cachableStatusCode(obj.RespStatusCode)
  126. if expires ||
  127. obj.RespDirectives.MaxAge != -1 ||
  128. (obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate) ||
  129. statusCachable ||
  130. obj.RespDirectives.Public {
  131. /* cachable by default, at least one of the above conditions was true */
  132. } else {
  133. rv.OutReasons = append(rv.OutReasons, ReasonResponseUncachableByDefault)
  134. }
  135. }
  136. var twentyFourHours = time.Duration(24 * time.Hour)
  137. const debug = false
  138. // LOW LEVEL API: Update an objects expiration time.
  139. func ExpirationObject(obj *Object, rv *ObjectResults) {
  140. /**
  141. * Okay, lets calculate Freshness/Expiration now. woo:
  142. * http://tools.ietf.org/html/rfc7234#section-4.2
  143. */
  144. /*
  145. o If the cache is shared and the s-maxage response directive
  146. (Section 5.2.2.9) is present, use its value, or
  147. o If the max-age response directive (Section 5.2.2.8) is present,
  148. use its value, or
  149. o If the Expires response header field (Section 5.3) is present, use
  150. its value minus the value of the Date response header field, or
  151. o Otherwise, no explicit expiration time is present in the response.
  152. A heuristic freshness lifetime might be applicable; see
  153. Section 4.2.2.
  154. */
  155. var expiresTime time.Time
  156. if obj.RespDirectives.SMaxAge != -1 && !obj.CacheIsPrivate {
  157. expiresTime = obj.NowUTC.Add(time.Second * time.Duration(obj.RespDirectives.SMaxAge))
  158. } else if obj.RespDirectives.MaxAge != -1 {
  159. expiresTime = obj.NowUTC.UTC().Add(time.Second * time.Duration(obj.RespDirectives.MaxAge))
  160. } else if !obj.RespExpiresHeader.IsZero() {
  161. serverDate := obj.RespDateHeader
  162. if serverDate.IsZero() {
  163. // common enough case when a Date: header has not yet been added to an
  164. // active response.
  165. serverDate = obj.NowUTC
  166. }
  167. expiresTime = obj.NowUTC.Add(obj.RespExpiresHeader.Sub(serverDate))
  168. } else if !obj.RespLastModifiedHeader.IsZero() {
  169. // heuristic freshness lifetime
  170. rv.OutWarnings = append(rv.OutWarnings, WarningHeuristicExpiration)
  171. // http://httpd.apache.org/docs/2.4/mod/mod_cache.html#cachelastmodifiedfactor
  172. // CacheMaxExpire defaults to 24 hours
  173. // CacheLastModifiedFactor: is 0.1
  174. //
  175. // expiry-period = MIN(time-since-last-modified-date * factor, 24 hours)
  176. //
  177. // obj.NowUTC
  178. since := obj.RespLastModifiedHeader.Sub(obj.NowUTC)
  179. since = time.Duration(float64(since) * -0.1)
  180. if since > twentyFourHours {
  181. expiresTime = obj.NowUTC.Add(twentyFourHours)
  182. } else {
  183. expiresTime = obj.NowUTC.Add(since)
  184. }
  185. if debug {
  186. println("Now UTC: ", obj.NowUTC.String())
  187. println("Last-Modified: ", obj.RespLastModifiedHeader.String())
  188. println("Since: ", since.String())
  189. println("TwentyFourHours: ", twentyFourHours.String())
  190. println("Expiration: ", expiresTime.String())
  191. }
  192. } else {
  193. // TODO(pquerna): what should the default behavoir be for expiration time?
  194. }
  195. rv.OutExpirationTime = expiresTime
  196. }
  197. // Evaluate cachability based on an HTTP request, and parts of the response.
  198. func UsingRequestResponse(req *http.Request,
  199. statusCode int,
  200. respHeaders http.Header,
  201. privateCache bool) ([]Reason, time.Time, error) {
  202. reasons, time, _, _, err := UsingRequestResponseWithObject(req, statusCode, respHeaders, privateCache)
  203. return reasons, time, err
  204. }
  205. // Evaluate cachability based on an HTTP request, and parts of the response.
  206. // Returns the parsed Object as well.
  207. func UsingRequestResponseWithObject(req *http.Request,
  208. statusCode int,
  209. respHeaders http.Header,
  210. privateCache bool) ([]Reason, time.Time, []Warning, *Object, error) {
  211. var reqHeaders http.Header
  212. var reqMethod string
  213. var reqDir *RequestCacheDirectives = nil
  214. respDir, err := ParseResponseCacheControl(respHeaders.Get("Cache-Control"))
  215. if err != nil {
  216. return nil, time.Time{}, nil, nil, err
  217. }
  218. if req != nil {
  219. reqDir, err = ParseRequestCacheControl(req.Header.Get("Cache-Control"))
  220. if err != nil {
  221. return nil, time.Time{}, nil, nil, err
  222. }
  223. reqHeaders = req.Header
  224. reqMethod = req.Method
  225. }
  226. var expiresHeader time.Time
  227. var dateHeader time.Time
  228. var lastModifiedHeader time.Time
  229. if respHeaders.Get("Expires") != "" {
  230. expiresHeader, err = http.ParseTime(respHeaders.Get("Expires"))
  231. if err != nil {
  232. // sometimes servers will return `Expires: 0` or `Expires: -1` to
  233. // indicate expired content
  234. expiresHeader = time.Time{}
  235. }
  236. expiresHeader = expiresHeader.UTC()
  237. }
  238. if respHeaders.Get("Date") != "" {
  239. dateHeader, err = http.ParseTime(respHeaders.Get("Date"))
  240. if err != nil {
  241. return nil, time.Time{}, nil, nil, err
  242. }
  243. dateHeader = dateHeader.UTC()
  244. }
  245. if respHeaders.Get("Last-Modified") != "" {
  246. lastModifiedHeader, err = http.ParseTime(respHeaders.Get("Last-Modified"))
  247. if err != nil {
  248. return nil, time.Time{}, nil, nil, err
  249. }
  250. lastModifiedHeader = lastModifiedHeader.UTC()
  251. }
  252. obj := Object{
  253. CacheIsPrivate: privateCache,
  254. RespDirectives: respDir,
  255. RespHeaders: respHeaders,
  256. RespStatusCode: statusCode,
  257. RespExpiresHeader: expiresHeader,
  258. RespDateHeader: dateHeader,
  259. RespLastModifiedHeader: lastModifiedHeader,
  260. ReqDirectives: reqDir,
  261. ReqHeaders: reqHeaders,
  262. ReqMethod: reqMethod,
  263. NowUTC: time.Now().UTC(),
  264. }
  265. rv := ObjectResults{}
  266. CachableObject(&obj, &rv)
  267. if rv.OutErr != nil {
  268. return nil, time.Time{}, nil, nil, rv.OutErr
  269. }
  270. ExpirationObject(&obj, &rv)
  271. if rv.OutErr != nil {
  272. return nil, time.Time{}, nil, nil, rv.OutErr
  273. }
  274. return rv.OutReasons, rv.OutExpirationTime, rv.OutWarnings, &obj, nil
  275. }
  276. // calculate if a freshness directive is present: http://tools.ietf.org/html/rfc7234#section-4.2.1
  277. func hasFreshness(reqDir *RequestCacheDirectives, respDir *ResponseCacheDirectives, respHeaders http.Header, respExpires time.Time, privateCache bool) bool {
  278. if !privateCache && respDir.SMaxAge != -1 {
  279. return true
  280. }
  281. if respDir.MaxAge != -1 {
  282. return true
  283. }
  284. if !respExpires.IsZero() || respHeaders.Get("Expires") != "" {
  285. return true
  286. }
  287. return false
  288. }
  289. func cachableStatusCode(statusCode int) bool {
  290. /*
  291. Responses with status codes that are defined as cacheable by default
  292. (e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in
  293. this specification) can be reused by a cache with heuristic
  294. expiration unless otherwise indicated by the method definition or
  295. explicit cache controls [RFC7234]; all other status codes are not
  296. cacheable by default.
  297. */
  298. switch statusCode {
  299. case 200:
  300. return true
  301. case 203:
  302. return true
  303. case 204:
  304. return true
  305. case 206:
  306. return true
  307. case 300:
  308. return true
  309. case 301:
  310. return true
  311. case 404:
  312. return true
  313. case 405:
  314. return true
  315. case 410:
  316. return true
  317. case 414:
  318. return true
  319. case 501:
  320. return true
  321. default:
  322. return false
  323. }
  324. }