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