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 }