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 }