1 /**
2 * Cached HTTP responses
3 *
4 * License:
5 * This Source Code Form is subject to the terms of
6 * the Mozilla Public License, v. 2.0. If a copy of
7 * the MPL was not distributed with this file, You
8 * can obtain one at http://mozilla.org/MPL/2.0/.
9 *
10 * Authors:
11 * Vladimir Panteleev <ae@cy.md>
12 */
13
14 module ae.net.http.caching;
15
16 import std.algorithm.mutation : move;
17 import std.datetime;
18
19 import ae.net.http.common;
20 import ae.net.http.responseex;
21 import ae.sys.data;
22 import ae.utils.mime;
23 import ae.utils.time.common;
24 import ae.utils.time.parse;
25 import ae.utils.zlib;
26 import ae.utils.gzip;
27 private alias zlib = ae.utils.zlib;
28
29 /// Controls which caching headers are sent to clients.
30 enum CachePolicy
31 {
32 /// No caching headers are sent.
33 /// Usually results in occasional If-Modified-Since / 304 Not Modified checks.
34 unspecified,
35
36 /// Data at this URL will never change.
37 forever,
38
39 /// Disable caching (use for frequently changing content).
40 disable
41 }
42
43 /// Abstract class for caching resources in memory.
44 /// Stores compressed version as well.
45 class AbstractCachedResource
46 {
47 /// Zlib compression level
48 int compressionLevel = 1;
49
50 protected: // interface with descendant classes
51
52 /// Get uncompressed data. The call may be expensive,
53 /// result is cached (in uncompressedData).
54 abstract DataVec getData();
55
56 /// Return last modified time.
57 /// Used for Last-Modified and If-Modified-Since headers.
58 /// Called when cached data is invalidated; return value is cached.
59 /// Returns current (invalidation) time by default.
60 SysTime getLastModified()
61 {
62 return Clock.currTime(UTC());
63 }
64
65 // TODO: ETag?
66
67 /// Caching policy. To be set during subclass construction.
68 CachePolicy cachePolicy = CachePolicy.unspecified;
69
70 /// MIME content type. To be set during subclass construction.
71 string contentType;
72
73 /// Called every time a request is done.
74 /// Can be used to check if cached data expired, and invalidate() it.
75 /// By default is a no-op.
76 void newRequest() {}
77
78 /// Clear cached uncompressed and compressed data.
79 /// An alternative to calling invalidate() from a subclass is to
80 /// simply create a new class instance when data becomes stale
81 /// (overhead is negligible).
82 final void invalidate()
83 {
84 uncompressedDataCache = deflateDataCache = gzipDataCache = null;
85 lastModified = getLastModified();
86 }
87
88 this()
89 {
90 invalidate();
91 }
92
93 private:
94 DataVec uncompressedDataCache, deflateDataCache, gzipDataCache;
95 SysTime lastModified;
96
97 @property final ref DataVec uncompressedData()
98 {
99 if (!uncompressedDataCache)
100 uncompressedDataCache = getData();
101 return uncompressedDataCache;
102 }
103
104 @property final ref DataVec deflateData()
105 {
106 if (!deflateDataCache)
107 deflateDataCache = zlib.compress(uncompressedData[], zlib.ZlibOptions(compressionLevel));
108 return deflateDataCache;
109 }
110
111 @property final ref DataVec gzipData()
112 {
113 // deflate2gzip doesn't actually make a copy of the compressed data (thanks to DataVec).
114 if (!!gzipDataCache)
115 gzipDataCache = deflate2gzip(deflateData[], crc32(uncompressedData[]), uncompressedData.bytes.length);
116 return gzipDataCache;
117 }
118
119 // Use one response object per HTTP request (as opposed to one response
120 // object per cached resource) to avoid problems with simultaneous HTTP
121 // requests to the same resource (server calls .optimizeData() which
122 // mutates the response object).
123 final class Response : HttpResponse
124 {
125 protected:
126 override void compressWithDeflate()
127 {
128 assert(data is uncompressedData);
129 data = deflateData.dup;
130 }
131
132 override void compressWithGzip()
133 {
134 assert(data is uncompressedData);
135 data = gzipData.dup;
136 }
137 }
138
139 public:
140 /// Used by application code.
141 final HttpResponse getResponse(HttpRequest request)
142 {
143 newRequest();
144 auto response = new Response();
145
146 if ("If-Modified-Since" in request.headers)
147 {
148 auto clientTime = request.headers["If-Modified-Since"].parseTime!(TimeFormats.HTTP)();
149 auto serverTime = httpTime(lastModified) .parseTime!(TimeFormats.HTTP)(); // make sure to avoid any issues of fractional seconds, etc.
150 if (serverTime <= clientTime)
151 {
152 response.setStatus(HttpStatusCode.NotModified);
153 return response;
154 }
155 }
156
157 response.setStatus(HttpStatusCode.OK);
158 response.data = uncompressedData.dup;
159 final switch (cachePolicy)
160 {
161 case CachePolicy.unspecified:
162 break;
163 case CachePolicy.forever:
164 cacheForever(response.headers);
165 break;
166 case CachePolicy.disable:
167 disableCache(response.headers);
168 break;
169 }
170
171 if (contentType)
172 response.headers["Content-Type"] = contentType;
173
174 response.headers["Last-Modified"] = httpTime(lastModified);
175
176 return response;
177 }
178 }
179
180 /// A cached static file on disk (e.g. style, script, image).
181 // TODO: cachePolicy = forever when integrated with URL generation
182 class StaticResource : AbstractCachedResource
183 {
184 private:
185 import std.file : exists, isFile, timeLastModified;
186
187 string filename;
188 SysTime fileTime, lastChecked;
189
190 /// Don't check if the file on disk was modified more often than this interval.
191 enum STAT_TIMEOUT = dur!"seconds"(1);
192
193 protected:
194 override DataVec getData()
195 {
196 if (!exists(filename) || !isFile(filename)) // TODO: 404
197 throw new Exception("Static resource does not exist on disk");
198
199 // maybe use mmap?
200 // mmap implies either file locking, or risk of bad data (file content changes, mapped length not)
201
202 import ae.sys.dataio : readData;
203 return DataVec(readData(filename));
204 }
205
206 override SysTime getLastModified()
207 {
208 return fileTime;
209 }
210
211 override void newRequest()
212 {
213 auto now = Clock.currTime();
214 if ((now - lastChecked) > STAT_TIMEOUT)
215 {
216 lastChecked = now;
217
218 auto newFileTime = timeLastModified(filename);
219 if (newFileTime != fileTime)
220 {
221 fileTime = newFileTime;
222 invalidate();
223 }
224 }
225 }
226
227 public:
228 ///
229 this(string filename)
230 {
231 this.filename = filename;
232 contentType = guessMime(filename);
233 }
234 }
235
236 /// A generic cached resource, for resources that change
237 /// less often than they are requested (e.g. RSS feeds).
238 class CachedResource : AbstractCachedResource
239 {
240 private:
241 DataVec data;
242
243 protected:
244 override DataVec getData()
245 {
246 return data.dup;
247 }
248
249 public:
250 ///
251 this(DataVec data, string contentType)
252 {
253 this.data = move(data);
254 this.contentType = contentType;
255 }
256
257 /// Update the contents.
258 void setData(DataVec data)
259 {
260 this.data = move(data);
261 invalidate();
262 }
263 }