13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
+
-
|
** drh@hwaci.com
** http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file implements ETags: cache control for Fossil
**
** Each ETag value is a text string that represents a sequence of conditionals
** like this:
** 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:
**
** if( executable-has-change ) return;
** if( database-has-changed ) return;
** if( display-cookie-"n"-attribute-has-changes ) return;
** Output "304 Not Modified" message and abort;
** (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
**
** In other words, if all conditions specified by the ETag are met, then
** Fossil will return a 304 and avoid doing all the work, and all of the
** bandwidth, associating with regenerating the whole page.
**
** To make use of this feature, page generators can invoke the
** etag_require() interface with mask of ETAG_CONST, ETAG_CONFIG,
** ETAG_DATA, and/or ETAG_COOKIE. Or it can invoke etag_require_hash()
** with some kind of text hash.
**
** Or, in the WEBPAGE: line for the page generator, extra arguments
** (5) A hash value supplied by the page generator
** can be added. "const", "config", "data", and/or "cookie"
**
** ETAG_CONST const The reply is always the same for the same
** build of the fossil binary. The content
** 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.
** is independent of the repository.
**
** ETAG_CONFIG config The reply is the same as long as the repository
** config is constant.
**
** ETAG_DATA data The reply is the same as long as no new artifacts
** are added to the repository
**
** ETAG_COOKIE cookie The reply is the same as long as the display
** cookie is unchanged.
**
** Page generator routines can also invoke etag_require_hash(HASH) where
** HASH is some string. In that case, the reply is the same as long as
** the hash is the same.
** 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.
*/
#include "config.h"
#include "etag.h"
#if INTERFACE
/*
** Things to monitor
*/
#define ETAG_CONST 0x00 /* Output is independent of database or parameters */
#define ETAG_CONFIG 0x01 /* Output depends on the configuration */
#define ETAG_DATA 0x02 /* Output depends on 'event' table */
#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_DYNAMIC 0x10 /* Output is always different */
#endif
/* Set of all etag requirements */
static int mEtag = 0; /* Mask of requirements */
static const char *zEHash = 0; /* Hash value if ETAG_HASH is set */
static char zETag[33]; /* The generated ETag */
static int iMaxAge = 0; /* The max-age parameter in the reply */
/* Check an ETag to see if all conditions are valid. If all conditions are
** valid, then return true. If any condition is false, return false.
*/
static int etag_valid(const char *zTag){
int iKey;
char *zCk;
int rc;
int nTag;
if( zTag==0 || zTag[0]<=0 ) return 0;
nTag = (int)strlen(zTag);
if( zTag[0]=='"' && zTag[nTag-1]=='"' ){
zTag++;
nTag -= 2;
}
iKey = zTag[0] - '0';
zCk = etag_generate(iKey);
rc = nTag==(int)strlen(zCk) && strncmp(zCk, zTag, nTag)==0;
fossil_free(zCk);
if( rc ) mEtag = iKey;
return rc;
}
/*
** Check to see if there is an If-None-Match: header that
** matches the current etag settings. If there is, then
** generate a 304 Not Modified reply.
**
** This routine exits and does not return if the 304 Not Modified
** reply is generated.
** Generate an ETag
**
** If the etag does not match, the routine returns normally.
*/
static void etag_check(void){
const char *zETag = P("HTTP_IF_NONE_MATCH");
if( zETag==0 ) return;
if( !etag_valid(zETag) ) return;
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();
fossil_exit(0);
}
/* Return the ETag, if there is one.
/* Add one or more new etag requirements.
**
** Page generator logic invokes one or both of these methods to signal
** under what conditions page generation can be skipped
**
** After each call to these routines, the HTTP_IF_NONE_MATCH cookie
** is checked, and if it contains a compatible ETag, then a
** 304 Not Modified return is generated and execution aborts. This
** routine does not return if the 304 is generated.
*/
void etag_require(int code){
if( (mEtag & code)==code ) return;
mEtag |= code;
etag_check();
}
void etag_require_hash(const char *zHash){
if( zHash ){
const char *etag_tag(void){
return zETag;
zEHash = zHash;
mEtag = ETAG_HASH;
etag_check();
}
}
}
/* Return the recommended max-age
/* Return an appropriate max-age.
*/
int etag_maxage(void){
if( mEtag ) return 1;
return 3600*24;
}
return iMaxAge;
}
/* Generate an appropriate ETags value that captures all requirements.
** Space is obtained from fossil_malloc().
**
** The argument is the mask of attributes to include in the ETag.
** If the argument is -1 then whatever mask is found from prior
** calls to etag_require() and etag_require_hash() is used.
**
** Format:
**
** <mask><exec-mtime>/<data-or-config-key>/<cookie>/<hash>
**
** The <mask> is a single-character decimal number that is the mask of
** all required attributes:
**
** ETAG_CONFIG: 1
** ETAG_DATA: 2
** ETAG_COOKIE: 4
** ETAG_HASH: 8
**
** If ETAG_HASH is present, the others are omitted, so the number is
** never greater than 8.
**
** The <exec-mtime> is the mtime of the Fossil executable. Since this
** is part of the ETag, it means that recompiling or just "touch"-ing the
** fossil binary is sufficient to invalidate all prior caches.
**
** The other elements are only present if the appropriate mask bits
** appear in the first character.
*/
char *etag_generate(int m){
Blob x = BLOB_INITIALIZER;
static int mtime = 0;
if( m<0 ) m = mEtag;
if( m & ETAG_DYNAMIC ) return 0;
if( mtime==0 ) mtime = file_mtime(g.nameOfExe, ExtFILE);
blob_appendf(&x,"%d%x", m, mtime);
if( m & ETAG_HASH ){
blob_appendf(&x, "/%s", zEHash);
}else if( m & ETAG_DATA ){
int iKey = db_int(0, "SELECT max(rcvid) FROM rcvfrom");
blob_appendf(&x, "/%x", iKey);
}else if( m & ETAG_CONFIG ){
int iKey = db_int(0, "SELECT value FROM config WHERE name='cfgcnt'");
blob_appendf(&x, "/%x", iKey);
}
if( m & ETAG_COOKIE ){
blob_appendf(&x, "/%s", P(DISPLAY_SETTINGS_COOKIE));
}
return blob_str(&x);
}
/*
** 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){
char *zTag;
const char *zHash;
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 ) etag_require(atoi(zKey));
if( zHash ) etag_require_hash(zHash);
if( zKey ) iKey = atoi(zKey);
etag_check(iKey, zHash);
zTag = etag_generate(mEtag);
fossil_print("%s\n", zTag);
fossil_print("%s\n", etag_tag());
fossil_free(zTag);
}
|