| | @@ -15,193 +15,124 @@ |
| 15 | 15 | ** |
| 16 | 16 | ******************************************************************************* |
| 17 | 17 | ** |
| 18 | 18 | ** This file implements ETags: cache control for Fossil |
| 19 | 19 | ** |
| 20 | | -** Each ETag value is a text string that represents a sequence of conditionals |
| 21 | | -** like this: |
| 22 | | -** |
| 23 | | -** if( executable-has-change ) return; |
| 24 | | -** if( database-has-changed ) return; |
| 25 | | -** if( display-cookie-"n"-attribute-has-changes ) return; |
| 26 | | -** Output "304 Not Modified" message and abort; |
| 27 | | -** |
| 28 | | -** In other words, if all conditions specified by the ETag are met, then |
| 29 | | -** Fossil will return a 304 and avoid doing all the work, and all of the |
| 30 | | -** bandwidth, associating with regenerating the whole page. |
| 31 | | -** |
| 32 | | -** To make use of this feature, page generators can invoke the |
| 33 | | -** etag_require() interface with mask of ETAG_CONST, ETAG_CONFIG, |
| 34 | | -** ETAG_DATA, and/or ETAG_COOKIE. Or it can invoke etag_require_hash() |
| 35 | | -** with some kind of text hash. |
| 36 | | -** |
| 37 | | -** Or, in the WEBPAGE: line for the page generator, extra arguments |
| 38 | | -** can be added. "const", "config", "data", and/or "cookie" |
| 39 | | -** |
| 40 | | -** ETAG_CONST const The reply is always the same for the same |
| 41 | | -** build of the fossil binary. The content |
| 42 | | -** is independent of the repository. |
| 43 | | -** |
| 44 | | -** ETAG_CONFIG config The reply is the same as long as the repository |
| 45 | | -** config is constant. |
| 46 | | -** |
| 47 | | -** ETAG_DATA data The reply is the same as long as no new artifacts |
| 48 | | -** are added to the repository |
| 49 | | -** |
| 50 | | -** ETAG_COOKIE cookie The reply is the same as long as the display |
| 51 | | -** cookie is unchanged. |
| 52 | | -** |
| 53 | | -** Page generator routines can also invoke etag_require_hash(HASH) where |
| 54 | | -** HASH is some string. In that case, the reply is the same as long as |
| 55 | | -** the hash is the same. |
| 20 | +** An ETag is a hash that encodes attributes which must be the same for |
| 21 | +** the page to continue to be valid. Attributes that might be contained |
| 22 | +** in the ETag include: |
| 23 | +** |
| 24 | +** (1) The mtime on the Fossil executable |
| 25 | +** (2) The last change to the CONFIG table |
| 26 | +** (3) The last change to the EVENT table |
| 27 | +** (4) The value of the display cookie |
| 28 | +** (5) A hash value supplied by the page generator |
| 29 | +** |
| 30 | +** Item (1) is always included in the ETag. The other elements are |
| 31 | +** optional. Because (1) is always included as part of the ETag, all |
| 32 | +** outstanding ETags can be invalidated by touching the fossil executable. |
| 33 | +** |
| 34 | +** A page generator routine invokes etag_check() exactly once, with |
| 35 | +** arguments that indicates which of the above elements to include in the |
| 36 | +** hash. If the request contained an If-None-Match header which matches |
| 37 | +** the generated ETag, then a 304 Not Modified reply is generated and |
| 38 | +** the process exits. In other words, etag_check() never returns. But |
| 39 | +** if there is no If-None_Match header or if the ETag does not match, |
| 40 | +** then etag_check() returns normally. Later, during reply generation, |
| 41 | +** the cgi.c module will invoke etag_tag() to recover the generated tag |
| 42 | +** and include it in the reply header. |
| 56 | 43 | */ |
| 57 | 44 | #include "config.h" |
| 58 | 45 | #include "etag.h" |
| 59 | 46 | |
| 60 | 47 | #if INTERFACE |
| 61 | 48 | /* |
| 62 | 49 | ** Things to monitor |
| 63 | 50 | */ |
| 64 | | -#define ETAG_CONST 0x00 /* Output is independent of database or parameters */ |
| 65 | | -#define ETAG_CONFIG 0x01 /* Output depends on the configuration */ |
| 66 | | -#define ETAG_DATA 0x02 /* Output depends on 'event' table */ |
| 51 | +#define ETAG_CONFIG 0x01 /* Output depends on the CONFIG table */ |
| 52 | +#define ETAG_DATA 0x02 /* Output depends on the EVENT table */ |
| 67 | 53 | #define ETAG_COOKIE 0x04 /* Output depends on a display cookie value */ |
| 68 | 54 | #define ETAG_HASH 0x08 /* Output depends on a hash */ |
| 69 | | -#define ETAG_DYNAMIC 0x10 /* Output is always different */ |
| 70 | 55 | #endif |
| 71 | 56 | |
| 72 | | -/* Set of all etag requirements */ |
| 73 | | -static int mEtag = 0; /* Mask of requirements */ |
| 74 | | -static const char *zEHash = 0; /* Hash value if ETAG_HASH is set */ |
| 75 | | - |
| 76 | | - |
| 77 | | -/* Check an ETag to see if all conditions are valid. If all conditions are |
| 78 | | -** valid, then return true. If any condition is false, return false. |
| 79 | | -*/ |
| 80 | | -static int etag_valid(const char *zTag){ |
| 81 | | - int iKey; |
| 82 | | - char *zCk; |
| 83 | | - int rc; |
| 84 | | - int nTag; |
| 85 | | - if( zTag==0 || zTag[0]<=0 ) return 0; |
| 86 | | - nTag = (int)strlen(zTag); |
| 87 | | - if( zTag[0]=='"' && zTag[nTag-1]=='"' ){ |
| 88 | | - zTag++; |
| 89 | | - nTag -= 2; |
| 90 | | - } |
| 91 | | - iKey = zTag[0] - '0'; |
| 92 | | - zCk = etag_generate(iKey); |
| 93 | | - rc = nTag==(int)strlen(zCk) && strncmp(zCk, zTag, nTag)==0; |
| 94 | | - fossil_free(zCk); |
| 95 | | - if( rc ) mEtag = iKey; |
| 96 | | - return rc; |
| 97 | | -} |
| 57 | +static char zETag[33]; /* The generated ETag */ |
| 58 | +static int iMaxAge = 0; /* The max-age parameter in the reply */ |
| 98 | 59 | |
| 99 | 60 | /* |
| 100 | | -** Check to see if there is an If-None-Match: header that |
| 101 | | -** matches the current etag settings. If there is, then |
| 102 | | -** generate a 304 Not Modified reply. |
| 103 | | -** |
| 104 | | -** This routine exits and does not return if the 304 Not Modified |
| 105 | | -** reply is generated. |
| 106 | | -** |
| 107 | | -** If the etag does not match, the routine returns normally. |
| 61 | +** Generate an ETag |
| 108 | 62 | */ |
| 109 | | -static void etag_check(void){ |
| 110 | | - const char *zETag = P("HTTP_IF_NONE_MATCH"); |
| 111 | | - if( zETag==0 ) return; |
| 112 | | - if( !etag_valid(zETag) ) return; |
| 63 | +void etag_check(unsigned eFlags, const char *zHash){ |
| 64 | + sqlite3_int64 mtime; |
| 65 | + const char *zIfNoneMatch; |
| 66 | + char zBuf[50]; |
| 67 | + assert( zETag[0]==0 ); /* Only call this routine once! */ |
| 68 | + |
| 69 | + iMaxAge = 86400; |
| 70 | + md5sum_init(); |
| 71 | + |
| 72 | + /* Always include the mtime of the executable as part of the hash */ |
| 73 | + mtime = file_mtime(g.nameOfExe, ExtFILE); |
| 74 | + sqlite3_snprintf(sizeof(zBuf),zBuf,"mtime: %lld\n", mtime); |
| 75 | + md5sum_step_text(zBuf, -1); |
| 76 | + |
| 77 | + if( (eFlags & ETAG_HASH)!=0 && zHash ){ |
| 78 | + md5sum_step_text("hash: ", -1); |
| 79 | + md5sum_step_text(zHash, -1); |
| 80 | + md5sum_step_text("\n", 1); |
| 81 | + iMaxAge = 0; |
| 82 | + }else if( eFlags & ETAG_DATA ){ |
| 83 | + int iKey = db_int(0, "SELECT max(rcvid) FROM rcvfrom"); |
| 84 | + sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey); |
| 85 | + md5sum_step_text("data: ", -1); |
| 86 | + md5sum_step_text(zBuf, -1); |
| 87 | + md5sum_step_text("\n", 1); |
| 88 | + iMaxAge = 60; |
| 89 | + }else if( eFlags & ETAG_CONFIG ){ |
| 90 | + int iKey = db_int(0, "SELECT value FROM config WHERE name='cfgcnt'"); |
| 91 | + sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey); |
| 92 | + md5sum_step_text("data: ", -1); |
| 93 | + md5sum_step_text(zBuf, -1); |
| 94 | + md5sum_step_text("\n", 1); |
| 95 | + iMaxAge = 3600; |
| 96 | + } |
| 97 | + |
| 98 | + /* Include the display cookie */ |
| 99 | + if( eFlags & ETAG_COOKIE ){ |
| 100 | + md5sum_step_text("display-cookie: ", -1); |
| 101 | + md5sum_step_text(PD(DISPLAY_SETTINGS_COOKIE,""), -1); |
| 102 | + md5sum_step_text("\n", 1); |
| 103 | + iMaxAge = 0; |
| 104 | + } |
| 105 | + |
| 106 | + /* Generate the ETag */ |
| 107 | + memcpy(zETag, md5sum_finish(0), 33); |
| 108 | + |
| 109 | + /* Check to see if the generated ETag matches If-None-Match and |
| 110 | + ** generate a 304 reply if it does. */ |
| 111 | + zIfNoneMatch = P("HTTP_IF_NONE_MATCH"); |
| 112 | + if( zIfNoneMatch==0 ) return; |
| 113 | + if( strcmp(zIfNoneMatch,zETag)!=0 ) return; |
| 113 | 114 | |
| 114 | 115 | /* If we get this far, it means that the content has |
| 115 | 116 | ** not changed and we can do a 304 reply */ |
| 116 | 117 | cgi_reset_content(); |
| 117 | 118 | cgi_set_status(304, "Not Modified"); |
| 118 | 119 | cgi_reply(); |
| 119 | 120 | fossil_exit(0); |
| 120 | 121 | } |
| 121 | 122 | |
| 122 | | - |
| 123 | | -/* Add one or more new etag requirements. |
| 124 | | -** |
| 125 | | -** Page generator logic invokes one or both of these methods to signal |
| 126 | | -** under what conditions page generation can be skipped |
| 127 | | -** |
| 128 | | -** After each call to these routines, the HTTP_IF_NONE_MATCH cookie |
| 129 | | -** is checked, and if it contains a compatible ETag, then a |
| 130 | | -** 304 Not Modified return is generated and execution aborts. This |
| 131 | | -** routine does not return if the 304 is generated. |
| 132 | | -*/ |
| 133 | | -void etag_require(int code){ |
| 134 | | - if( (mEtag & code)==code ) return; |
| 135 | | - mEtag |= code; |
| 136 | | - etag_check(); |
| 137 | | -} |
| 138 | | -void etag_require_hash(const char *zHash){ |
| 139 | | - if( zHash ){ |
| 140 | | - zEHash = zHash; |
| 141 | | - mEtag = ETAG_HASH; |
| 142 | | - etag_check(); |
| 143 | | - } |
| 144 | | -} |
| 145 | | - |
| 146 | | -/* Return an appropriate max-age. |
| 123 | +/* Return the ETag, if there is one. |
| 124 | +*/ |
| 125 | +const char *etag_tag(void){ |
| 126 | + return zETag; |
| 127 | +} |
| 128 | + |
| 129 | +/* Return the recommended max-age |
| 147 | 130 | */ |
| 148 | 131 | int etag_maxage(void){ |
| 149 | | - if( mEtag ) return 1; |
| 150 | | - return 3600*24; |
| 151 | | -} |
| 152 | | - |
| 153 | | -/* Generate an appropriate ETags value that captures all requirements. |
| 154 | | -** Space is obtained from fossil_malloc(). |
| 155 | | -** |
| 156 | | -** The argument is the mask of attributes to include in the ETag. |
| 157 | | -** If the argument is -1 then whatever mask is found from prior |
| 158 | | -** calls to etag_require() and etag_require_hash() is used. |
| 159 | | -** |
| 160 | | -** Format: |
| 161 | | -** |
| 162 | | -** <mask><exec-mtime>/<data-or-config-key>/<cookie>/<hash> |
| 163 | | -** |
| 164 | | -** The <mask> is a single-character decimal number that is the mask of |
| 165 | | -** all required attributes: |
| 166 | | -** |
| 167 | | -** ETAG_CONFIG: 1 |
| 168 | | -** ETAG_DATA: 2 |
| 169 | | -** ETAG_COOKIE: 4 |
| 170 | | -** ETAG_HASH: 8 |
| 171 | | -** |
| 172 | | -** If ETAG_HASH is present, the others are omitted, so the number is |
| 173 | | -** never greater than 8. |
| 174 | | -** |
| 175 | | -** The <exec-mtime> is the mtime of the Fossil executable. Since this |
| 176 | | -** is part of the ETag, it means that recompiling or just "touch"-ing the |
| 177 | | -** fossil binary is sufficient to invalidate all prior caches. |
| 178 | | -** |
| 179 | | -** The other elements are only present if the appropriate mask bits |
| 180 | | -** appear in the first character. |
| 181 | | -*/ |
| 182 | | -char *etag_generate(int m){ |
| 183 | | - Blob x = BLOB_INITIALIZER; |
| 184 | | - static int mtime = 0; |
| 185 | | - if( m<0 ) m = mEtag; |
| 186 | | - if( m & ETAG_DYNAMIC ) return 0; |
| 187 | | - if( mtime==0 ) mtime = file_mtime(g.nameOfExe, ExtFILE); |
| 188 | | - blob_appendf(&x,"%d%x", m, mtime); |
| 189 | | - if( m & ETAG_HASH ){ |
| 190 | | - blob_appendf(&x, "/%s", zEHash); |
| 191 | | - }else if( m & ETAG_DATA ){ |
| 192 | | - int iKey = db_int(0, "SELECT max(rcvid) FROM rcvfrom"); |
| 193 | | - blob_appendf(&x, "/%x", iKey); |
| 194 | | - }else if( m & ETAG_CONFIG ){ |
| 195 | | - int iKey = db_int(0, "SELECT value FROM config WHERE name='cfgcnt'"); |
| 196 | | - blob_appendf(&x, "/%x", iKey); |
| 197 | | - } |
| 198 | | - if( m & ETAG_COOKIE ){ |
| 199 | | - blob_appendf(&x, "/%s", P(DISPLAY_SETTINGS_COOKIE)); |
| 200 | | - } |
| 201 | | - return blob_str(&x); |
| 202 | | -} |
| 132 | + return iMaxAge; |
| 133 | +} |
| 203 | 134 | |
| 204 | 135 | /* |
| 205 | 136 | ** COMMAND: test-etag |
| 206 | 137 | ** |
| 207 | 138 | ** Usage: fossil test-etag -key KEY-NUMBER -hash HASH |
| | @@ -213,17 +144,15 @@ |
| 213 | 144 | ** 1 ETAG_CONFIG The config table version number |
| 214 | 145 | ** 2 ETAG_DATA The event table version number |
| 215 | 146 | ** 4 ETAG_COOKIE The display cookie |
| 216 | 147 | */ |
| 217 | 148 | void test_etag_cmd(void){ |
| 218 | | - char *zTag; |
| 219 | | - const char *zHash; |
| 149 | + const char *zHash = 0; |
| 220 | 150 | const char *zKey; |
| 151 | + int iKey = 0; |
| 221 | 152 | db_find_and_open_repository(0, 0); |
| 222 | 153 | zKey = find_option("key",0,1); |
| 223 | 154 | zHash = find_option("hash",0,1); |
| 224 | | - if( zKey ) etag_require(atoi(zKey)); |
| 225 | | - if( zHash ) etag_require_hash(zHash); |
| 226 | | - zTag = etag_generate(mEtag); |
| 227 | | - fossil_print("%s\n", zTag); |
| 228 | | - fossil_free(zTag); |
| 155 | + if( zKey ) iKey = atoi(zKey); |
| 156 | + etag_check(iKey, zHash); |
| 157 | + fossil_print("%s\n", etag_tag()); |
| 229 | 158 | } |
| 230 | 159 | |