1
2
3
4
5
6
7
8
9
10
11
12
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
| /*
** 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();
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, exeMtime;
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<=0 || x>mtime ) return;
#if 0
/* If the Fossil executable is more recent than If-Modified-Since,
** go ahead and regenerate the resource. */
exeMtime = file_mtime(g.nameOfExe, ExtFILE);
if( exeMtime>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();
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());
}
|