HTTP Compliance: Reply with an error if required validation failed

RFC 2616 says that proxy MUST not use stale entries that have s-maxage, proxy-revalidate, or must-revalidate cache-directive.

Add new fail_on_validation_err request flag to store result from refreshCheck(). It is needed to avoid refreshLimits() recalculation in clientReplyContext::handleIMSReply().

Split LOG_TCP_REFRESH_FAIL into LOG_TCP_REFRESH_FAIL_OLD (stale reply sent) and LOG_TCP_REFRESH_FAIL_ERR (error forwarded). Though both logged as TCP_REFRESH_FAIL for backward-compatibility with external scripts, etc.

Co-Advisor test cases:
    test_case/rfc2616/noSrv-hit-must-reval-s-maxage-resp
    test_case/rfc2616/noSrv-hit-must-reval-proxy-revalidate-resp
    test_case/rfc2616/noSrv-hit-must-reval-must-revalidate-resp

-------------

Is it a good idea to log new LOG_TCP_REFRESH_FAIL_OLD (stale reply sent) and LOG_TCP_REFRESH_FAIL_ERR (error forwarded) as "LOG_TCP_REFRESH_FAIL"? There are probably a lot of scripts out there that cannot easily handle new outcomes, but I do not know whether we should be kept hostage to them as the two outcomes are rather different. Will we have to use the old tag forever?

Thank you,

Alex.
HTTP Compliance: Reply with an error if required validation failed.

RFC 2616 says that proxy MUST not use stale entries that have s-maxage,
proxy-revalidate, or must-revalidate cache-directive. 

Add new fail_on_validation_err request flag to store result from
refreshCheck().  It is needed to avoid refreshLimits() recalculation in
clientReplyContext::handleIMSReply().

Split LOG_TCP_REFRESH_FAIL into LOG_TCP_REFRESH_FAIL_OLD (stale reply sent)
and LOG_TCP_REFRESH_FAIL_ERR (error forwarded). Though both logged as
TCP_REFRESH_FAIL for backward-compatibility with external scripts, etc.

Co-Advisor test cases:
    test_case/rfc2616/noSrv-hit-must-reval-s-maxage-resp
    test_case/rfc2616/noSrv-hit-must-reval-proxy-revalidate-resp
    test_case/rfc2616/noSrv-hit-must-reval-must-revalidate-resp

=== modified file 'src/client_side_reply.cc'
--- src/client_side_reply.cc	2010-09-13 02:25:09 +0000
+++ src/client_side_reply.cc	2010-09-19 04:19:39 +0000
@@ -332,83 +332,88 @@ clientReplyContext::handleIMSReply(Store
 {
     if (deleting)
         return;
 
     debugs(88, 3, "handleIMSReply: " << http->storeEntry()->url() << ", " << (long unsigned) result.length << " bytes" );
 
     if (http->storeEntry() == NULL)
         return;
 
     if (result.flags.error && !EBIT_TEST(http->storeEntry()->flags, ENTRY_ABORTED))
         return;
 
     /* update size of the request */
     reqsize = result.length + reqofs;
 
     const http_status status = http->storeEntry()->getReply()->sline.status;
 
     // request to origin was aborted
     if (EBIT_TEST(http->storeEntry()->flags, ENTRY_ABORTED)) {
         debugs(88, 3, "handleIMSReply: request to origin aborted '" << http->storeEntry()->url() << "', sending old entry to client" );
-        http->logType = LOG_TCP_REFRESH_FAIL;
+        http->logType = LOG_TCP_REFRESH_FAIL_OLD;
         sendClientOldEntry();
     }
 
     HttpReply *old_rep = (HttpReply *) old_entry->getReply();
 
     // origin replied 304
     // TODO FIXME: old_rep2 was forcibly unshadowed, used to be old_rep. Are we sure
     //  that the right semantics were preserved?
     if (status == HTTP_NOT_MODIFIED) {
         http->logType = LOG_TCP_REFRESH_UNMODIFIED;
 
         // update headers on existing entry
         HttpReply *old_rep2 = (HttpReply *) old_entry->getReply();
         old_rep2->updateOnNotModified(http->storeEntry()->getReply());
         old_entry->timestampsSet();
 
         // if client sent IMS
 
         if (http->request->flags.ims && !old_entry->modifiedSince(http->request)) {
             // forward the 304 from origin
             debugs(88, 3, "handleIMSReply: origin replied 304, revalidating existing entry and forwarding 304 to client");
             sendClientUpstreamResponse();
         } else {
             // send existing entry, it's still valid
             debugs(88, 3, "handleIMSReply: origin replied 304, revalidating existing entry and sending " <<
                    old_rep2->sline.status << " to client");
             sendClientOldEntry();
         }
     }
 
     // origin replied with a non-error code
     else if (status > HTTP_STATUS_NONE && status < HTTP_INTERNAL_SERVER_ERROR) {
         // forward response from origin
         http->logType = LOG_TCP_REFRESH_MODIFIED;
         debugs(88, 3, "handleIMSReply: origin replied " << status << ", replacing existing entry and forwarding to client");
         sendClientUpstreamResponse();
     }
 
     // origin replied with an error
-    else {
+    else if (http->request->flags.fail_on_validation_err) {
+        http->logType = LOG_TCP_REFRESH_FAIL_ERR;
+        debugs(88, 3, "handleIMSReply: origin replied with error " << status <<
+               ", forwarding to client due to fail_on_validation_err");
+        sendClientUpstreamResponse();
+    } else {
         // ignore and let client have old entry
-        http->logType = LOG_TCP_REFRESH_FAIL;
+        http->logType = LOG_TCP_REFRESH_FAIL_OLD;
         debugs(88, 3, "handleIMSReply: origin replied with error " <<
                status << ", sending old entry (" << old_rep->sline.status << ") to client");
         sendClientOldEntry();
     }
 }
 
 extern "C" CSR clientGetMoreData;
 extern "C" CSD clientReplyDetach;
 
 /**
  * clientReplyContext::cacheHit Should only be called until the HTTP reply headers
  * have been parsed.  Normally this should be a single call, but
  * it might take more than one.  As soon as we have the headers,
  * we hand off to clientSendMoreData, processExpired, or
  * processMiss.
  */
 void
 clientReplyContext::CacheHit(void *data, StoreIOBuffer result)
 {
     clientReplyContext *context = (clientReplyContext *)data;

=== modified file 'src/enums.h'
--- src/enums.h	2010-09-01 08:18:47 +0000
+++ src/enums.h	2010-09-19 04:11:16 +0000
@@ -24,41 +24,42 @@
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
  *
  */
 
 #ifndef SQUID_ENUMS_H
 #define SQUID_ENUMS_H
 
 #include "HttpStatusCode.h"
 
 typedef enum {
     LOG_TAG_NONE,
     LOG_TCP_HIT,
     LOG_TCP_MISS,
     LOG_TCP_REFRESH_UNMODIFIED, // refresh from origin revalidated existing entry
-    LOG_TCP_REFRESH_FAIL,       // refresh from origin failed
+    LOG_TCP_REFRESH_FAIL_OLD,   // refresh from origin failed, stale reply sent
+    LOG_TCP_REFRESH_FAIL_ERR,   // refresh from origin failed, error forwarded
     LOG_TCP_REFRESH_MODIFIED,   // refresh from origin replaced existing entry
     LOG_TCP_CLIENT_REFRESH_MISS,
     LOG_TCP_IMS_HIT,
     LOG_TCP_SWAPFAIL_MISS,
     LOG_TCP_NEGATIVE_HIT,
     LOG_TCP_MEM_HIT,
     LOG_TCP_DENIED,
     LOG_TCP_DENIED_REPLY,
     LOG_TCP_OFFLINE_HIT,
 #if LOG_TCP_REDIRECTS
     LOG_TCP_REDIRECT,
 #endif
     LOG_UDP_HIT,
     LOG_UDP_MISS,
     LOG_UDP_DENIED,
     LOG_UDP_INVALID,
     LOG_UDP_MISS_NOFETCH,
     LOG_ICP_QUERY,
     LOG_TYPE_MAX
 } log_type;

=== modified file 'src/http.cc'
--- src/http.cc	2010-09-14 07:37:38 +0000
+++ src/http.cc	2010-09-19 04:04:53 +0000
@@ -924,43 +924,43 @@ HttpStateData::haveParsedReplyHeaders()
     case -1:
 
 #if USE_HTTP_VIOLATIONS
         if (Config.negativeTtl > 0)
             entry->cacheNegatively();
         else
 #endif
             entry->makePrivate();
 
         break;
 
     default:
         assert(0);
 
         break;
     }
 
 no_cache:
 
     if (!ignoreCacheControl && rep->cache_control) {
-        if (EBIT_TEST(rep->cache_control->mask, CC_PROXY_REVALIDATE))
-            EBIT_SET(entry->flags, ENTRY_REVALIDATE);
-        else if (EBIT_TEST(rep->cache_control->mask, CC_MUST_REVALIDATE))
+        if (EBIT_TEST(rep->cache_control->mask, CC_PROXY_REVALIDATE) ||
+            EBIT_TEST(rep->cache_control->mask, CC_MUST_REVALIDATE) ||
+            EBIT_TEST(rep->cache_control->mask, CC_S_MAXAGE))
             EBIT_SET(entry->flags, ENTRY_REVALIDATE);
     }
 
 #if HEADERS_LOG
     headersLog(1, 0, request->method, rep);
 
 #endif
 
     ctx_exit(ctx);
 }
 
 HttpStateData::ConnectionStatus
 HttpStateData::statusIfComplete() const
 {
     const HttpReply *rep = virginReply();
     /** \par
      * If the reply wants to close the connection, it takes precedence */
 
     if (httpHeaderHasConnDir(&rep->header, "close"))
         return COMPLETE_NONPERSISTENT_MSG;

=== modified file 'src/log/access_log.cc'
--- src/log/access_log.cc	2010-09-01 08:18:47 +0000
+++ src/log/access_log.cc	2010-09-19 03:56:40 +0000
@@ -56,41 +56,42 @@
 
 static void accessLogSquid(AccessLogEntry * al, Logfile * logfile);
 static void accessLogCommon(AccessLogEntry * al, Logfile * logfile);
 static void accessLogCustom(AccessLogEntry * al, customlog * log);
 #if HEADERS_LOG
 static Logfile *headerslog = NULL;
 #endif
 
 #if MULTICAST_MISS_STREAM
 static int mcast_miss_fd = -1;
 
 static struct sockaddr_in mcast_miss_to;
 static void mcast_encode(unsigned int *, size_t, const unsigned int *);
 #endif
 
 const char *log_tags[] = {
     "NONE",
     "TCP_HIT",
     "TCP_MISS",
     "TCP_REFRESH_UNMODIFIED",
-    "TCP_REFRESH_FAIL",
+    "TCP_REFRESH_FAIL", // same tag logged for LOG_TCP_REFRESH_FAIL_OLD and
+    "TCP_REFRESH_FAIL", // LOG_TCP_REFRESH_FAIL_ERR for backward-compatibility
     "TCP_REFRESH_MODIFIED",
     "TCP_CLIENT_REFRESH_MISS",
     "TCP_IMS_HIT",
     "TCP_SWAPFAIL_MISS",
     "TCP_NEGATIVE_HIT",
     "TCP_MEM_HIT",
     "TCP_DENIED",
     "TCP_DENIED_REPLY",
     "TCP_OFFLINE_HIT",
 #if LOG_TCP_REDIRECTS
     "TCP_REDIRECT",
 #endif
     "UDP_HIT",
     "UDP_MISS",
     "UDP_DENIED",
     "UDP_INVALID",
     "UDP_MISS_NOFETCH",
     "ICP_QUERY",
     "LOG_TYPE_MAX"
 };
@@ -2482,37 +2483,37 @@ accessLogFreeMemory(AccessLogEntry * aLo
 
     HTTPMSGUNLOCK(aLogEntry->reply);
     HTTPMSGUNLOCK(aLogEntry->request);
 #if ICAP_CLIENT
     HTTPMSGUNLOCK(aLogEntry->icap.reply);
     HTTPMSGUNLOCK(aLogEntry->icap.request);
 #endif
 }
 
 int
 logTypeIsATcpHit(log_type code)
 {
     /* this should be a bitmap for better optimization */
 
     if (code == LOG_TCP_HIT)
         return 1;
 
     if (code == LOG_TCP_IMS_HIT)
         return 1;
 
-    if (code == LOG_TCP_REFRESH_FAIL)
+    if (code == LOG_TCP_REFRESH_FAIL_OLD)
         return 1;
 
     if (code == LOG_TCP_REFRESH_UNMODIFIED)
         return 1;
 
     if (code == LOG_TCP_NEGATIVE_HIT)
         return 1;
 
     if (code == LOG_TCP_MEM_HIT)
         return 1;
 
     if (code == LOG_TCP_OFFLINE_HIT)
         return 1;
 
     return 0;
 }

=== modified file 'src/refresh.cc'
--- src/refresh.cc	2010-05-14 09:47:03 +0000
+++ src/refresh.cc	2010-09-19 04:04:53 +0000
@@ -260,40 +260,41 @@ refreshCheck(const StoreEntry * entry, H
 
     debugs(22, 3, "Staleness = " << staleness);
 
     debugs(22, 3, "refreshCheck: Matched '" << R->pattern << " " <<
            (int) R->min << " " << (int) (100.0 * R->pct) << "%% " <<
            (int) R->max << "'");
 
 
     debugs(22, 3, "refreshCheck: age = " << age);
 
     debugs(22, 3, "\tcheck_time:\t" << mkrfc1123(check_time));
 
     debugs(22, 3, "\tentry->timestamp:\t" << mkrfc1123(entry->timestamp));
 
     if (EBIT_TEST(entry->flags, ENTRY_REVALIDATE) && staleness > -1
 #if USE_HTTP_VIOLATIONS
             && !R->flags.ignore_must_revalidate
 #endif
        ) {
         debugs(22, 3, "refreshCheck: YES: Must revalidate stale response");
+        request->flags.fail_on_validation_err = 1;
         return STALE_MUST_REVALIDATE;
     }
 
     /* request-specific checks */
     if (request && !request->flags.ignore_cc) {
         HttpHdrCc *cc = request->cache_control;
 
         if (request->flags.ims && (R->flags.refresh_ims || Config.onoff.refresh_all_ims)) {
             /* The clients no-cache header is changed into a IMS query */
             debugs(22, 3, "refreshCheck: YES: refresh-ims");
             return STALE_FORCED_RELOAD;
         }
 
 #if USE_HTTP_VIOLATIONS
 
         if (!request->flags.nocache_hack) {
             (void) 0;
         } else if (R->flags.ignore_reload) {
             /* The clients no-cache header is ignored */
             debugs(22, 3, "refreshCheck: MAYBE: ignore-reload");

=== modified file 'src/structs.h'
--- src/structs.h	2010-09-14 07:37:38 +0000
+++ src/structs.h	2010-09-19 04:04:53 +0000
@@ -991,62 +991,63 @@ struct _netdbEntry {
     int n_peers;
 };
 
 struct _iostats {
 
     enum { histSize = 16 };
 
     struct {
         int reads;
         int reads_deferred;
         int read_hist[histSize];
         int writes;
         int write_hist[histSize];
     }
 
     Http, Ftp, Gopher;
 };
 
 
 struct request_flags {
-    request_flags(): range(0),nocache(0),ims(0),auth(0),cachable(0),hierarchical(0),loopdetect(0),proxy_keepalive(0),proxying(0),refresh(0),redirected(0),need_validation(0),accelerated(0),ignore_cc(0),intercepted(0),spoof_client_ip(0),internal(0),internalclient(0),must_keepalive(0),chunked_reply(0),stream_error(0),destinationIPLookedUp_(0) {
+    request_flags(): range(0),nocache(0),ims(0),auth(0),cachable(0),hierarchical(0),loopdetect(0),proxy_keepalive(0),proxying(0),refresh(0),redirected(0),need_validation(0),fail_on_validation_err(0),accelerated(0),ignore_cc(0),intercepted(0),spoof_client_ip(0),internal(0),internalclient(0),must_keepalive(0),chunked_reply(0),stream_error(0),destinationIPLookedUp_(0) {
 #if USE_HTTP_VIOLATIONS
         nocache_hack = 0;
 #endif
 #if FOLLOW_X_FORWARDED_FOR
         done_follow_x_forwarded_for = 0;
 #endif /* FOLLOW_X_FORWARDED_FOR */
     }
 
     unsigned int range:1;
     unsigned int nocache:1;
     unsigned int ims:1;
     unsigned int auth:1;
     unsigned int cachable:1;
     unsigned int hierarchical:1;
     unsigned int loopdetect:1;
     unsigned int proxy_keepalive:1;
 unsigned int proxying:
     1;	/* this should be killed, also in httpstateflags */
     unsigned int refresh:1;
     unsigned int redirected:1;
     unsigned int need_validation:1;
+    unsigned int fail_on_validation_err:1; ///< whether we should fail if validation fails
 #if USE_HTTP_VIOLATIONS
     unsigned int nocache_hack:1;	/* for changing/ignoring no-cache requests */
 #endif
     unsigned int accelerated:1;
     unsigned int ignore_cc:1;
     unsigned int intercepted:1;  /**< transparently intercepted request */
     unsigned int spoof_client_ip:1;  /**< spoof client ip if possible */
     unsigned int internal:1;
     unsigned int internalclient:1;
     unsigned int must_keepalive:1;
     unsigned int connection_auth:1; /** Request wants connection oriented auth */
     unsigned int connection_auth_disabled:1; /** Connection oriented auth can not be supported */
     unsigned int connection_proxy_auth:1; /** Request wants connection oriented auth */
     unsigned int pinned:1;      /* Request sent on a pinned connection */
     unsigned int auth_sent:1;   /* Authentication forwarded */
     unsigned int no_direct:1;	/* Deny direct forwarding unless overriden by always_direct. Used in accelerator mode */
     unsigned int chunked_reply:1; /**< Reply with chunked transfer encoding */
     unsigned int stream_error:1; /**< Whether stream error has occured */
 
     // When adding new flags, please update cloneAdaptationImmune() as needed.

Reply via email to