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