HTTP Compliance: Support If-Match and If-None-Match requests.

Add support for If-Match and If-None-Match headers as described in RFC 2616
(sections 14.24 and 14.26 in particular).

Moved IMS handling from clientReplyContext::cacheHit() to
clientReplyContext::processConditional() while preserving the original IMS
logic, except for the case when a request has both IMS and If-None-Match.

Co-Advisors test cases:
    test_clause/rfc2616/ifMatch-mismatch-strong
    test_clause/rfc2616/ifMatch-mismatch-weak
    test_clause/rfc2616/ifNoneMatch-match-imsNone
    and many more

=== modified file 'errors/Makefile.am'
--- errors/Makefile.am	2010-08-23 02:21:19 +0000
+++ errors/Makefile.am	2010-10-01 16:49:56 +0000
@@ -19,40 +19,41 @@ ERROR_TEMPLATES =  \
 	templates/ERR_CONNECT_FAIL \
 	templates/ERR_DIR_LISTING \
 	templates/ERR_DNS_FAIL \
 	templates/ERR_ESI \
 	templates/ERR_FORWARDING_DENIED \
 	templates/ERR_FTP_DISABLED \
 	templates/ERR_FTP_FAILURE \
 	templates/ERR_FTP_FORBIDDEN \
 	templates/ERR_FTP_NOT_FOUND \
 	templates/ERR_FTP_PUT_CREATED \
 	templates/ERR_FTP_PUT_ERROR \
 	templates/ERR_FTP_PUT_MODIFIED \
 	templates/ERR_FTP_UNAVAILABLE \
 	templates/ERR_ICAP_FAILURE \
 	templates/ERR_INVALID_REQ \
 	templates/ERR_INVALID_RESP \
 	templates/ERR_INVALID_URL \
 	templates/ERR_LIFETIME_EXP \
 	templates/ERR_NO_RELAY \
 	templates/ERR_ONLY_IF_CACHED_MISS \
+	templates/ERR_PRECONDITION_FAILED \
 	templates/ERR_READ_ERROR \
 	templates/ERR_READ_TIMEOUT \
 	templates/ERR_SECURE_CONNECT_FAIL \
 	templates/ERR_SHUTTING_DOWN \
 	templates/ERR_SOCKET_FAILURE \
 	templates/ERR_TOO_BIG \
 	templates/ERR_UNSUP_HTTPVERSION \
 	templates/ERR_UNSUP_REQ \
 	templates/ERR_URN_RESOLVE \
 	templates/ERR_WRITE_ERROR \
 	templates/ERR_ZERO_SIZE_OBJECT
 
 TRANSLATE_LANGUAGES = \
 	af.lang \
 	ar.lang \
 	az.lang \
 	bg.lang \
 	ca.lang \
 	cs.lang \
 	da.lang \

=== modified file 'errors/list'
--- errors/list	2009-09-19 09:15:45 +0000
+++ errors/list	2010-10-01 16:49:56 +0000
@@ -5,31 +5,32 @@ ERR_CANNOT_FORWARD
 ERR_CONNECT_FAIL
 ERR_DIR_LISTING
 ERR_DNS_FAIL
 ERR_ESI
 ERR_FORWARDING_DENIED
 ERR_FTP_DISABLED
 ERR_FTP_FAILURE
 ERR_FTP_FORBIDDEN
 ERR_FTP_NOT_FOUND
 ERR_FTP_PUT_CREATED
 ERR_FTP_PUT_ERROR
 ERR_FTP_PUT_MODIFIED
 ERR_FTP_UNAVAILABLE
 ERR_ICAP_FAILURE
 ERR_INVALID_REQ
 ERR_INVALID_RESP
 ERR_INVALID_URL
 ERR_LIFETIME_EXP
 ERR_NO_RELAY
 ERR_ONLY_IF_CACHED_MISS
+ERR_PRECONDITION_FAILED
 ERR_READ_ERROR
 ERR_READ_TIMEOUT
 ERR_SECURE_CONNECT_FAIL
 ERR_SHUTTING_DOWN
 ERR_SOCKET_FAILURE
 ERR_TOO_BIG
 ERR_UNSUP_HTTPVERSION
 ERR_UNSUP_REQ
 ERR_URN_RESOLVE
 ERR_WRITE_ERROR
 ERR_ZERO_SIZE_OBJECT

=== added file 'errors/templates/ERR_PRECONDITION_FAILED'
--- errors/templates/ERR_PRECONDITION_FAILED	1970-01-01 00:00:00 +0000
+++ errors/templates/ERR_PRECONDITION_FAILED	2010-10-01 17:04:18 +0000
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html><head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+<title>ERROR: The requested URL could not be retrieved</title>
+<style type="text/css"><!-- 
+ %l
+
+body
+:lang(fa) { direction: rtl; font-size: 100%; font-family: Tahoma, Roya, sans-serif; float: right; }
+:lang(he) { direction: rtl; float: right; }
+ --></style>
+</head><body>
+<div id="titles">
+<h1>ERROR</h1>
+<h2>The requested URL could not be retrieved</h2>
+</div>
+<hr>
+
+<div id="content">
+<p>The following error was encountered while trying to retrieve the URL: <a href="%U">%U</a></p>
+
+<blockquote id="error">
+<p><b>Precondition Failed.</b></p>
+</blockquote>
+
+<p>This means:</p>
+<blockquote>
+    <p>At least one precondition specified by the HTTP client in the request header has failed.</p>
+</blockquote>
+
+<br>
+</div>
+
+<hr>
+<div id="footer">
+<p>Generated %T by %h (%s)</p>
+<!-- %c -->
+</div>
+</body></html>

=== modified file 'src/HttpRequest.cc'
--- src/HttpRequest.cc	2010-09-12 00:10:47 +0000
+++ src/HttpRequest.cc	2010-10-01 16:49:56 +0000
@@ -557,40 +557,48 @@ HttpRequest::cacheable() const
      * The below looks questionable: what non HTTP protocols use connect,
      * trace, put and post? RC
      */
 
     if (!method.isCacheble())
         return false;
 
     /*
      * XXX POST may be cached sometimes.. ignored
      * for now
      */
     if (protocol == PROTO_GOPHER)
         return gopherCachable(this);
 
     if (protocol == PROTO_CACHEOBJ)
         return false;
 
     return true;
 }
 
+bool
+HttpRequest::conditional() const
+{
+    return flags.ims ||
+        header.has(HDR_IF_MATCH) ||
+        header.has(HDR_IF_NONE_MATCH);
+}
+
 bool HttpRequest::inheritProperties(const HttpMsg *aMsg)
 {
     const HttpRequest* aReq = dynamic_cast<const HttpRequest*>(aMsg);
     if (!aReq)
         return false;
 
     client_addr = aReq->client_addr;
 #if FOLLOW_X_FORWARDED_FOR
     indirect_client_addr = aReq->indirect_client_addr;
 #endif
 #if USE_SQUID_EUI
     client_eui48 = aReq->client_eui48;
     client_eui64 = aReq->client_eui64;
 #endif
     my_addr = aReq->my_addr;
 
     dnsWait = aReq->dnsWait;
 
 #if USE_ADAPTATION
     adaptHistory_ = aReq->adaptHistory();

=== modified file 'src/HttpRequest.h'
--- src/HttpRequest.h	2010-09-11 23:58:15 +0000
+++ src/HttpRequest.h	2010-10-01 17:05:03 +0000
@@ -74,40 +74,42 @@ public:
     typedef HttpMsgPointerT<HttpRequest> Pointer;
 
     MEMPROXY_CLASS(HttpRequest);
     HttpRequest();
     HttpRequest(const HttpRequestMethod& aMethod, protocol_t aProtocol, const char *aUrlpath);
     ~HttpRequest();
     virtual void reset();
 
     // use HTTPMSGLOCK() instead of calling this directly
     virtual HttpRequest *_lock() {
         return static_cast<HttpRequest*>(HttpMsg::_lock());
     };
 
     void initHTTP(const HttpRequestMethod& aMethod, protocol_t aProtocol, const char *aUrlpath);
 
     virtual HttpRequest *clone() const;
 
     /* are responses to this request potentially cachable */
     bool cacheable() const;
 
+    bool conditional() const; ///< has at least one recognized If-* header
+
     /// whether the client is likely to be able to handle a 1xx reply
     bool canHandle1xx() const;
 
     /* Now that we care what host contains it is better off being protected. */
     /* HACK: These two methods are only inline to get around Makefile dependancies */
     /*      caused by HttpRequest being used in places it really shouldn't.        */
     /*      ideally they would be methods of URL instead. */
     inline void SetHost(const char *src) {
         host_addr.SetEmpty();
         host_addr = src;
         if ( host_addr.IsAnyAddr() ) {
             xstrncpy(host, src, SQUIDHOSTNAMELEN);
             host_is_numeric = 0;
         } else {
             host_addr.ToHostname(host, SQUIDHOSTNAMELEN);
             debugs(23, 3, "HttpRequest::SetHost() given IP: " << host_addr);
             host_is_numeric = 1;
         }
     };
     inline const char* GetHost(void) const { return host; };

=== modified file 'src/Store.h'
--- src/Store.h	2009-12-26 00:25:57 +0000
+++ src/Store.h	2010-10-01 17:33:09 +0000
@@ -106,40 +106,44 @@ public:
     int checkNegativeHit() const;
     int locked() const;
     int validToSend() const;
     int keepInMemory() const;
     void createMemObject(const char *, const char *);
     void dump(int debug_lvl) const;
     void hashDelete();
     void hashInsert(const cache_key *);
     void registerAbort(STABH * cb, void *);
     void reset();
     void setMemStatus(mem_status_t);
     void timestampsSet();
     void unregisterAbort();
     void destroyMemObject();
     int checkTooSmall();
 
     void delayAwareRead(int fd, char *buf, int len, AsyncCall::Pointer callback);
 
     void setNoDelay (bool const);
     bool modifiedSince(HttpRequest * request) const;
+    /// has ETag matching at least one of the If-Match etags
+    bool hasIfMatchEtag(const HttpRequest &request) const;
+    /// has ETag matching at least one of the If-None-Match etags
+    bool hasIfNoneMatchEtag(const HttpRequest &request) const;
 
     /** What store does this entry belong too ? */
     virtual RefCount<Store> store() const;
 
     MemObject *mem_obj;
     RemovalPolicyNode repl;
     /* START OF ON-DISK STORE_META_STD TLV field */
     time_t timestamp;
     time_t lastref;
     time_t expires;
     time_t lastmod;
     uint64_t swap_file_sz;
     u_short refcount;
     u_short flags;
     /* END OF ON-DISK STORE_META_STD */
 
     sfileno swap_filen:25;
 
     sdirno swap_dirn:7;
 
@@ -172,40 +176,41 @@ public:
 #endif
     /** append bytes to the buffer */
     virtual void append(char const *, int len);
     /** disable sending content to the clients */
     virtual void buffer();
     /** flush any buffered content */
     virtual void flush();
     /** reduce the memory lock count on the entry */
     virtual int unlock();
     /** increate the memory lock count on the entry */
     virtual int64_t objectLen() const;
     virtual int64_t contentLen() const;
 
     virtual void lock();
     virtual void release();
 
 private:
     static MemAllocator *pool;
 
     bool validLength() const;
+    bool hasOneOfEtags(const String &reqETags, const bool allowWeakMatch) const;
 };
 
 /// \ingroup StoreAPI
 class NullStoreEntry:public StoreEntry
 {
 
 public:
     static NullStoreEntry *getInstance();
     bool isNull() {
         return true;
     }
 
     const char *getMD5Text() const;
     _SQUID_INLINE_ HttpReply const *getReply() const;
     void write (StoreIOBuffer) {}
 
     bool isEmpty () const {return true;}
 
     virtual size_t bytesWanted(Range<size_t> const aRange) const { assert (aRange.size()); return aRange.end - 1;}
 

=== modified file 'src/client_side_reply.cc'
--- src/client_side_reply.cc	2010-09-28 15:20:36 +0000
+++ src/client_side_reply.cc	2010-10-01 19:05:13 +0000
@@ -540,75 +540,43 @@ clientReplyContext::cacheHit(StoreIOBuff
             /*
              * This did not match a refresh pattern that overrides no-cache
              * we should honour the client no-cache header.
              */
             http->logType = LOG_TCP_CLIENT_REFRESH_MISS;
             processMiss();
         } else if (r->protocol == PROTO_HTTP) {
             /*
              * Object needs to be revalidated
              * XXX This could apply to FTP as well, if Last-Modified is known.
              */
             processExpired();
         } else {
             /*
              * We don't know how to re-validate other protocols. Handle
              * them as if the object has expired.
              */
             http->logType = LOG_TCP_MISS;
             processMiss();
         }
-    } else if (r->flags.ims) {
-        /*
-         * Handle If-Modified-Since requests from the client
-         */
-
-        if (e->getReply()->sline.status != HTTP_OK) {
-            debugs(88, 4, "clientCacheHit: Reply code " <<
-                   e->getReply()->sline.status << " != 200");
-            http->logType = LOG_TCP_MISS;
-            processMiss();
-        } else if (e->modifiedSince(http->request)) {
-            http->logType = LOG_TCP_IMS_HIT;
-            sendMoreData(result);
-        } else {
-            time_t const timestamp = e->timestamp;
-            HttpReply *temprep = e->getReply()->make304();
-            http->logType = LOG_TCP_IMS_HIT;
-            removeClientStoreReference(&sc, http);
-            createStoreEntry(http->request->method,
-                             request_flags());
-            e = http->storeEntry();
-            /*
-             * Copy timestamp from the original entry so the 304
-             * reply has a meaningful Age: header.
-             */
-            e->timestamp = timestamp;
-            e->replaceHttpReply(temprep);
-            e->complete();
-            /* TODO: why put this in the store and then serialise it and then parse it again.
-             * Simply mark the request complete in our context and
-             * write the reply struct to the client side
-             */
-            triggerInitialStoreRead();
-        }
-    } else {
+    } else if (r->conditional())
+        processConditional(result);
+    else {
         /*
          * plain ol' cache hit
          */
 
 #if DELAY_POOLS
         if (e->store_status != STORE_OK)
             http->logType = LOG_TCP_MISS;
         else
 #endif
             if (e->mem_status == IN_MEMORY)
                 http->logType = LOG_TCP_MEM_HIT;
             else if (Config.onoff.offline)
                 http->logType = LOG_TCP_OFFLINE_HIT;
 
         sendMoreData(result);
     }
 }
 
 /**
  * Prepare to fetch the object as it's a cache miss of some kind.
@@ -694,40 +662,121 @@ clientReplyContext::processMiss()
 }
 
 /**
  * client issued a request with an only-if-cached cache-control directive;
  * we did not find a cached object that can be returned without
  *     contacting other servers;
  * respond with a 504 (Gateway Timeout) as suggested in [RFC 2068]
  */
 void
 clientReplyContext::processOnlyIfCachedMiss()
 {
     ErrorState *err = NULL;
     debugs(88, 4, "clientProcessOnlyIfCachedMiss: '" <<
            RequestMethodStr(http->request->method) << " " << http->uri << "'");
     http->al.http.code = HTTP_GATEWAY_TIMEOUT;
     err = clientBuildError(ERR_ONLY_IF_CACHED_MISS, HTTP_GATEWAY_TIMEOUT, NULL, http->getConn()->peer, http->request);
     removeClientStoreReference(&sc, http);
     startError(err);
 }
 
+/// process conditional request from client
+void
+clientReplyContext::processConditional(StoreIOBuffer &result)
+{
+    StoreEntry *e = http->storeEntry();
+
+    if (e->getReply()->sline.status != HTTP_OK) {
+        debugs(88, 4, "clientReplyContext::processConditional: Reply code " <<
+               e->getReply()->sline.status << " != 200");
+        http->logType = LOG_TCP_MISS;
+        processMiss();
+        return;
+    }
+
+    HttpRequest &r = *http->request;
+
+    if (r.header.has(HDR_IF_MATCH) && !e->hasIfMatchEtag(r)) {
+        // RFC 2616: reply with 412 Precondition Failed if If-Match did not match
+        sendPreconditionFailedError();
+        return;
+    }
+
+    bool matchedIfNoneMatch = false;
+    if (r.header.has(HDR_IF_NONE_MATCH)) {
+        if (!e->hasIfNoneMatchEtag(r)) {
+            // RFC 2616: ignore IMS if If-None-Match did not match
+            r.flags.ims = 0;
+            r.ims = -1;
+            r.imslen = 0;
+            r.header.delById(HDR_IF_MODIFIED_SINCE);
+            http->logType = LOG_TCP_MISS;
+            sendMoreData(result);
+            return;
+        }
+
+        if (!r.flags.ims) {
+            // RFC 2616: if If-None-Match matched and there is no IMS,
+            // reply with 412 Precondition Failed
+            sendPreconditionFailedError();
+            return;
+        }
+
+        // otherwise check IMS below to decide if we reply with 412
+        matchedIfNoneMatch = true;
+    }
+
+    if (r.flags.ims) {
+        // handle If-Modified-Since requests from the client
+        if (e->modifiedSince(&r)) {
+            http->logType = LOG_TCP_IMS_HIT;
+            sendMoreData(result);
+            return;
+        }
+
+        if (matchedIfNoneMatch) {
+            // If-None-Match matched, reply with 412 Precondition Failed
+            sendPreconditionFailedError();
+            return;
+        }
+
+        // otherwise reply with 304 Not Modified
+        const time_t timestamp = e->timestamp;
+        HttpReply *const temprep = e->getReply()->make304();
+        http->logType = LOG_TCP_IMS_HIT;
+        removeClientStoreReference(&sc, http);
+        createStoreEntry(http->request->method, request_flags());
+        e = http->storeEntry();
+        // Copy timestamp from the original entry so the 304
+        // reply has a meaningful Age: header.
+        e->timestamp = timestamp;
+        e->replaceHttpReply(temprep);
+        e->complete();
+        /*
+         * TODO: why put this in the store and then serialise it and
+         * then parse it again. Simply mark the request complete in
+         * our context and write the reply struct to the client side.
+         */
+        triggerInitialStoreRead();
+    }
+}
+
 void
 clientReplyContext::purgeRequestFindObjectToPurge()
 {
     /* Try to find a base entry */
     http->flags.purging = 1;
     lookingforstore = 1;
 
     // TODO: can we use purgeAllCached() here instead of doing the
     // getPublicByRequestMethod() dance?
     StoreEntry::getPublicByRequestMethod(this, http->request, METHOD_GET);
 }
 
 // Purges all entries with a given url
 // TODO: move to SideAgent parent, when we have one
 /*
  * We probably cannot purge Vary-affected responses because their MD5
  * keys depend on vary headers.
  */
 void
 purgeEntriesByUrl(HttpRequest * req, const char *url)
@@ -1783,40 +1832,53 @@ clientReplyContext::next() const
 {
     assert ( (clientStreamNode*)http->client_stream.head->next->data == getNextNode());
     return getNextNode();
 }
 
 void
 clientReplyContext::sendBodyTooLargeError()
 {
     Ip::Address tmp_noaddr;
     tmp_noaddr.SetNoAddr(); // TODO: make a global const
     http->logType = LOG_TCP_DENIED_REPLY;
     ErrorState *err = clientBuildError(ERR_TOO_BIG, HTTP_FORBIDDEN, NULL,
                                        http->getConn() != NULL ? http->getConn()->peer : tmp_noaddr,
                                        http->request);
     removeClientStoreReference(&(sc), http);
     HTTPMSGUNLOCK(reply);
     startError(err);
 
 }
 
+/// send 412 (Precondition Failed) to client
+void
+clientReplyContext::sendPreconditionFailedError()
+{
+    http->logType = LOG_TCP_HIT;
+    ErrorState *const err =
+        clientBuildError(ERR_PRECONDITION_FAILED, HTTP_PRECONDITION_FAILED,
+                         NULL, http->getConn()->peer, http->request);
+    removeClientStoreReference(&sc, http);
+    HTTPMSGUNLOCK(reply);
+    startError(err);
+}
+
 void
 clientReplyContext::processReplyAccess ()
 {
     /* NP: this should probably soft-fail to a zero-sized-reply error ?? */
     assert(reply);
 
     /** Don't block our own responses or HTTP status messages */
     if (http->logType == LOG_TCP_DENIED ||
             http->logType == LOG_TCP_DENIED_REPLY ||
             alwaysAllowResponse(reply->sline.status)) {
         headers_sz = reply->hdr_sz;
         processReplyAccessResult(1);
         return;
     }
 
     /** Check for reply to big error */
     if (reply->expectedBodyTooLarge(*http->request)) {
         sendBodyTooLargeError();
         return;
     }

=== modified file 'src/client_side_reply.h'
--- src/client_side_reply.h	2010-05-13 06:20:23 +0000
+++ src/client_side_reply.h	2010-10-01 17:09:31 +0000
@@ -111,35 +111,37 @@ public:
     clientStreamNode *ourNode;	/* This will go away if/when this file gets refactored some more */
 
 private:
     CBDATA_CLASS(clientReplyContext);
     clientStreamNode *getNextNode() const;
     void makeThisHead();
     bool errorInStream(StoreIOBuffer const &result, size_t const &sizeToProcess)const ;
     void sendStreamError(StoreIOBuffer const &result);
     void pushStreamData(StoreIOBuffer const &result, char *source);
     clientStreamNode * next() const;
     StoreIOBuffer holdingBuffer;
     HttpReply *reply;
     void processReplyAccess();
     static PF ProcessReplyAccessResult;
     void processReplyAccessResult(bool accessAllowed);
     void cloneReply();
     void buildReplyHeader ();
     bool alwaysAllowResponse(http_status sline) const;
     int checkTransferDone();
     void processOnlyIfCachedMiss();
+    void processConditional(StoreIOBuffer &result);
     void cacheHit(StoreIOBuffer result);
     void handleIMSReply(StoreIOBuffer result);
     void sendMoreData(StoreIOBuffer result);
     void triggerInitialStoreRead();
     void sendClientOldEntry();
     void purgeAllCached();
 
     void sendBodyTooLargeError();
+    void sendPreconditionFailedError();
 
     StoreEntry *old_entry;
     store_client *old_sc;	/* ... for entry to be validated */
     bool deleting;
 };
 
 #endif /* SQUID_CLIENTSIDEREPLY_H */

=== modified file 'src/err_type.h'
--- src/err_type.h	2009-10-31 11:53:09 +0000
+++ src/err_type.h	2010-10-01 16:49:56 +0000
@@ -17,40 +17,41 @@ typedef enum {
     ERR_LIFETIME_EXP,
     ERR_READ_ERROR,
     ERR_WRITE_ERROR,
     ERR_CONNECT_FAIL,
     ERR_SECURE_CONNECT_FAIL,
     ERR_SOCKET_FAILURE,
 
     /* DNS Errors */
     ERR_DNS_FAIL,
     ERR_URN_RESOLVE,
 
     /* HTTP Errors */
     ERR_ONLY_IF_CACHED_MISS,    /* failure to satisfy only-if-cached request */
     ERR_TOO_BIG,
     ERR_INVALID_RESP,
     ERR_UNSUP_HTTPVERSION,     /* HTTP version is not supported */
     ERR_INVALID_REQ,
     ERR_UNSUP_REQ,
     ERR_INVALID_URL,
     ERR_ZERO_SIZE_OBJECT,
+    ERR_PRECONDITION_FAILED,
 
     /* FTP Errors */
     ERR_FTP_DISABLED,
     ERR_FTP_UNAVAILABLE,
     ERR_FTP_FAILURE,
     ERR_FTP_PUT_ERROR,
     ERR_FTP_NOT_FOUND,
     ERR_FTP_FORBIDDEN,
     ERR_FTP_PUT_CREATED,        /* !error,a note that the file was created */
     ERR_FTP_PUT_MODIFIED,       /* modified, !created */
 
     /* ESI Errors */
     ERR_ESI,                    /* Failure to perform ESI processing */
 
     /* ICAP Errors */
     ERR_ICAP_FAILURE,
 
     /* Special Cases */
     ERR_DIR_LISTING,            /* Display of remote directory (FTP, Gopher) */
     ERR_SQUID_SIGNATURE,        /* not really an error */

=== modified file 'src/store.cc'
--- src/store.cc	2010-09-28 15:40:07 +0000
+++ src/store.cc	2010-10-01 18:04:27 +0000
@@ -1887,40 +1887,85 @@ StoreEntry::modifiedSince(HttpRequest *
         object_length = contentLen();
 
     if (mod_time > request->ims) {
         debugs(88, 3, "--> YES: entry newer than client");
         return true;
     } else if (mod_time < request->ims) {
         debugs(88, 3, "-->  NO: entry older than client");
         return false;
     } else if (request->imslen < 0) {
         debugs(88, 3, "-->  NO: same LMT, no client length");
         return false;
     } else if (request->imslen == object_length) {
         debugs(88, 3, "-->  NO: same LMT, same length");
         return false;
     } else {
         debugs(88, 3, "--> YES: same LMT, different length");
         return true;
     }
 }
 
+bool
+StoreEntry::hasIfMatchEtag(const HttpRequest &request) const
+{
+    const String reqETags = request.header.getList(HDR_IF_MATCH);
+    return hasOneOfEtags(reqETags, false);
+}
+
+bool
+StoreEntry::hasIfNoneMatchEtag(const HttpRequest &request) const
+{
+    const String reqETags = request.header.getList(HDR_IF_NONE_MATCH);
+    // weak comparison is allowed only for HEAD or full-body GET requests
+    const bool allowWeakMatch = !request.flags.range &&
+        (request.method == METHOD_GET || request.method == METHOD_HEAD);
+    return hasOneOfEtags(reqETags, allowWeakMatch);
+}
+
+/// whether at least one of the request ETags matches entity ETag
+bool
+StoreEntry::hasOneOfEtags(const String &reqETags, const bool allowWeakMatch) const
+{
+    const ETag repETag = getReply()->header.getETag(HDR_ETAG);
+    if (!repETag.str)
+        return strListIsMember(&reqETags, "*", ',');
+
+    bool matched = false;
+    const char *pos = NULL;
+    const char *item;
+    int ilen;
+    while (!matched && strListGetItem(&reqETags, ',', &item, &ilen, &pos)) {
+        if (!strncmp(item, "*", ilen))
+            matched = true;
+        else {
+            String str;
+            str.append(item, ilen);
+            ETag reqETag;
+            if (etagParseInit(&reqETag, str.termedBuf())) {
+                matched = allowWeakMatch ? etagIsWeakEqual(repETag, reqETag) :
+                    etagIsStrongEqual(repETag, reqETag);
+            }
+        }
+    }
+    return matched;
+}
+
 StorePointer
 StoreEntry::store() const
 {
     assert(0 <= swap_dirn && swap_dirn < Config.cacheSwap.n_configured);
     return INDEXSD(swap_dirn);
 }
 
 void
 StoreEntry::unlink()
 {
     store()->unlink(*this);
 }
 
 /*
  * return true if the entry is in a state where
  * it can accept more data (ie with write() method)
  */
 bool
 StoreEntry::isAccepting() const
 {