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