/* ** Copyright (c) 2018 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file implements ETags: cache control for Fossil ** ** An ETag is a hash that encodes attributes which must be the same for ** the page to continue to be valid. Attributes that might be contained ** in the ETag include: ** ** (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 ** ** 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. ** ** A page generator routine invokes etag_check() exactly once, with ** arguments that indicates which of the above elements to include in the ** hash. If the request contained an If-None-Match header which matches ** the generated ETag, then a 304 Not Modified reply is generated and ** the process exits. In other words, etag_check() never returns. But ** if there is no If-None_Match header or if the ETag does not match, ** then etag_check() returns normally. Later, during reply generation, ** the cgi.c module will invoke etag_tag() to recover the generated tag ** and include it in the reply header. ** ** 2018-02-25: ** ** Also support Last-Modified: and If-Modified-Since:. The ** etag_last_modified(mtime) API records a timestamp for the page in ** seconds since 1970. This causes a Last-Modified: header to be ** issued in the reply. Or, if the request contained If-Modified-Since: ** and the new mtime is not greater than the mtime associated with ** If-Modified-Since, then a 304 Not Modified reply is generated and ** the etag_last_modified() API never returns. */ #include "config.h" #include "etag.h" #if INTERFACE /* ** Things to monitor */ #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 */ #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){ sqlite3_int64 mtime; const char *zIfNoneMatch; char zBuf[50]; assert( zETag[0]==0 ); /* Only call this routine once! */ iMaxAge = 86400; md5sum_init(); /* 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); if( (eFlags & ETAG_HASH)!=0 && zHash ){ md5sum_step_text("hash: ", -1); md5sum_step_text(zHash, -1); md5sum_step_text("\n", 1); iMaxAge = 0; }else if( eFlags & ETAG_DATA ){ int iKey = db_int(0, "SELECT max(rcvid) FROM rcvfrom"); sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey); md5sum_step_text("data: ", -1); md5sum_step_text(zBuf, -1); md5sum_step_text("\n", 1); iMaxAge = 60; }else if( eFlags & ETAG_CONFIG ){ int iKey = db_int(0, "SELECT value FROM config WHERE name='cfgcnt'"); sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey); md5sum_step_text("data: ", -1); md5sum_step_text(zBuf, -1); md5sum_step_text("\n", 1); iMaxAge = 3600; } /* Include the display cookie */ if( eFlags & ETAG_COOKIE ){ md5sum_step_text("display-cookie: ", -1); md5sum_step_text(PD(DISPLAY_SETTINGS_COOKIE,""), -1); md5sum_step_text("\n", 1); iMaxAge = 0; } /* Generate the ETag */ 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( 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(); cgi_set_status(304, "Not Modified"); cgi_reply(); db_close(0); fossil_exit(0); } /* ** Accept a new Last-Modified time. This routine should be called by ** page generators that know a valid last-modified time. This routine ** might generate a 304 Not Modified reply and exit(), never returning. ** Or, if not, it will cause a Last-Modified: header to be included in the ** reply. */ void etag_last_modified(sqlite3_int64 mtime){ const char *zIfModifiedSince; sqlite3_int64 x; assert( iEtagMtime==0 ); /* Only call this routine once */ assert( mtime>0 ); /* Only call with a valid mtime */ iEtagMtime = mtime; /* Check to see the If-Modified-Since constraint is satisfied */ zIfModifiedSince = P("HTTP_IF_MODIFIED_SINCE"); if( zIfModifiedSince==0 ) return; x = cgi_rfc822_parsedate(zIfModifiedSince); if( xx ) return; #endif /* If we reach this point, it means that the resource has not changed ** and that we should generate a 304 Not Modified reply */ cgi_reset_content(); cgi_set_status(304, "Not Modified"); cgi_reply(); db_close(0); fossil_exit(0); } /* Return the ETag, if there is one. */ const char *etag_tag(void){ return zETag; } /* Return the recommended max-age */ int etag_maxage(void){ return iMaxAge; } /* Return the last-modified time in seconds since 1970. Or return 0 if ** there is no last-modified time. */ sqlite3_int64 etag_mtime(void){ return iEtagMtime; } /* ** COMMAND: test-etag ** ** Usage: fossil test-etag -key KEY-NUMBER -hash HASH ** ** Generate an etag given a KEY-NUMBER and/or a HASH. ** ** KEY-NUMBER is some combination of: ** ** 1 ETAG_CONFIG The config table version number ** 2 ETAG_DATA The event table version number ** 4 ETAG_COOKIE The display cookie */ void test_etag_cmd(void){ const char *zHash = 0; const char *zKey; 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); fossil_print("%s\n", etag_tag()); }