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