1 /*
2 ** Copyright (c) 2018 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 **
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file implements ETags: cache control for Fossil
19 **
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 **   (6)  The details of the request URI
30 **   (7)  The name user as determined by the login cookie
31 **
32 ** Item (1) is always included in the ETag.  The other elements are
33 ** optional.  Because (1) is always included as part of the ETag, all
34 ** outstanding ETags can be invalidated by touching the fossil executable.
35 **
36 ** A page generator routine invokes etag_check() exactly once, with
37 ** arguments that indicates which of the above elements to include in the
38 ** hash.  If the request contained an If-None-Match header which matches
39 ** the generated ETag, then a 304 Not Modified reply is generated and
40 ** the process exits.  In other words, etag_check() never returns.  But
41 ** if there is no If-None_Match header or if the ETag does not match,
42 ** then etag_check() returns normally.  Later, during reply generation,
43 ** the cgi.c module will invoke etag_tag() to recover the generated tag
44 ** and include it in the reply header.
45 **
46 ** 2018-02-25:
47 **
48 ** Also support Last-Modified: and If-Modified-Since:.  The
49 ** etag_last_modified(mtime) API records a timestamp for the page in
50 ** seconds since 1970.  This causes a Last-Modified: header to be
51 ** issued in the reply.  Or, if the request contained If-Modified-Since:
52 ** and the new mtime is not greater than the mtime associated with
53 ** If-Modified-Since, then a 304 Not Modified reply is generated and
54 ** the etag_last_modified() API never returns.
55 */
56 #include "config.h"
57 #include "etag.h"
58 
59 #if INTERFACE
60 /*
61 ** Things to monitor
62 */
63 #define ETAG_CONFIG   0x01 /* Output depends on the CONFIG table */
64 #define ETAG_DATA     0x02 /* Output depends on the EVENT table */
65 #define ETAG_COOKIE   0x04 /* Output depends on a display cookie value */
66 #define ETAG_HASH     0x08 /* Output depends on a hash */
67 #define ETAG_QUERY    0x10 /* Output depends on PATH_INFO and QUERY_STRING */
68                            /*   and the g.zLogin value */
69 #endif
70 
71 static char zETag[33];      /* The generated ETag */
72 static int iMaxAge = 0;     /* The max-age parameter in the reply */
73 static sqlite3_int64 iEtagMtime = 0;  /* Last-Modified time */
74 static int etagCancelled = 0;         /* Never send an etag */
75 
76 /*
77 ** Return a hash that changes every time the Fossil source code is
78 ** rebuilt.
79 **
80 ** The FOSSIL_BUILD_HASH string that is returned here gets computed by
81 ** the mkversion utility program.  The result is a hash of MANIFEST_UUID
82 ** and the unix timestamp for when the mkversion utility program is run.
83 **
84 ** During development rebuilds, if you need the source code id to change
85 ** in order to invalidate caches, simply "touch" the "manifest" file in
86 ** the top of the source directory prior to running "make" and a new
87 ** FOSSIL_BUILD_HASH will be generated automatically.
88 */
fossil_exe_id(void)89 const char *fossil_exe_id(void){
90   return FOSSIL_BUILD_HASH;
91 }
92 
93 /*
94 ** Generate an ETag
95 */
etag_check(unsigned eFlags,const char * zHash)96 void etag_check(unsigned eFlags, const char *zHash){
97   const char *zIfNoneMatch;
98   char zBuf[50];
99   assert( zETag[0]==0 );  /* Only call this routine once! */
100 
101   if( etagCancelled ) return;
102 
103   /* By default, ETagged URLs never expire since the ETag will change
104    * when the content changes.  Approximate this policy as 10 years. */
105   iMaxAge = 10 * 365 * 24 * 60 * 60;
106   md5sum_init();
107 
108   /* Always include the executable ID as part of the hash */
109   md5sum_step_text("exe-id: ", -1);
110   md5sum_step_text(fossil_exe_id(), -1);
111   md5sum_step_text("\n", 1);
112 
113   if( (eFlags & ETAG_HASH)!=0 && zHash ){
114     md5sum_step_text("hash: ", -1);
115     md5sum_step_text(zHash, -1);
116     md5sum_step_text("\n", 1);
117     iMaxAge = 0;
118   }
119   if( eFlags & ETAG_DATA ){
120     int iKey = db_int(0, "SELECT max(rcvid) FROM rcvfrom");
121     sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey);
122     md5sum_step_text("data: ", -1);
123     md5sum_step_text(zBuf, -1);
124     md5sum_step_text("\n", 1);
125     iMaxAge = 60;
126   }
127   if( eFlags & ETAG_CONFIG ){
128     int iKey = db_int(0, "SELECT value FROM config WHERE name='cfgcnt'");
129     sqlite3_snprintf(sizeof(zBuf),zBuf,"%d",iKey);
130     md5sum_step_text("config: ", -1);
131     md5sum_step_text(zBuf, -1);
132     md5sum_step_text("\n", 1);
133     iMaxAge = 3600;
134   }
135 
136   /* Include the display cookie */
137   if( eFlags & ETAG_COOKIE ){
138     md5sum_step_text("display-cookie: ", -1);
139     md5sum_step_text(PD(DISPLAY_SETTINGS_COOKIE,""), -1);
140     md5sum_step_text("\n", 1);
141     iMaxAge = 0;
142   }
143 
144   /* Output depends on PATH_INFO and QUERY_STRING */
145   if( eFlags & ETAG_QUERY ){
146     const char *zQS = P("QUERY_STRING");
147     md5sum_step_text("query: ", -1);
148     md5sum_step_text(PD("PATH_INFO",""), -1);
149     if( zQS ){
150       md5sum_step_text("?", 1);
151       md5sum_step_text(zQS, -1);
152     }
153     md5sum_step_text("\n",1);
154     if( g.zLogin ){
155       md5sum_step_text("login: ", -1);
156       md5sum_step_text(g.zLogin, -1);
157       md5sum_step_text("\n", 1);
158     }
159   }
160 
161   /* Generate the ETag */
162   memcpy(zETag, md5sum_finish(0), 33);
163 
164   /* Check to see if the generated ETag matches If-None-Match and
165   ** generate a 304 reply if it does. */
166   zIfNoneMatch = P("HTTP_IF_NONE_MATCH");
167   if( zIfNoneMatch==0 ) return;
168   if( strcmp(zIfNoneMatch,zETag)!=0 ) return;
169 
170   /* If we get this far, it means that the content has
171   ** not changed and we can do a 304 reply */
172   cgi_reset_content();
173   cgi_set_status(304, "Not Modified");
174   cgi_reply();
175   db_close(0);
176   fossil_exit(0);
177 }
178 
179 /*
180 ** If the output is determined purely by hash parameter and the hash
181 ** is long enough to be invariant, then set the g.isConst flag, indicating
182 ** that the output will never change.
183 */
etag_check_for_invariant_name(const char * zHash)184 void etag_check_for_invariant_name(const char *zHash){
185   size_t nHash = strlen(zHash);
186   if( nHash<HNAME_MIN ){
187     return;  /* Name is too short */
188   }
189   if( !validate16(zHash, (int)nHash) ){
190     return;  /* Name is not pure hex */
191   }
192   g.isConst = 1;  /* A long hex identifier must be a unique hash */
193 }
194 
195 /*
196 ** Accept a new Last-Modified time.  This routine should be called by
197 ** page generators that know a valid last-modified time.  This routine
198 ** might generate a 304 Not Modified reply and exit(), never returning.
199 ** Or, if not, it will cause a Last-Modified: header to be included in the
200 ** reply.
201 */
etag_last_modified(sqlite3_int64 mtime)202 void etag_last_modified(sqlite3_int64 mtime){
203   const char *zIfModifiedSince;
204   sqlite3_int64 x;
205   assert( iEtagMtime==0 );   /* Only call this routine once */
206   assert( mtime>0 );         /* Only call with a valid mtime */
207   iEtagMtime = mtime;
208 
209   /* Check to see the If-Modified-Since constraint is satisfied */
210   zIfModifiedSince = P("HTTP_IF_MODIFIED_SINCE");
211   if( zIfModifiedSince==0 ) return;
212   x = cgi_rfc822_parsedate(zIfModifiedSince);
213   if( x<mtime ) return;
214 
215 #if 0
216   /* If the Fossil executable is more recent than If-Modified-Since,
217   ** go ahead and regenerate the resource. */
218   if( file_mtime(g.nameOfExe, ExtFILE)>x ) return;
219 #endif
220 
221   /* If we reach this point, it means that the resource has not changed
222   ** and that we should generate a 304 Not Modified reply */
223   cgi_reset_content();
224   cgi_set_status(304, "Not Modified");
225   cgi_reply();
226   db_close(0);
227   fossil_exit(0);
228 }
229 
230 /* Return the ETag, if there is one.
231 */
etag_tag(void)232 const char *etag_tag(void){
233   return g.isConst ? "" : zETag;
234 }
235 
236 /* Return the recommended max-age
237 */
etag_maxage(void)238 int etag_maxage(void){
239   return iMaxAge;
240 }
241 
242 /* Return the last-modified time in seconds since 1970.  Or return 0 if
243 ** there is no last-modified time.
244 */
etag_mtime(void)245 sqlite3_int64 etag_mtime(void){
246   return iEtagMtime;
247 }
248 
249 /*
250 ** COMMAND: test-etag
251 **
252 ** Usage:  fossil test-etag -key KEY-NUMBER  -hash HASH
253 **
254 ** Generate an etag given a KEY-NUMBER and/or a HASH.
255 **
256 ** KEY-NUMBER is some combination of:
257 **
258 **    1   ETAG_CONFIG   The config table version number
259 **    2   ETAG_DATA     The event table version number
260 **    4   ETAG_COOKIE   The display cookie
261 */
test_etag_cmd(void)262 void test_etag_cmd(void){
263   const char *zHash = 0;
264   const char *zKey;
265   int iKey = 0;
266   db_find_and_open_repository(0, 0);
267   zKey = find_option("key",0,1);
268   zHash = find_option("hash",0,1);
269   if( zKey ) iKey = atoi(zKey);
270   etag_check(iKey, zHash);
271   fossil_print("%s\n", etag_tag());
272 }
273 
274 /*
275 ** Cancel the ETag.
276 */
etag_cancel(void)277 void etag_cancel(void){
278   etagCancelled = 1;
279   zETag[0] = 0;
280 }
281