diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/etag.c | |
| download | relic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz | |
Diffstat (limited to 'src/etag.c')
| -rw-r--r-- | src/etag.c | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/src/etag.c b/src/etag.c new file mode 100644 index 0000000..d417ba3 --- /dev/null +++ b/src/etag.c @@ -0,0 +1,288 @@ +/* +** 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 +** (6) The details of the request URI +** (7) The name user as determined by the login cookie +** +** 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 */ +#define ETAG_QUERY 0x10 /* Output depends on PATH_INFO and QUERY_STRING */ + /* and the g.zLogin value */ +#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 */ +static int etagCancelled = 0; /* Never send an etag */ + +/* +** Return a hash that changes every time the Fossil source code is +** rebuilt. +** +** The FOSSIL_BUILD_HASH string that is returned here gets computed by +** the mkversion utility program. The result is a hash of MANIFEST_UUID +** and the unix timestamp for when the mkversion utility program is run. +** +** During development rebuilds, if you need the source code id to change +** in order to invalidate caches, simply "touch" the "manifest" file in +** the top of the source directory prior to running "make" and a new +** FOSSIL_BUILD_HASH will be generated automatically. +*/ +const char *fossil_exe_id(void){ + return FOSSIL_BUILD_HASH; +} + +/* +** Generate an ETag +*/ +void etag_check(unsigned eFlags, const char *zHash){ + const char *zIfNoneMatch; + char zBuf[50]; + const int cchETag = 32; /* Not including NULL terminator. */ + int cch; /* Length of zIfNoneMatch header. */ + assert( zETag[0]==0 ); /* Only call this routine once! */ + + if( etagCancelled ) return; + + /* By default, ETagged URLs never expire since the ETag will change + * when the content changes. Approximate this policy as 10 years. */ + iMaxAge = 10 * 365 * 24 * 60 * 60; + md5sum_init(); + + /* Always include the executable ID as part of the hash */ + md5sum_step_text("exe-id: ", -1); + md5sum_step_text(fossil_exe_id(), -1); + md5sum_step_text("\n", 1); + + if( (eFlags & ETAG_HASH)!=0 && zHash ){ + md5sum_step_text("hash: ", -1); + md5sum_step_text(zHash, -1); + md5sum_step_text("\n", 1); + iMaxAge = 0; + } + 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; + } + 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("config: ", -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; + } + + /* Output depends on PATH_INFO and QUERY_STRING */ + if( eFlags & ETAG_QUERY ){ + const char *zQS = P("QUERY_STRING"); + md5sum_step_text("query: ", -1); + md5sum_step_text(PD("PATH_INFO",""), -1); + if( zQS ){ + md5sum_step_text("?", 1); + md5sum_step_text(zQS, -1); + } + md5sum_step_text("\n",1); + if( g.zLogin ){ + md5sum_step_text("login: ", -1); + md5sum_step_text(g.zLogin, -1); + md5sum_step_text("\n", 1); + } + } + + /* Generate the ETag */ + memcpy(zETag, md5sum_finish(0), cchETag+1); + + /* Check to see if the generated ETag matches If-None-Match and + ** generate a 304 reply if it does. Test both with and without + ** double quotes. */ + zIfNoneMatch = P("HTTP_IF_NONE_MATCH"); + if( zIfNoneMatch==0 ) return; + cch = strlen(zIfNoneMatch); + if( cch==cchETag+2 && zIfNoneMatch[0]=='"' && zIfNoneMatch[cch-1]=='"' ){ + if( memcmp(&zIfNoneMatch[1],zETag,cchETag)!=0 ) return; + }else{ + 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); +} + +/* +** If the output is determined purely by hash parameter and the hash +** is long enough to be invariant, then set the g.isConst flag, indicating +** that the output will never change. +*/ +void etag_check_for_invariant_name(const char *zHash){ + size_t nHash = strlen(zHash); + if( nHash<HNAME_MIN ){ + return; /* Name is too short */ + } + if( !validate16(zHash, (int)nHash) ){ + return; /* Name is not pure hex */ + } + g.isConst = 1; /* A long hex identifier must be a unique hash */ +} + +/* +** 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( x<mtime ) return; + +#if 0 + /* If the Fossil executable is more recent than If-Modified-Since, + ** go ahead and regenerate the resource. */ + if( file_mtime(g.nameOfExe, ExtFILE)>x ) 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 g.isConst ? "" : 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()); +} + +/* +** Cancel the ETag. +*/ +void etag_cancel(void){ + etagCancelled = 1; + zETag[0] = 0; +} |
