Fossil

Check-in [5b84cab0]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:ETags now working for the /uv page.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | etags-cache-control
Files: files | file ages | folders
SHA3-256: 5b84cab0d6d0dd8ac6820d68e7c76e55e025747f9fed7211aaa01485cc7be4bd
User & Date: drh 2018-02-24 03:38:07.398
Context
2018-02-24
03:47
Optimizations to the ETag implementation. ... (check-in: 2588d447 user: drh tags: etags-cache-control)
03:38
ETags now working for the /uv page. ... (check-in: 5b84cab0 user: drh tags: etags-cache-control)
00:39
First cut at supporting ETags: and If-None-Match: for cache control. ... (check-in: 94c0b8ec user: drh tags: etags-cache-control)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/doc.c.
637
638
639
640
641
642
643
644



645
646
647
648

649
650
651
652
653
654
655
          goto doc_not_found;
        }
      }else{
        goto doc_not_found;
      }
    }
    if( isUV ){
      if( db_table_exists("repository","unversioned")



       && unversioned_content(zName, &filebody)==0
      ){
        rid = 1;
        zDfltTitle = zName;

      }
    }else if( fossil_strcmp(zCheckin,"ckout")==0 ){
      /* Read from the local checkout */
      char *zFullpath;
      db_must_be_within_tree();
      zFullpath = mprintf("%s/%s", g.zLocalRoot, zName);
      if( file_isfile(zFullpath, RepoFILE)







|
>
>
>
|
<
|
|
>







637
638
639
640
641
642
643
644
645
646
647
648

649
650
651
652
653
654
655
656
657
658
          goto doc_not_found;
        }
      }else{
        goto doc_not_found;
      }
    }
    if( isUV ){
      if( db_table_exists("repository","unversioned") ){
        char *zHash;
        zHash = db_text(0, "SELECT hash FROM unversioned WHERE name=%Q",zName);
        etag_require_hash(zHash);
        if( unversioned_content(zName, &filebody)==0 ){

          rid = 1;
          zDfltTitle = zName;
        }
      }
    }else if( fossil_strcmp(zCheckin,"ckout")==0 ){
      /* Read from the local checkout */
      char *zFullpath;
      db_must_be_within_tree();
      zFullpath = mprintf("%s/%s", g.zLocalRoot, zName);
      if( file_isfile(zFullpath, RepoFILE)
Changes to src/etag.c.
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
**    if( database-has-changed ) return;
**    if( display-cookie-"n"-attribute-has-changes ) return;
**    Output "304 Not Modified" message and abort;
**
** 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.

























*/
#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_COOKIE   0x04 /* Output depends on a display cookie value */

#define ETAG_DYNAMIC  0x08 /* Output is always different */
#endif

/* Set of all etag requirements */
static int mEtag = 0;

















































/* Add one or more new etag requirements */









void etag_require(int code){
  mEtag |= code;

}








/* Return an appropriate max-age */

int etag_maxage(void){
  if( mEtag ) return 1;
  return 3600*24;
}

/* Generate an appropriate ETags value that captures all requirements.
** Space is obtained from fossil_malloc().


























*/
char *etag_generate(int m){
  Blob x = BLOB_INITIALIZER;
  int mtime;
  if( m<0 ) m = mEtag;
  if( m & ETAG_DYNAMIC ) return 0;
  mtime = file_mtime(g.nameOfExe, ExtFILE);
  blob_appendf(&x,"%d%x", m, mtime);
  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










*/
void test_etag_cmd(void){
  int iKey;
  char *zTag;


  db_find_and_open_repository(0, 0);
  iKey = g.argc>2 ? atoi(g.argv[2]) : 0;



  zTag = etag_generate(iKey);
  fossil_print("%s\n", zTag);
  fossil_free(zTag);
}

/* Check an ETag to see if all conditions are valid.  If all conditions are
** valid, then return true.
*/
int etag_valid(const char *zTag){
  int iKey;
  char *zCk;
  int rc;
  int nTag;
  if( zTag==0 || zTag[0]<=0 || zTag[0]>=5 ) 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;
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>












>
|



|
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>


>

>
>
>
>
>
|
>
>
|
>







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








|
>
>














>
>
>
>
>
>
>
>
>
>


<

>
>

|
>
>
>
|



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
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






















**    if( database-has-changed ) return;
**    if( display-cookie-"n"-attribute-has-changes ) return;
**    Output "304 Not Modified" message and abort;
**
** 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
** 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
**                         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.
*/
#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_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 */


/* 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.
**
** 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;

  /* 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);
}


/* 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){
  mEtag |= code;
  etag_check();
}
void etag_require_hash(const char *zHash){
  if( zHash ){
    zEHash = zHash;
    mEtag = ETAG_HASH;
    etag_check();
  }
}

/* Return an appropriate max-age.
*/
int etag_maxage(void){
  if( mEtag ) return 1;
  return 3600*24;
}

/* 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;
  int mtime;
  if( m<0 ) m = mEtag;
  if( m & ETAG_DYNAMIC ) return 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 *zKey;
  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);
  zTag = etag_generate(mEtag);
  fossil_print("%s\n", zTag);
  fossil_free(zTag);
}






















Changes to src/main.c.
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
      strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
    /* Special case:  If the content mimetype shows that it is "fossil sync"
    ** payload, then pretend that the PATH_INFO is /xfer so that we always
    ** invoke the sync page. */
    zPathInfo = "/xfer";
  }

  /* Check for an ETAG line
  */
  zETag = P("HTTP_IF_NONE_MATCH");
  if( etag_valid(zETag) ){
    cgi_set_status(304, "Not Modified");
    cgi_reply();
    return;
  }

  /* Use the first element of PATH_INFO as the page name
  ** and deliver the appropriate page back to the user.
  */
  set_base_url(0);
  if( zPathInfo==0 || zPathInfo[0]==0
      || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
    /* Second special case: If the PATH_INFO is blank, issue a redirect to







<
<
<
<
<
<
<
<
<







1627
1628
1629
1630
1631
1632
1633









1634
1635
1636
1637
1638
1639
1640
      strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
    /* Special case:  If the content mimetype shows that it is "fossil sync"
    ** payload, then pretend that the PATH_INFO is /xfer so that we always
    ** invoke the sync page. */
    zPathInfo = "/xfer";
  }










  /* Use the first element of PATH_INFO as the page name
  ** and deliver the appropriate page back to the user.
  */
  set_base_url(0);
  if( zPathInfo==0 || zPathInfo[0]==0
      || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
    /* Second special case: If the PATH_INFO is blank, issue a redirect to
Changes to src/mkindex.c.
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
#include <assert.h>
#include <string.h>

/***************************************************************************
** These macros must match similar macros in dispatch.c.
**
** Allowed values for CmdOrPage.eCmdFlags. */
#define CMDFLAG_1ST_TIER    0x0001      /* Most important commands */
#define CMDFLAG_2ND_TIER    0x0002      /* Obscure and seldom used commands */
#define CMDFLAG_TEST        0x0004      /* Commands for testing only */
#define CMDFLAG_WEBPAGE     0x0008      /* Web pages */
#define CMDFLAG_COMMAND     0x0010      /* A command */
#define CMDFLAG_SETTING     0x0020      /* A setting */
#define CMDFLAG_VERSIONABLE 0x0040      /* A versionable setting */
#define CMDFLAG_BLOCKTEXT   0x0080      /* Multi-line text setting */
#define CMDFLAG_BOOLEAN     0x0100      /* A boolean setting */
#define CMDFLAG_CONST       0x0000      /* ETAG_CONST */
#define CMDFLAG_CONFIG      0x1000      /* ETAG_CONFIG */
#define CMDFLAG_DATA        0x2000      /* ETAG_DATA */

#define CMDFLAG_DYNAMIC     0x8000      /* ETAG_DYNAMIC - on by default */
#define CMDFLAG_ETAG        0xf000      /* Mask of all ETAG entries */
#define CMDFLAG_TO_ETAG(X)  ((X)>>12)
/**************************************************************************/

/*
** Each entry looks like this:
*/
typedef struct Entry {







|
|
|
|
|
|
|
|
|
|
|
|
>
|
|







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
#include <assert.h>
#include <string.h>

/***************************************************************************
** These macros must match similar macros in dispatch.c.
**
** Allowed values for CmdOrPage.eCmdFlags. */
#define CMDFLAG_1ST_TIER    0x00001      /* Most important commands */
#define CMDFLAG_2ND_TIER    0x00002      /* Obscure and seldom used commands */
#define CMDFLAG_TEST        0x00004      /* Commands for testing only */
#define CMDFLAG_WEBPAGE     0x00008      /* Web pages */
#define CMDFLAG_COMMAND     0x00010      /* A command */
#define CMDFLAG_SETTING     0x00020      /* A setting */
#define CMDFLAG_VERSIONABLE 0x00040      /* A versionable setting */
#define CMDFLAG_BLOCKTEXT   0x00080      /* Multi-line text setting */
#define CMDFLAG_BOOLEAN     0x00100      /* A boolean setting */
#define CMDFLAG_CONST       0x00000      /* ETAG_CONST */
#define CMDFLAG_CONFIG      0x01000      /* ETAG_CONFIG */
#define CMDFLAG_DATA        0x02000      /* ETAG_DATA */
#define CMDFLAG_COOKIE      0x04000      /* ETAG_COOKIE */
#define CMDFLAG_DYNAMIC     0x10000      /* ETAG_DYNAMIC - on by default */
#define CMDFLAG_ETAG        0x1f000      /* Mask of all ETAG entries */
#define CMDFLAG_TO_ETAG(X)  ((X)>>12)
/**************************************************************************/

/*
** Each entry looks like this:
*/
typedef struct Entry {
249
250
251
252
253
254
255



256
257
258
259
260
261
262
      aEntry[nUsed].eType |= CMDFLAG_CONST;
    }else if( j==6 && strncmp(&zLine[i], "config", j)==0 ){
      aEntry[nUsed].eType &= ~CMDFLAG_ETAG;
      aEntry[nUsed].eType |= CMDFLAG_CONFIG;
    }else if( j==4 && strncmp(&zLine[i], "data", j)==0 ){
      aEntry[nUsed].eType &= ~CMDFLAG_ETAG;
      aEntry[nUsed].eType |= CMDFLAG_DATA;



    }else if( j==7 && strncmp(&zLine[i], "boolean", j)==0 ){
      aEntry[nUsed].eType &= ~(CMDFLAG_BLOCKTEXT);
      aEntry[nUsed].iWidth = 0;
      aEntry[nUsed].eType |= CMDFLAG_BOOLEAN;
    }else if( j==10 && strncmp(&zLine[i], "block-text", j)==0 ){
      aEntry[nUsed].eType &= ~(CMDFLAG_BOOLEAN);
      aEntry[nUsed].eType |= CMDFLAG_BLOCKTEXT;







>
>
>







250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
      aEntry[nUsed].eType |= CMDFLAG_CONST;
    }else if( j==6 && strncmp(&zLine[i], "config", j)==0 ){
      aEntry[nUsed].eType &= ~CMDFLAG_ETAG;
      aEntry[nUsed].eType |= CMDFLAG_CONFIG;
    }else if( j==4 && strncmp(&zLine[i], "data", j)==0 ){
      aEntry[nUsed].eType &= ~CMDFLAG_ETAG;
      aEntry[nUsed].eType |= CMDFLAG_DATA;
    }else if( j==4 && strncmp(&zLine[i], "cookie", j)==0 ){
      aEntry[nUsed].eType &= ~CMDFLAG_ETAG;
      aEntry[nUsed].eType |= CMDFLAG_COOKIE;
    }else if( j==7 && strncmp(&zLine[i], "boolean", j)==0 ){
      aEntry[nUsed].eType &= ~(CMDFLAG_BLOCKTEXT);
      aEntry[nUsed].iWidth = 0;
      aEntry[nUsed].eType |= CMDFLAG_BOOLEAN;
    }else if( j==10 && strncmp(&zLine[i], "block-text", j)==0 ){
      aEntry[nUsed].eType &= ~(CMDFLAG_BOOLEAN);
      aEntry[nUsed].eType |= CMDFLAG_BLOCKTEXT;
Changes to src/unversioned.c.
151
152
153
154
155
156
157
158




159
160
161
162
163
164
165
**    0:     zName does not exist in the unversioned table.
**    1:     zName exists and should be replaced by the mtime/zHash remote.
**    2:     zName exists and is the same as zHash but has a older mtime
**    3:     zName exists and is identical to mtime/zHash in all respects.
**    4:     zName exists and is the same as zHash but has a newer mtime.
**    5:     zName exists and should override the mtime/zHash remote.
*/
int unversioned_status(const char *zName, sqlite3_int64 mtime, const char *zHash){




  int iStatus = 0;
  Stmt q;
  db_prepare(&q, "SELECT mtime, hash FROM unversioned WHERE name=%Q", zName);
  if( db_step(&q)==SQLITE_ROW ){
    const char *zLocalHash = db_column_text(&q, 1);
    int hashCmp;
    sqlite3_int64 iLocalMtime = db_column_int64(&q, 0);







|
>
>
>
>







151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
**    0:     zName does not exist in the unversioned table.
**    1:     zName exists and should be replaced by the mtime/zHash remote.
**    2:     zName exists and is the same as zHash but has a older mtime
**    3:     zName exists and is identical to mtime/zHash in all respects.
**    4:     zName exists and is the same as zHash but has a newer mtime.
**    5:     zName exists and should override the mtime/zHash remote.
*/
int unversioned_status(
  const char *zName,
  sqlite3_int64 mtime,
  const char *zHash
){
  int iStatus = 0;
  Stmt q;
  db_prepare(&q, "SELECT mtime, hash FROM unversioned WHERE name=%Q", zName);
  if( db_step(&q)==SQLITE_ROW ){
    const char *zLocalHash = db_column_text(&q, 1);
    int hashCmp;
    sqlite3_int64 iLocalMtime = db_column_int64(&q, 0);