]> git.ipfire.org Git - thirdparty/squid.git/blobdiff - src/acl/Checklist.cc
SourceFormat Enforcement
[thirdparty/squid.git] / src / acl / Checklist.cc
index 17a203c066674367f37d289753f71f60e34cf9b5..1c6ee95d5dc413399f907a521cc40c3c14ad48e9 100644 (file)
 /*
- * $Id$
+ * Copyright (C) 1996-2017 The Squid Software Foundation and contributors
  *
- * DEBUG: section 28    Access Control
- * AUTHOR: Duane Wessels
- *
- * SQUID Web Proxy Cache          http://www.squid-cache.org/
- * ----------------------------------------------------------
- *
- *  Squid is the result of efforts by numerous individuals from
- *  the Internet community; see the CONTRIBUTORS file for full
- *  details.   Many organizations have provided support for Squid's
- *  development; see the SPONSORS file for full details.  Squid is
- *  Copyrighted (C) 2001 by the Regents of the University of
- *  California; see the COPYRIGHT file for full details.  Squid
- *  incorporates software developed and/or copyrighted by other
- *  sources; see the CREDITS file for full details.
- *
- *  This program is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 2 of the License, or
- *  (at your option) any later version.
- *
- *  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.  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.
- *
- * Copyright (c) 2003, Robert Collins <robertc@squid-cache.org>
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
  */
 
+/* DEBUG: section 28    Access Control */
+
 #include "squid.h"
 #include "acl/Checklist.h"
+#include "acl/Tree.h"
+#include "Debug.h"
+#include "profiler/Profiler.h"
 
-allow_t const &
-ACLChecklist::currentAnswer() const
-{
-    return allow_;
-}
-
-void
-ACLChecklist::currentAnswer(allow_t const newAnswer)
-{
-    allow_ = newAnswer;
-}
+#include <algorithm>
 
-void
-ACLChecklist::check()
+/// common parts of nonBlockingCheck() and resumeNonBlockingCheck()
+bool
+ACLChecklist::prepNonBlocking()
 {
-    if (checking())
-        return;
-
-    /** Deny if no rules present. */
-    currentAnswer(ACCESS_DENIED);
+    assert(accessList);
 
     if (callerGone()) {
-        checkCallback(currentAnswer());
-        return;
+        checkCallback(ACCESS_DUNNO); // the answer does not really matter
+        return false;
     }
 
-    /** The ACL List should NEVER be NULL when calling this method.
-     * Always caller should check for NULL and handle appropriate to its needs first.
-     * We cannot select a sensible default for all callers here. */
-    if (accessList == NULL) {
-        debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
-        currentAnswer(ACCESS_DENIED);
-        checkCallback(currentAnswer());
-        return;
-    }
-
-    /* NOTE: This holds a cbdata reference to the current access_list
-     * entry, not the whole list.
+    /** \par
+     * If the accessList is no longer valid (i.e. its been
+     * freed because of a reconfigure), then bail with ACCESS_DUNNO.
      */
-    while (accessList != NULL) {
-        /** \par
-         * If the _acl_access is no longer valid (i.e. its been
-         * freed because of a reconfigure), then bail on this
-         * access check.  For now, return ACCESS_DENIED.
-         */
-
-        if (!cbdataReferenceValid(accessList)) {
-            cbdataReferenceDone(accessList);
-            debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
-            continue;
-        }
 
-        checking (true);
-        checkAccessList();
-        checking (false);
-
-        if (asyncInProgress()) {
-            return;
-        }
-
-        if (finished()) {
-            /** \par
-             * Either the request is allowed, denied, requires authentication.
-             */
-            debugs(28, 3, "ACLChecklist::check: " << this << " match found, calling back with " << currentAnswer());
-            cbdataReferenceDone(accessList); /* A */
-            checkCallback(currentAnswer());
-            /* From here on in, this may be invalid */
-            return;
-        }
-
-        /*
-         * Reference the next access entry
-         */
-        const acl_access *A = accessList;
-
-        assert (A);
-
-        accessList = cbdataReference(A->next);
-
-        cbdataReferenceDone(A);
+    if (!cbdataReferenceValid(accessList)) {
+        cbdataReferenceDone(accessList);
+        debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
+        checkCallback(ACCESS_DUNNO);
+        return false;
     }
 
-    /** If dropped off the end of the list return inversion of last line allow/deny action. */
-    debugs(28, 3, HERE << this << " NO match found, returning " <<
-           (currentAnswer() != ACCESS_DENIED ? ACCESS_DENIED : ACCESS_ALLOWED));
-
-    checkCallback(currentAnswer() != ACCESS_DENIED ? ACCESS_DENIED : ACCESS_ALLOWED);
-}
-
-bool
-ACLChecklist::asyncInProgress() const
-{
-    return async_;
+    return true;
 }
 
 void
-ACLChecklist::asyncInProgress(bool const newAsync)
+ACLChecklist::completeNonBlocking()
 {
-    assert (!finished() && !(asyncInProgress() && newAsync));
-    async_ = newAsync;
-    debugs(28, 3, "ACLChecklist::asyncInProgress: " << this <<
-           " async set to " << async_);
-}
+    assert(!asyncInProgress());
 
-bool
-ACLChecklist::finished() const
-{
-    return finished_;
+    if (!finished())
+        calcImplicitAnswer();
+
+    cbdataReferenceDone(accessList);
+    checkCallback(currentAnswer());
 }
 
 void
-ACLChecklist::markFinished()
+ACLChecklist::markFinished(const allow_t &finalAnswer, const char *reason)
 {
     assert (!finished() && !asyncInProgress());
     finished_ = true;
-    debugs(28, 3, "ACLChecklist::markFinished: " << this <<
-           " checklist processing finished");
+    allow_ = finalAnswer;
+    debugs(28, 3, HERE << this << " answer " << allow_ << " for " << reason);
 }
 
+/// Called first (and once) by all checks to initialize their state
 void
-ACLChecklist::preCheck()
+ACLChecklist::preCheck(const char *what)
 {
-    debugs(28, 3, "ACLChecklist::preCheck: " << this << " checking '" << accessList->cfgline << "'");
-    /* what is our result on a match? */
-    currentAnswer(accessList->allow);
+    debugs(28, 3, HERE << this << " checking " << what);
+
+    // concurrent checks using the same Checklist are not supported
+    assert(!occupied_);
+    occupied_ = true;
+    asyncLoopDepth_ = 0;
+
+    AclMatchedName = NULL;
+    finished_ = false;
 }
 
-void
-ACLChecklist::checkAccessList()
+bool
+ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child)
 {
-    preCheck();
-    /* does the current AND clause match */
-    matchAclListSlow(accessList->aclList);
+    assert(current && child);
+
+    // Remember the current tree location to prevent "async loop" cases where
+    // the same child node wants to go async more than once.
+    matchLoc_ = Breadcrumb(current, pos);
+    asyncLoopDepth_ = 0;
+
+    // if there are any breadcrumbs left, then follow them on the way down
+    bool result = false;
+    if (matchPath.empty()) {
+        result = child->matches(this);
+    } else {
+        const Breadcrumb top(matchPath.top());
+        assert(child == top.parent);
+        matchPath.pop();
+        result = top.parent->resumeMatchingAt(this, top.position);
+    }
+
+    if (asyncInProgress()) {
+        // We get here for node N that called goAsync() and then, as the call
+        // stack unwinds, for the nodes higher in the ACL tree that led to N.
+        matchPath.push(Breadcrumb(current, pos));
+    } else {
+        asyncLoc_.clear();
+    }
+
+    matchLoc_.clear();
+    return result;
 }
 
-void
-ACLChecklist::checkForAsync()
+bool
+ACLChecklist::goAsync(AsyncState *state)
 {
-    asyncState()->checkForAsync(this);
+    assert(state);
+    assert(!asyncInProgress());
+    assert(matchLoc_.parent);
+
+    // TODO: add a once-in-a-while WARNING about fast directive using slow ACL?
+    if (!asyncCaller_) {
+        debugs(28, 2, this << " a fast-only directive uses a slow ACL!");
+        return false;
+    }
+
+    // TODO: add a once-in-a-while WARNING about async loops?
+    if (matchLoc_ == asyncLoc_) {
+        debugs(28, 2, this << " a slow ACL resumes by going async again! (loop #" << asyncLoopDepth_ << ")");
+        // external_acl_type may cause async auth lookup plus its own async check
+        // which has the appearance of a loop. Allow some retries.
+        // TODO: make it configurable and check BH retry attempts vs this check?
+        if (asyncLoopDepth_ > 5)
+            return false;
+    }
+
+    asyncLoc_ = matchLoc_; // prevent async loops
+    ++asyncLoopDepth_;
+
+    asyncStage_ = asyncStarting;
+    changeState(state);
+    state->checkForAsync(this); // this is supposed to go async
+
+    // Did AsyncState object actually go async? If not, tell the caller.
+    if (asyncStage_ != asyncStarting) {
+        assert(asyncStage_ == asyncFailed);
+        asyncStage_ = asyncNone; // sanity restored
+        return false;
+    }
+
+    // yes, we must pause until the async callback calls resumeNonBlockingCheck
+    asyncStage_ = asyncRunning;
+    return true;
 }
 
 // ACLFilledChecklist overwrites this to unclock something before we
@@ -183,7 +158,7 @@ ACLChecklist::checkForAsync()
 void
 ACLChecklist::checkCallback(allow_t answer)
 {
-    PF *callback_;
+    ACLCB *callback_;
     void *cbdata_;
     debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer);
 
@@ -193,82 +168,23 @@ ACLChecklist::checkCallback(allow_t answer)
     if (cbdataReferenceValidDone(callback_data, &cbdata_))
         callback_(answer, cbdata_);
 
-    delete this;
-}
-
-void
-ACLChecklist::matchAclListSlow(const ACLList * list)
-{
-    matchAclList(list, false);
-}
-
-void
-ACLChecklist::matchAclList(const ACLList * head, bool const fast)
-{
-    PROF_start(aclMatchAclList);
-    const ACLList *node = head;
-
-    finished_ = false;
-
-    while (node) {
-        bool nodeMatched = node->matches(this);
-
-        if (fast)
-            changeState(NullState::Instance());
-
-        if (finished()) {
-            PROF_stop(aclMatchAclList);
-            return;
-        }
-
-        if (!nodeMatched || state_ != NullState::Instance()) {
-            debugs(28, 3, "aclmatchAclList: " << this << " returning false (AND list entry failed to match)");
-
-            bool async = state_ != NullState::Instance();
-
-            checkForAsync();
-
-            bool async_in_progress = asyncInProgress();
-            debugs(28, 3, "aclmatchAclList: async=" << (async ? 1 : 0) <<
-                   " nodeMatched=" << (nodeMatched ? 1 : 0) <<
-                   " async_in_progress=" << (async_in_progress ? 1 : 0) <<
-                   " lastACLResult() = " << (lastACLResult() ? 1 : 0) <<
-                   " finished() = " << finished());
-
-            if (finished()) {
-                PROF_stop(aclMatchAclList);
-                return;
-            }
+    // not really meaningful just before delete, but here for completeness sake
+    occupied_ = false;
 
-            if (async && nodeMatched && !asyncInProgress() && lastACLResult()) {
-                // async acl, but using cached response, and it was a match
-                node = node->next;
-                continue;
-            }
-
-            PROF_stop(aclMatchAclList);
-
-            return;
-        }
-
-        node = node->next;
-    }
-
-    debugs(28, 3, "aclmatchAclList: " << this << " returning true (AND list satisfied)");
-
-    markFinished();
-    PROF_stop(aclMatchAclList);
+    delete this;
 }
 
 ACLChecklist::ACLChecklist() :
-        accessList (NULL),
-        callback (NULL),
-        callback_data (NULL),
-        async_(false),
-        finished_(false),
-        allow_(ACCESS_DENIED),
-        state_(NullState::Instance()),
-        lastACLResult_(false)
+    accessList (NULL),
+    callback (NULL),
+    callback_data (NULL),
+    asyncCaller_(false),
+    occupied_(false),
+    finished_(false),
+    allow_(ACCESS_DENIED),
+    asyncStage_(asyncNone),
+    state_(NullState::Instance()),
+    asyncLoopDepth_(0)
 {
 }
 
@@ -276,18 +192,11 @@ ACLChecklist::~ACLChecklist()
 {
     assert (!asyncInProgress());
 
-    cbdataReferenceDone(accessList);
+    changeAcl(nullptr);
 
     debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this);
 }
 
-
-void
-ACLChecklist::AsyncState::changeState (ACLChecklist *checklist, AsyncState *newState) const
-{
-    checklist->changeState(newState);
-}
-
 ACLChecklist::NullState *
 ACLChecklist::NullState::Instance()
 {
@@ -296,7 +205,9 @@ ACLChecklist::NullState::Instance()
 
 void
 ACLChecklist::NullState::checkForAsync(ACLChecklist *) const
-{}
+{
+    assert(false); // or the Checklist will never get out of the async state
+}
 
 ACLChecklist::NullState ACLChecklist::NullState::_instance;
 
@@ -324,62 +235,153 @@ ACLChecklist::asyncState() const
  * NP: this should probably be made Async now.
  */
 void
-ACLChecklist::nonBlockingCheck(PF * callback_, void *callback_data_)
+ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_)
 {
+    preCheck("slow rules");
     callback = callback_;
     callback_data = cbdataReference(callback_data_);
-    check();
+    asyncCaller_ = true;
+
+    /** The ACL List should NEVER be NULL when calling this method.
+     * Always caller should check for NULL and handle appropriate to its needs first.
+     * We cannot select a sensible default for all callers here. */
+    if (accessList == NULL) {
+        debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
+        checkCallback(ACCESS_DUNNO);
+        return;
+    }
+
+    if (prepNonBlocking()) {
+        matchAndFinish(); // calls markFinished() on success
+        if (!asyncInProgress())
+            completeNonBlocking();
+    } // else checkCallback() has been called
+}
+
+void
+ACLChecklist::resumeNonBlockingCheck(AsyncState *state)
+{
+    assert(asyncState() == state);
+    changeState(NullState::Instance());
+
+    if (asyncStage_ == asyncStarting) { // oops, we did not really go async
+        asyncStage_ = asyncFailed; // goAsync() checks for that
+        // Do not fall through to resume checks from the async callback. Let
+        // the still-pending(!) goAsync() notice and notify its caller instead.
+        return;
+    }
+    assert(asyncStage_ == asyncRunning);
+    asyncStage_ = asyncNone;
+
+    assert(!matchPath.empty());
+
+    if (!prepNonBlocking())
+        return; // checkCallback() has been called
+
+    if (!finished())
+        matchAndFinish();
+
+    if (asyncInProgress())
+        assert(!matchPath.empty()); // we have breadcrumbs to resume matching
+    else
+        completeNonBlocking();
+}
+
+/// performs (or resumes) an ACL tree match and, if successful, sets the action
+void
+ACLChecklist::matchAndFinish()
+{
+    bool result = false;
+    if (matchPath.empty()) {
+        result = accessList->matches(this);
+    } else {
+        const Breadcrumb top(matchPath.top());
+        matchPath.pop();
+        result = top.parent->resumeMatchingAt(this, top.position);
+    }
+
+    if (result) // the entire tree matched
+        markFinished(accessList->winningAction(), "match");
+}
+
+allow_t const &
+ACLChecklist::fastCheck(const Acl::Tree * list)
+{
+    PROF_start(aclCheckFast);
+
+    preCheck("fast ACLs");
+    asyncCaller_ = false;
+
+    // Concurrent checks are not supported, but sequential checks are, and they
+    // may use a mixture of fastCheck(void) and fastCheck(list) calls.
+    const Acl::Tree * const savedList = changeAcl(list);
+
+    // assume DENY/ALLOW on mis/matches due to action-free accessList
+    // matchAndFinish() takes care of the ALLOW case
+    if (accessList && cbdataReferenceValid(accessList))
+        matchAndFinish(); // calls markFinished() on success
+    if (!finished())
+        markFinished(ACCESS_DENIED, "ACLs failed to match");
+
+    changeAcl(savedList);
+    occupied_ = false;
+    PROF_stop(aclCheckFast);
+    return currentAnswer();
 }
 
 /* Warning: do not cbdata lock this here - it
  * may be static or on the stack
  */
-int
+allow_t const &
 ACLChecklist::fastCheck()
 {
     PROF_start(aclCheckFast);
-    currentAnswer(ACCESS_DENIED);
-    debugs(28, 5, "aclCheckFast: list: " << accessList);
 
-    while (accessList) {
-        preCheck();
-        matchAclListFast(accessList->aclList);
+    preCheck("fast rules");
+    asyncCaller_ = false;
+
+    debugs(28, 5, "aclCheckFast: list: " << accessList);
+    const Acl::Tree *acl = cbdataReference(accessList);
+    if (acl != NULL && cbdataReferenceValid(acl)) {
+        matchAndFinish(); // calls markFinished() on success
 
+        // if finished (on a match or in exceptional cases), stop
         if (finished()) {
+            cbdataReferenceDone(acl);
+            occupied_ = false;
             PROF_stop(aclCheckFast);
-            cbdataReferenceDone(accessList);
-            return currentAnswer() == ACCESS_ALLOWED;
+            return currentAnswer();
         }
 
-        /*
-         * Reference the next access entry
-         */
-        const acl_access *A = accessList;
-
-        assert (A);
-
-        accessList = cbdataReference(A->next);
-
-        cbdataReferenceDone(A);
+        // fall through for mismatch handling
     }
 
-    debugs(28, 5, "aclCheckFast: no matches, returning: " << (currentAnswer() == ACCESS_DENIED));
-
+    // There were no rules to match or no rules matched
+    calcImplicitAnswer();
+    cbdataReferenceDone(acl);
+    occupied_ = false;
     PROF_stop(aclCheckFast);
-    return currentAnswer() == ACCESS_DENIED;
-}
 
-
-bool
-ACLChecklist::checking() const
-{
-    return checking_;
+    return currentAnswer();
 }
 
+/// When no rules matched, the answer is the inversion of the last rule
+/// action (or ACCESS_DUNNO if the reversal is not possible).
 void
-ACLChecklist::checking (bool const newValue)
+ACLChecklist::calcImplicitAnswer()
 {
-    checking_ = newValue;
+    const allow_t lastAction = (accessList && cbdataReferenceValid(accessList)) ?
+                               accessList->lastAction() : allow_t(ACCESS_DUNNO);
+    allow_t implicitRuleAnswer = ACCESS_DUNNO;
+    if (lastAction == ACCESS_DENIED) // reverse last seen "deny"
+        implicitRuleAnswer = ACCESS_ALLOWED;
+    else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow"
+        implicitRuleAnswer = ACCESS_DENIED;
+    // else we saw no rules and will respond with ACCESS_DUNNO
+
+    debugs(28, 3, HERE << this << " NO match found, last action " <<
+           lastAction << " so returning " << implicitRuleAnswer);
+    markFinished(implicitRuleAnswer, "implicit rule won");
 }
 
 bool
@@ -389,10 +391,16 @@ ACLChecklist::callerGone()
 }
 
 bool
-ACLChecklist::matchAclListFast(const ACLList * list)
+ACLChecklist::bannedAction(const allow_t &action) const
 {
-    matchAclList(list, true);
-    return finished();
+    const bool found = std::find(bannedActions_.begin(), bannedActions_.end(), action) != bannedActions_.end();
+    debugs(28, 5, "Action '" << action << "/" << action.kind << (found ? "' is " : "' is not") << " banned");
+    return found;
 }
 
+void
+ACLChecklist::banAction(const allow_t &action)
+{
+    bannedActions_.push_back(action);
+}