Now I see why the clever and elegant solution to use "Vary: Cookie", as suggested by Joerg, does not fix /uv page expiration after login and logout, and I can also explain the strange differences between the local Fossil built-in web server, and my remote web server.
The local Fossil built-in web server uses the HTTP/1.0 protocol. On my remote web server, Apache automatically upgrades the CGI responses generated by Fossil to HTTP/1.1. HTTP/1.0 does not yet support ETags, but only Last-Modified stamps, and thus web browsers do not include If-None-Match with their requests, but just stick to If-Modified-Since [0]. Interestingly, "Vary: Cookie" works (i.e. /uv pages are expired after login and logout) with Chrome, and looks like it's working with IE and Edge (but in fact they are not caching pages with "Vary: Cookie" at all, mimicking a correct refresh triggered by a cookie update; it may be my browser settings). [0] https://stackoverflow.com/a/28033770 With HTTP/1.1, browsers include If-None-Match with their requests, but "Vary: Cookie" no longer has any effects (i.e. /uv pages are not expired after login and logout). I think that "Vary: Cookie" is intended to work with unconditional HTTP requests: the browser is directed to stick to the expiry date and use the cached page, unless the cookies have changed. But caching works differently with conditional HTTP requests (If-None-Match, If-Modified-Since): pages are always revalidated with the server, whether or not the cookies have changed, and "Vary: Cookie" has no additional effects, here. I've attached a simple PHP script to test this: The script generates two web pages, one to expire after 10 seconds (through a "Cache-Control: max-age=10" HTTP response header), the other to handle a conditional request (through a "Cache-Control: must-revalidate, private" header) that always returns "304 Not Modified" (to simulate the current Fossil "Last-Modified always wins" caching behavior). Repeatedly clicking the "Reload" links causes the first page to refresh every 10 seconds (watch the "Date" and "Cookie" entries). The second page remains unchanged (unless reloaded with Ctrl+F5). Clicking "Update cookie" modifies the test cookie (by JavaScript). Now the first page is refreshed immediately - the effect of "Vary: Cookie". The second page still remains unchanged. That's why /uv pages are not expired after login and logout, even if the login cookie has changed. The browser always revalidates the page, and whether or not Fossil detects an ETag mismatch after login and logout (currently, it doesn't, as the ETag is not "login-time-sensitive"), it is immediately undermined by a Last-Modified match. "Vary: Cookie" can't fix this. I have been using Fossil with the patch to expire /uv web pages whenever a new user is logged-in for a few days, now. With the repository index page set to a /uv page [1], I have a very smooth user experience for login and logout actions: After login, I can immediately see the Admin menu entry and the user name display in the top right corner, without the need to do a "hard" reload (Ctrl+F5) -- exactly the way it was before Fossil supported HTTP caching. My index page has a direct logout link, and when clicked, the Admin menu entry and user name display are again updated immediately: [/login?out | Logout] If the user login state does not change, Fossil sends a "304 Not Modified" response, and the web browser shows the cached page, with the Admin menu entry and the user name display in sync. If there's future plans to use HTTP caching not only for /uv, but also for /doc and /wiki pages, more people may run into the issue that they need to do "hard" reloads after login and logout. I have refactored the patch (attached) to have one single function handle either conditional request, and hide the logic to ensure that ETag mismatches won't be undermined by Last-Modified matches, so it's easier to reuse it for other page generators than /uv, at a later stage. I'm not sure if it's safe to change HTTP/1.0 to HTTP/1.1 for the local Fossil built-in web server (I think it is, as it's likely that it is silently upgraded by most web servers). "Vary: Cookie" was left in place, just in case, as a possible fallback for local HTTP/1.0 servers. Like this, it's easy to test the impact of "Vary: Cookie", as removing the ETAG_CEXP flag from the call to etag_check() changes ETag generation to be "login-time-agnostic", again. I would really like to encourage you to try the patch, and see how this changes the user experience for login and logout actions related to /uv web pages, on local and remote Fossil web servers. Thank you very much --Florian [1] A /uv repository index page can be updated by scripting and replaced for local clones, and unlike with /doc or /wiki, changes never show up in the Fossil timeline and/or file hierarchy. I'm keeping the (identical) index pages for my repositories in a separate meta-repository, so no need to archive their history with each repository individually. ===================== vary-cookie.php ================================== <?php if (isset($_GET['rv'])) { if (isset($_SERVER['HTTP_IF_NONE_MATCH']) || isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { header('Not Modified',true,304); // always hit exit; } else { header('ETag: "caffee"'); header('Last-Modified: Wed, 28 Feb 2018 00:00:00 GMT'); header('Cache-Control: must-revalidate, private'); } } else { header('Expires: '.gmdate('D, d M Y H:i:s',time()+10).' GMT'); header('Cache-Control: public, max-age=10'); } setcookie('test-cookie',@$_COOKIE['test-cookie'] ?: '␢'); header('Vary: Cookie'); echo <<<html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script> window.onload = function() { document.getElementById('u').onclick = function() { var ck = (new Date().getTime()).toString(16).slice(-8); document.cookie = 'test-cookie='+ck; document.getElementById('c').innerHTML = '→ <code>'+ck+'</code>'; }; }; </script> </head> <body> <h2>Test HTTP "Vary: Cookie" Request Header</h2> <ul> <li><a href="{$_SERVER['SCRIPT_NAME']}">Reload:</a> <code>Cache-Control: public, max-age=10</code></li> <li><a href="{$_SERVER['SCRIPT_NAME']}?rv">Reload:</a> <code>Cache-Control: must-revalidate, private</code> → <code>304 Not Modified</code></li> </ul> <ul> <li><a id="u" href="#">Update cookie</a> <span id="c"></span></li> </ul> <h3>Script State:</h3> <ul> html; echo '<li>Mode: <code>'. (isset($_GET['rv']) ? 'must-revalidate' : 'max-age=10'). "</code></li>\n"; echo '<li>Date: <code>'.gmdate('D, d M Y H:i:s')." GMT</code></li>\n"; echo '<li>Cookie: <code>'.@$_COOKIE['test-cookie']."</code></li>\n"; echo "</ul>\n\n<h3>Request Headers:</h3>\n\n<ul>\n"; foreach ($_SERVER as $k=>$v) if (substr($k,0,5)=='HTTP_') echo '<li><code>'.htmlspecialchars($k).': '. htmlspecialchars($v)."</code></li>\n"; echo "</ul>\n\n<h3>Response Headers:</h3>\n\n<ul>\n"; foreach (headers_list() as $k=>$v) echo '<li><code>'.htmlspecialchars($v)."</code></li>\n"; echo <<<html </ul> </body> </html> html; ?> ===================== vary-cookie.php ================================== ===================== Patch for Fossil [a7056e64] ====================== Index: src/cgi.c ================================================================== --- src/cgi.c +++ src/cgi.c @@ -249,11 +249,11 @@ iReplyStatus = 200; zReplyStatus = "OK"; } if( g.fullHttpReply ){ - fprintf(g.httpOut, "HTTP/1.0 %d %s\r\n", iReplyStatus, zReplyStatus); + fprintf(g.httpOut, "HTTP/1.1 %d %s\r\n", iReplyStatus, zReplyStatus); fprintf(g.httpOut, "Date: %s\r\n", cgi_rfc822_datestamp(time(0))); fprintf(g.httpOut, "Connection: close\r\n"); fprintf(g.httpOut, "X-UA-Compatible: IE=edge\r\n"); }else{ fprintf(g.httpOut, "Status: %d %s\r\n", iReplyStatus, zReplyStatus); @@ -269,10 +269,11 @@ fprintf(g.httpOut, "Cache-control: no-cache\r\n"); } if( etag_mtime()>0 ){ fprintf(g.httpOut, "Last-Modified: %s\r\n", cgi_rfc822_datestamp(etag_mtime())); + fprintf(g.httpOut, "Vary: Cookie\r\n"); /* HTTP/1.0 (no ETags) */ } if( blob_size(&extraHeader)>0 ){ fprintf(g.httpOut, "%s", blob_buffer(&extraHeader)); } Index: src/doc.c ================================================================== --- src/doc.c +++ src/doc.c @@ -641,17 +641,21 @@ } } if( isUV ){ if( db_table_exists("repository","unversioned") ){ Stmt q; + char *zHash=0; + sqlite3_int64 mtime=0; db_prepare(&q, "SELECT hash, mtime FROM unversioned" " WHERE name=%Q", zName); if( db_step(&q)==SQLITE_ROW ){ - etag_check(ETAG_HASH, db_column_text(&q,0)); - etag_last_modified(db_column_int64(&q,1)); + zHash = fossil_strdup(db_column_text(&q,0)); + mtime = db_column_int64(&q,1); } db_finalize(&q); + etag_check(ETAG_HASH|ETAG_CEXP, zHash, mtime); + if( zHash ) free(zHash); if( unversioned_content(zName, &filebody)==0 ){ rid = 1; zDfltTitle = zName; } } @@ -847,11 +851,11 @@ */ void logo_page(void){ Blob logo; char *zMime; - etag_check(ETAG_CONFIG, 0); + etag_check(ETAG_CONFIG, 0, 0); zMime = db_get("logo-mimetype", "image/gif"); blob_zero(&logo); db_blob(&logo, "SELECT value FROM config WHERE name='logo-image'"); if( blob_size(&logo)==0 ){ blob_init(&logo, (char*)aLogo, sizeof(aLogo)); @@ -881,11 +885,11 @@ */ void background_page(void){ Blob bgimg; char *zMime; - etag_check(ETAG_CONFIG, 0); + etag_check(ETAG_CONFIG, 0, 0); zMime = db_get("background-mimetype", "image/gif"); blob_zero(&bgimg); db_blob(&bgimg, "SELECT value FROM config WHERE name='background-image'"); if( blob_size(&bgimg)==0 ){ blob_init(&bgimg, (char*)aBackground, sizeof(aBackground)); Index: src/etag.c ================================================================== --- src/etag.c +++ src/etag.c @@ -24,10 +24,11 @@ ** (1) The mtime on the Fossil executable ** (2) The last change to the CONFIG table ** (3) The last change to the EVENT table ** (4) The value of the display cookie ** (5) A hash value supplied by the page generator +** (6) The "user.cexpire" field for logged-in users ** ** Item (1) is always included in the ETag. The other elements are ** optional. Because (1) is always included as part of the ETag, all ** outstanding ETags can be invalidated by touching the fossil executable. ** @@ -60,20 +61,21 @@ */ #define ETAG_CONFIG 0x01 /* Output depends on the CONFIG table */ #define ETAG_DATA 0x02 /* Output depends on the EVENT table */ #define ETAG_COOKIE 0x04 /* Output depends on a display cookie value */ #define ETAG_HASH 0x08 /* Output depends on a hash */ +#define ETAG_CEXP 0x10 /* Output depends on "user.cexpire" */ #endif static char zETag[33]; /* The generated ETag */ static int iMaxAge = 0; /* The max-age parameter in the reply */ static sqlite3_int64 iEtagMtime = 0; /* Last-Modified time */ /* ** Generate an ETag */ -void etag_check(unsigned eFlags, const char *zHash){ +void etag_check(unsigned eFlags, const char *zHash, sqlite3_int64 lmt){ sqlite3_int64 mtime; const char *zIfNoneMatch; char zBuf[50]; assert( zETag[0]==0 ); /* Only call this routine once! */ @@ -82,10 +84,22 @@ /* Always include the mtime of the executable as part of the hash */ mtime = file_mtime(g.nameOfExe, ExtFILE); sqlite3_snprintf(sizeof(zBuf),zBuf,"mtime: %lld\n", mtime); md5sum_step_text(zBuf, -1); + + /* Include "user.cexpire" for logged-in users in the hash */ + if ( (eFlags & ETAG_CEXP)!=0 && g.zLogin ){ + char *zCExp = db_text(0, "SELECT cexpire FROM user WHERE uid=%d", + g.userUid); + if ( zCExp ){ + md5sum_step_text("cexp: ", -1); + md5sum_step_text(zCExp, -1); + md5sum_step_text("\n", 1); + fossil_free(zCExp); + } + } if( (eFlags & ETAG_HASH)!=0 && zHash ){ md5sum_step_text("hash: ", -1); md5sum_step_text(zHash, -1); md5sum_step_text("\n", 1); @@ -118,11 +132,17 @@ memcpy(zETag, md5sum_finish(0), 33); /* Check to see if the generated ETag matches If-None-Match and ** generate a 304 reply if it does. */ zIfNoneMatch = P("HTTP_IF_NONE_MATCH"); - if( zIfNoneMatch==0 ) return; + if( zIfNoneMatch==0 ){ + /* Prevent the If-Modified-Since cache handler to undermine cache + ** misses already cleared by the If-None-Match cache handler, and + ** call it only if there's no If-None-Match request header. */ + if ( lmt ) etag_last_modified(lmt); + return; + } if( strcmp(zIfNoneMatch,zETag)!=0 ) return; /* If we get this far, it means that the content has ** not changed and we can do a 304 reply */ cgi_reset_content(); @@ -203,8 +223,8 @@ int iKey = 0; db_find_and_open_repository(0, 0); zKey = find_option("key",0,1); zHash = find_option("hash",0,1); if( zKey ) iKey = atoi(zKey); - etag_check(iKey, zHash); + etag_check(iKey, zHash, 0); fossil_print("%s\n", etag_tag()); } Index: src/style.c ================================================================== --- src/style.c +++ src/style.c @@ -858,11 +858,11 @@ cgi_set_content_type("text/plain"); } if( zId && (nId = (int)strlen(zId))>=8 && strncmp(zId,MANIFEST_UUID,nId)==0 ){ g.isConst = 1; }else{ - etag_check(0,0); + etag_check(0,0,0); } blob_init(&out, zTxt, -1); cgi_set_content(&out); } Index: src/tar.c ================================================================== --- src/tar.c +++ src/tar.c @@ -772,11 +772,11 @@ blob_appendf(&cacheKey, "/tarball/%z", rid_to_uuid(rid)); blob_appendf(&cacheKey, "/%q", zName); if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude); if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude); zKey = blob_str(&cacheKey); - etag_check(ETAG_HASH, zKey); + etag_check(ETAG_HASH, zKey, 0); if( P("debug")!=0 ){ style_header("Tarball Generator Debug Screen"); @ zName = "%h(zName)"<br /> @ rid = %d(rid)<br /> Index: src/zip.c ================================================================== --- src/zip.c +++ src/zip.c @@ -942,11 +942,11 @@ blob_appendf(&cacheKey, "/%s/%z", g.zPath, rid_to_uuid(rid)); blob_appendf(&cacheKey, "/%q", zName); if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude); if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude); zKey = blob_str(&cacheKey); - etag_check(ETAG_HASH, zKey); + etag_check(ETAG_HASH, zKey, 0); if( P("debug")!=0 ){ style_header("%s Archive Generator Debug Screen", zType); @ zName = "%h(zName)"<br /> @ rid = %d(rid)<br /> ===================== Patch for Fossil [a7056e64] ====================== _______________________________________________ fossil-users mailing list fossil-users@lists.fossil-scm.org http://lists.fossil-scm.org:8080/cgi-bin/mailman/listinfo/fossil-users