object.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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. var reqHeaders http.Header
  203. var reqMethod string
  204. var reqDir *RequestCacheDirectives = nil
  205. respDir, err := ParseResponseCacheControl(respHeaders.Get("Cache-Control"))
  206. if err != nil {
  207. return nil, time.Time{}, err
  208. }
  209. if req != nil {
  210. reqDir, err = ParseRequestCacheControl(req.Header.Get("Cache-Control"))
  211. if err != nil {
  212. return nil, time.Time{}, err
  213. }
  214. reqHeaders = req.Header
  215. reqMethod = req.Method
  216. }
  217. var expiresHeader time.Time
  218. var dateHeader time.Time
  219. var lastModifiedHeader time.Time
  220. if respHeaders.Get("Expires") != "" {
  221. expiresHeader, err = http.ParseTime(respHeaders.Get("Expires"))
  222. if err != nil {
  223. // sometimes servers will return `Expires: 0` or `Expires: -1` to
  224. // indicate expired content
  225. expiresHeader = time.Time{}
  226. }
  227. expiresHeader = expiresHeader.UTC()
  228. }
  229. if respHeaders.Get("Date") != "" {
  230. dateHeader, err = http.ParseTime(respHeaders.Get("Date"))
  231. if err != nil {
  232. return nil, time.Time{}, err
  233. }
  234. dateHeader = dateHeader.UTC()
  235. }
  236. if respHeaders.Get("Last-Modified") != "" {
  237. lastModifiedHeader, err = http.ParseTime(respHeaders.Get("Last-Modified"))
  238. if err != nil {
  239. return nil, time.Time{}, err
  240. }
  241. lastModifiedHeader = lastModifiedHeader.UTC()
  242. }
  243. obj := Object{
  244. CacheIsPrivate: privateCache,
  245. RespDirectives: respDir,
  246. RespHeaders: respHeaders,
  247. RespStatusCode: statusCode,
  248. RespExpiresHeader: expiresHeader,
  249. RespDateHeader: dateHeader,
  250. RespLastModifiedHeader: lastModifiedHeader,
  251. ReqDirectives: reqDir,
  252. ReqHeaders: reqHeaders,
  253. ReqMethod: reqMethod,
  254. NowUTC: time.Now().UTC(),
  255. }
  256. rv := ObjectResults{}
  257. CachableObject(&obj, &rv)
  258. if rv.OutErr != nil {
  259. return nil, time.Time{}, rv.OutErr
  260. }
  261. ExpirationObject(&obj, &rv)
  262. if rv.OutErr != nil {
  263. return nil, time.Time{}, rv.OutErr
  264. }
  265. return rv.OutReasons, rv.OutExpirationTime, nil
  266. }
  267. // calculate if a freshness directive is present: http://tools.ietf.org/html/rfc7234#section-4.2.1
  268. func hasFreshness(reqDir *RequestCacheDirectives, respDir *ResponseCacheDirectives, respHeaders http.Header, respExpires time.Time, privateCache bool) bool {
  269. if !privateCache && respDir.SMaxAge != -1 {
  270. return true
  271. }
  272. if respDir.MaxAge != -1 {
  273. return true
  274. }
  275. if !respExpires.IsZero() || respHeaders.Get("Expires") != "" {
  276. return true
  277. }
  278. return false
  279. }
  280. func cachableStatusCode(statusCode int) bool {
  281. /*
  282. Responses with status codes that are defined as cacheable by default
  283. (e.g., 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 in
  284. this specification) can be reused by a cache with heuristic
  285. expiration unless otherwise indicated by the method definition or
  286. explicit cache controls [RFC7234]; all other status codes are not
  287. cacheable by default.
  288. */
  289. switch statusCode {
  290. case 200:
  291. return true
  292. case 203:
  293. return true
  294. case 204:
  295. return true
  296. case 206:
  297. return true
  298. case 300:
  299. return true
  300. case 301:
  301. return true
  302. case 404:
  303. return true
  304. case 405:
  305. return true
  306. case 410:
  307. return true
  308. case 414:
  309. return true
  310. case 501:
  311. return true
  312. default:
  313. return false
  314. }
  315. }