]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Author: Alex Rousskov <rousskov@measurement-factory.com>
authorAmos Jeffries <squid3@treenet.co.nz>
Mon, 20 Dec 2010 13:37:07 +0000 (06:37 -0700)
committerAmos Jeffries <squid3@treenet.co.nz>
Mon, 20 Dec 2010 13:37:07 +0000 (06:37 -0700)
Author: Dmitry Kurochkin <dmitry.kurochkin@measurement-factory.com>
Bug 427: 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

errors/Makefile.am
errors/templates/ERR_PRECONDITION_FAILED [new file with mode: 0644]
src/HttpRequest.cc
src/HttpRequest.h
src/Store.h
src/client_side_reply.cc
src/client_side_reply.h
src/enums.h
src/store.cc

index fff6bb75f41fd87b70c1f71c83cdaa0e18e42d51..d2f73f57dba86b8cb2129566939ff166f71ea9e0 100644 (file)
@@ -36,6 +36,7 @@ ERROR_TEMPLATES =  \
        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 \
diff --git a/errors/templates/ERR_PRECONDITION_FAILED b/errors/templates/ERR_PRECONDITION_FAILED
new file mode 100644 (file)
index 0000000..c99e336
--- /dev/null
@@ -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>
index b0615b9849a221e16ed4275f4b7013a75b583055..3071f15e0d2520a23af5dea1920c39da4efdf292 100644 (file)
@@ -570,6 +570,14 @@ HttpRequest::cacheable() const
     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);
index ed5ea1db7de295b28431f54b933be0bc9ec484ee..1c1bb3da234be00bd19afaec5301accfb0c90f17 100644 (file)
@@ -85,6 +85,8 @@ public:
     /* are responses to this request potentially cachable */
     bool cacheable() const;
 
+    bool conditional() const; ///< has at least one recognized If-* header
+
     /* 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.        */
index 6b360a4b7c46c1b2355cc224ef6d8d4f363ffe2f..f117d27c0eeff5fcd38b5e2db6fa30aa0f78c087 100644 (file)
@@ -123,6 +123,10 @@ public:
 
     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;
@@ -189,6 +193,7 @@ private:
     static MemAllocator *pool;
 
     bool validLength() const;
+    bool hasOneOfEtags(const String &reqETags, const bool allowWeakMatch) const;
 };
 
 /// \ingroup StoreAPI
index e7a9957b907c32a802243b61b70f85ff9b6a1cc9..e4bad4d0216697cdc6fc1dbcc43f3105034b07eb 100644 (file)
@@ -558,41 +558,9 @@ clientReplyContext::cacheHit(StoreIOBuffer result)
             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
          */
@@ -710,6 +678,72 @@ clientReplyContext::processOnlyIfCachedMiss()
     startError(err);
 }
 
+/// process conditional request from client
+void
+clientReplyContext::processConditional(StoreIOBuffer &result)
+{
+    StoreEntry *const 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 304 Not Modified or 412 Precondition Failed
+            sendNotModifiedOrPreconditionFailedError();
+            return;
+        }
+
+        // otherwise check IMS below to decide if we reply with 304 or 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 304 Not Modified or
+            // 412 Precondition Failed
+            sendNotModifiedOrPreconditionFailedError();
+            return;
+        }
+
+        // otherwise reply with 304 Not Modified
+        sendNotModified();
+    }
+}
+
 void
 clientReplyContext::purgeRequestFindObjectToPurge()
 {
@@ -1781,6 +1815,55 @@ clientReplyContext::sendBodyTooLargeError()
 
 }
 
+/// 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);
+}
+
+/// send 304 (Not Modified) to client
+void
+clientReplyContext::sendNotModified()
+{
+    StoreEntry *e = http->storeEntry();
+    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();
+}
+
+/// send 304 (Not Modified) or 412 (Precondition Failed) to client
+/// depending on request method
+void
+clientReplyContext::sendNotModifiedOrPreconditionFailedError()
+{
+    if (http->request->method == METHOD_GET ||
+        http->request->method == METHOD_HEAD)
+        sendNotModified();
+    else
+        sendPreconditionFailedError();
+}
+
 void
 clientReplyContext::processReplyAccess ()
 {
index 1d0e827f59c5e36809f705a33930f4fccea965bf..c979fe4e8446a80b2739b0642b54d39750d28c29 100644 (file)
@@ -128,6 +128,7 @@ private:
     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);
@@ -136,6 +137,9 @@ private:
     void purgeAllCached();
 
     void sendBodyTooLargeError();
+    void sendPreconditionFailedError();
+    void sendNotModified();
+    void sendNotModifiedOrPreconditionFailedError();
 
     StoreEntry *old_entry;
     store_client *old_sc;      /* ... for entry to be validated */
index f3b4bfde3357a1a55eda774fe5e26d986c7e9f55..efee9c1bc65cd73318bab51a2c6bdfe4ab2da463 100644 (file)
@@ -82,6 +82,7 @@ typedef enum {
     ERR_FORWARDING_DENIED,
     ERR_NO_RELAY,
     ERR_ZERO_SIZE_OBJECT,
+    ERR_PRECONDITION_FAILED,
     ERR_FTP_DISABLED,
     ERR_FTP_FAILURE,
     ERR_URN_RESOLVE,
index ca877eae5ee29fd5f181944ba9670eb45fc1fd71..73a7705df91351dbcbae935d7c3f2d3b43cf71e2 100644 (file)
@@ -1882,6 +1882,51 @@ StoreEntry::modifiedSince(HttpRequest * request) const
     }
 }
 
+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
 {