]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Allow upgrading from HTTP/1.1 to other protocols (#481)
authorChristos Tsantilas <christos@chtsanti.net>
Fri, 29 May 2020 21:26:08 +0000 (21:26 +0000)
committerAmos Jeffries <yadij@users.noreply.github.com>
Thu, 11 Jun 2020 17:14:06 +0000 (05:14 +1200)
Support admin-authorized HTTP Upgrade-driven (RFC 7230 Section 6.7)
protocol switching. The new http_upgrade_request_protocols configuration
directive allows admin to control what client Upgrade offer(s) are
forwarded to the server. Squid does not automatically check whether the
server selection matches one of the client offers, but the admin can
deny unacceptable server responses using http_reply_access.

By default, Squid still does not forward any Upgrade headers,
effectively blocking an upgrade attempt.

Squid itself does not understand the protocols being upgraded to and
participates in the upgraded communication only as a dumb TCP proxy.

This is a Measurement Factory project.

28 files changed:
src/HttpHeaderTools.cc
src/HttpUpgradeProtocolAccess.cc [new file with mode: 0644]
src/HttpUpgradeProtocolAccess.h [new file with mode: 0644]
src/Makefile.am
src/SquidConfig.h
src/StrList.cc
src/StrList.h
src/acl/Acl.h
src/acl/Checklist.cc
src/adaptation/icap/ModXact.cc
src/cache_cf.cc
src/cf.data.depend
src/cf.data.pre
src/client_side.cc
src/client_side.h
src/comm/Connection.cc
src/comm/Connection.h
src/http.cc
src/http.h
src/http/StateFlags.h
src/servers/Http1Server.cc
src/servers/Http1Server.h
src/ssl/PeekingPeerConnector.cc
src/ssl/PeekingPeerConnector.h
src/tests/stub_HttpUpgradeProtocolAccess.cc [new file with mode: 0644]
src/tests/stub_cache_cf.cc
src/tests/stub_tunnel.cc
src/tunnel.cc

index ab84a20a5fc40031d153942d1837213db4e1fd74..86560d09f7e369f961bd5d03603d6ec80c4a4d7f 100644 (file)
@@ -295,6 +295,10 @@ httpHdrMangle(HttpHeaderEntry * e, HttpRequest * request, HeaderManglers *hms, c
         HTTPMSGLOCK(checklist.reply);
     }
 
+    // XXX: The two "It was denied" clauses below mishandle cases with no
+    // matching rules, violating the "If no rules within the set have matching
+    // ACLs, the header field is left as is" promise in squid.conf.
+    // TODO: Use Acl::Answer::implicit. See HttpStateData::forwardUpgrade().
     if (checklist.fastCheck().allowed()) {
         /* aclCheckFast returns true for allow. */
         debugs(66, 7, "checklist for mangler is positive. Mangle");
@@ -302,6 +306,7 @@ httpHdrMangle(HttpHeaderEntry * e, HttpRequest * request, HeaderManglers *hms, c
     } else if (NULL == hm->replacement) {
         /* It was denied, and we don't have any replacement */
         debugs(66, 7, "checklist denied, we have no replacement. Pass");
+        // XXX: We said "Pass", but the caller will delete on zero retval.
         retval = 0;
     } else {
         /* It was denied, but we have a replacement. Replace the
diff --git a/src/HttpUpgradeProtocolAccess.cc b/src/HttpUpgradeProtocolAccess.cc
new file mode 100644 (file)
index 0000000..212119a
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 1996-2019 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+#include "acl/Acl.h"
+#include "acl/Gadgets.h"
+#include "ConfigParser.h"
+#include "globals.h"
+#include "HttpUpgradeProtocolAccess.h"
+#include "sbuf/Stream.h"
+
+#include <algorithm>
+
+const SBuf HttpUpgradeProtocolAccess::ProtoOther("OTHER");
+
+ProtocolView::ProtocolView(const char * const start, const size_t len):
+    ProtocolView(SBuf(start, len))
+{
+}
+
+ProtocolView::ProtocolView(const SBuf &proto):
+    name(proto.substr(0, proto.find('/'))),
+    version(proto.substr(name.length()))
+{
+}
+
+std::ostream &
+operator <<(std::ostream &os, const ProtocolView &view)
+{
+    os << view.name;
+    if (!view.version.isEmpty())
+        os << view.version;
+    return os;
+}
+
+/* HttpUpgradeProtocolAccess */
+
+HttpUpgradeProtocolAccess::~HttpUpgradeProtocolAccess()
+{
+    aclDestroyAccessList(&other);
+}
+
+void
+HttpUpgradeProtocolAccess::configureGuard(ConfigParser &parser)
+{
+    const auto rawProto = parser.NextToken();
+    if (!rawProto)
+        throw TextException(ToSBuf("expected a protocol name or ", ProtoOther), Here());
+
+    if (ProtoOther.cmp(rawProto) == 0) {
+        aclParseAccessLine(cfg_directive, parser, &other);
+        return;
+    }
+
+    // To preserve ACL rules checking order, to exclude inapplicable (i.e. wrong
+    // protocol version) rules, and to keep things simple, we merge no rules.
+    acl_access *access = nullptr;
+    aclParseAccessLine(cfg_directive, parser, &access);
+    if (access)
+        namedGuards.emplace_back(rawProto, access);
+}
+
+/* HttpUpgradeProtocolAccess::NamedGuard */
+
+HttpUpgradeProtocolAccess::NamedGuard::NamedGuard(const char *rawProtocol, acl_access *acls):
+    protocol(rawProtocol),
+    proto(protocol),
+    guard(acls)
+{
+}
+
+HttpUpgradeProtocolAccess::NamedGuard::~NamedGuard() {
+    aclDestroyAccessList(&guard);
+}
+
diff --git a/src/HttpUpgradeProtocolAccess.h b/src/HttpUpgradeProtocolAccess.h
new file mode 100644 (file)
index 0000000..3721c72
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 1996-2019 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#ifndef SQUID_HTTP_UPGRADE_H
+#define SQUID_HTTP_UPGRADE_H
+
+#include "acl/forward.h"
+#include "sbuf/SBuf.h"
+
+#include <map>
+
+/// a reference to a protocol name[/version] string; no 0-termination is assumed
+class ProtocolView
+{
+public:
+    ProtocolView(const char * const start, const size_t len);
+    explicit ProtocolView(const SBuf &proto);
+
+    SBuf name; ///< everything up to (but excluding) the first slash('/')
+    SBuf version; ///< everything after the name, including the slash('/')
+};
+
+std::ostream &operator <<(std::ostream &, const ProtocolView &);
+
+// HTTP is not explicit about case sensitivity of Upgrade protocol strings, but
+// there are bug reports showing different case variants used for WebSocket. We
+// conservatively preserve the received case and compare case-sensitively.
+
+/// Either b has no version restrictions or both have the same version.
+/// For example, "ws/1" is in "ws" but "ws" is not in "ws/1".
+inline bool
+vAinB(const ProtocolView &a, const ProtocolView &b)
+{
+    // Optimization: Do not assert(a.name == b.name).
+    return b.version.isEmpty() || (a.version == b.version);
+}
+
+/// Allows or blocks HTTP Upgrade protocols (see http_upgrade_request_protocols)
+class HttpUpgradeProtocolAccess
+{
+public:
+    HttpUpgradeProtocolAccess() = default;
+    ~HttpUpgradeProtocolAccess();
+    HttpUpgradeProtocolAccess(HttpUpgradeProtocolAccess &&) = delete; // no copying of any kind
+
+    /// \returns the ACLs matching the given "name[/version]" protocol (or nil)
+    const acl_access *findGuard(const SBuf &proto) const;
+
+    /// parses a single allow/deny rule
+    void configureGuard(ConfigParser&);
+
+    /// iterates over all configured rules, calling the given visitor
+    template <typename Visitor> inline void forEach(const Visitor &) const;
+
+    /// iterates over rules applicable to the given protocol, calling visitor;
+    /// breaks iteration if the visitor returns true
+    template <typename Visitor> inline void forApplicable(const ProtocolView &, const Visitor &) const;
+
+private:
+    /// a single configured access rule for an explicitly named protocol
+    class NamedGuard
+    {
+    public:
+        NamedGuard(const char *rawProtocol, acl_access*);
+        NamedGuard(const NamedGuard &&) = delete; // no copying of any kind
+        ~NamedGuard();
+
+        const SBuf protocol; ///< configured protocol name (and version)
+        const ProtocolView proto; ///< optimization: compiled this->protocol
+        acl_access *guard = nullptr; ///< configured access rule; never nil
+    };
+
+    /// maps HTTP Upgrade protocol name/version to the ACLs guarding its usage
+    typedef std::deque<NamedGuard> NamedGuards;
+
+    /// pseudonym to specify rules for "all other protocols"
+    static const SBuf ProtoOther;
+
+    /// rules governing upgrades to explicitly named protocols
+    NamedGuards namedGuards;
+
+    /// OTHER rules governing unnamed protocols
+    acl_access *other = nullptr;
+};
+
+template <typename Visitor>
+inline void
+HttpUpgradeProtocolAccess::forEach(const Visitor &visitor) const
+{
+    for (const auto &namedGuard: namedGuards)
+        visitor(namedGuard.protocol, namedGuard.guard);
+    if (other)
+        visitor(ProtoOther, other);
+}
+
+template <typename Visitor>
+inline void
+HttpUpgradeProtocolAccess::forApplicable(const ProtocolView &offer, const Visitor &visitor) const
+{
+    auto seenApplicable = false;
+    for (const auto &namedGuard: namedGuards) {
+        if (offer.name != namedGuard.proto.name)
+            continue;
+        if (vAinB(offer, namedGuard.proto) && visitor(namedGuard.protocol, namedGuard.guard))
+            return;
+        seenApplicable = true; // may already be true
+    }
+    if (!seenApplicable && other) // OTHER is applicable if named rules were not
+        (void)visitor(ProtoOther, other);
+}
+
+#endif /* SQUID_HTTP_UPGRADE_H */
+
index 9be7ca262ac62684034130edea2a74088eee5d22..bfc439b2276a049672195890793e510afa72fe55 100644 (file)
@@ -319,6 +319,8 @@ squid_SOURCES = \
        hier_code.h \
        HierarchyLogEntry.h \
        $(HTCPSOURCE) \
+       HttpUpgradeProtocolAccess.cc \
+       HttpUpgradeProtocolAccess.h \
        http.cc \
        http.h \
        HttpHeaderFieldStat.h \
@@ -1862,7 +1864,6 @@ tests_test_http_range_SOURCES = \
        cache_cf.h \
        AuthReg.h \
        RefreshPattern.h \
-       cache_cf.cc \
        CachePeer.cc \
        CachePeer.h \
        cache_manager.cc \
@@ -2020,7 +2021,9 @@ tests_test_http_range_SOURCES = \
        tests/stub_libstore.cc \
        Transients.cc \
        tests/test_http_range.cc \
+       tests/stub_cache_cf.cc \
        tests/stub_external_acl.cc \
+       tests/stub_HttpUpgradeProtocolAccess.cc \
        tests/stub_ipc_Forwarder.cc \
        tests/stub_libdiskio.cc \
        tests/stub_libeui.cc \
@@ -2265,7 +2268,6 @@ tests_testHttpRequest_SOURCES = \
        cache_cf.h \
        AuthReg.h \
        RefreshPattern.h \
-       cache_cf.cc \
        debug.cc \
        CacheDigest.h \
        tests/stub_CacheDigest.cc \
@@ -2417,6 +2419,8 @@ tests_testHttpRequest_SOURCES = \
        tools.h \
        tools.cc \
        Transients.cc \
+       tests/stub_cache_cf.cc \
+       tests/stub_HttpUpgradeProtocolAccess.cc \
        tests/stub_tunnel.cc \
        tests/stub_libstore.cc \
        MemStore.cc \
@@ -2563,7 +2567,6 @@ tests_testCacheManager_SOURCES = \
        cache_cf.h \
        AuthReg.h \
        RefreshPattern.h \
-       cache_cf.cc \
        CachePeer.cc \
        CachePeer.h \
        CacheDigest.h \
@@ -2717,6 +2720,8 @@ tests_testCacheManager_SOURCES = \
        tools.h \
        tools.cc \
        Transients.cc \
+       tests/stub_cache_cf.cc \
+       tests/stub_HttpUpgradeProtocolAccess.cc \
        tests/stub_tunnel.cc \
        tests/stub_libstore.cc \
        MemStore.cc \
@@ -2869,7 +2874,6 @@ tests_testEvent_SOURCES = \
        cache_cf.h \
        AuthReg.h \
        RefreshPattern.h \
-       cache_cf.cc \
        CachePeer.cc \
        CachePeer.h \
        cache_manager.cc \
@@ -3026,6 +3030,8 @@ tests_testEvent_SOURCES = \
        StoreMetaUnpacker.cc \
        StoreSwapLogData.cc \
        String.cc \
+       tests/stub_cache_cf.cc \
+       tests/stub_HttpUpgradeProtocolAccess.cc \
        tests/stub_libstore.cc \
        tests/CapturingStoreEntry.h \
        tests/testEvent.cc \
@@ -3108,7 +3114,6 @@ tests_testEventLoop_SOURCES = \
        cache_cf.h \
        AuthReg.h \
        RefreshPattern.h \
-       cache_cf.cc \
        CachePeer.cc \
        CachePeer.h \
        carp.h \
@@ -3263,6 +3268,8 @@ tests_testEventLoop_SOURCES = \
        String.cc \
        StrList.h \
        StrList.cc \
+       tests/stub_cache_cf.cc \
+       tests/stub_HttpUpgradeProtocolAccess.cc \
        tests/stub_libstore.cc \
        tests/testEventLoop.cc \
        tests/testEventLoop.h \
index bb96f3320d1127a26a474d3cb9bebc6c13d696e5..9ea7d1d6c9cf30ff9a85128e574fecfc3e6e657e 100644 (file)
@@ -47,6 +47,7 @@ class external_acl;
 class HeaderManglers;
 class RefreshPattern;
 class RemovalPolicySettings;
+class HttpUpgradeProtocolAccess;
 
 namespace AnyP
 {
@@ -474,6 +475,8 @@ public:
     HeaderWithAclList *request_header_add;
     ///reply_header_add access list
     HeaderWithAclList *reply_header_add;
+    /// http_upgrade_request_protocols
+    HttpUpgradeProtocolAccess *http_upgrade_request_protocols;
     ///note
     Notes notes;
     char *coredump_dir;
index 7f45ee4c09c42a9be64e84c00fc9e078ac6851c1..2d65b2cfecd35b11b6a6e57bd15d1c7ea77dd133 100644 (file)
 #include "SquidString.h"
 #include "StrList.h"
 
-/** appends an item to the list */
 void
-strListAdd(String * str, const char *item, char del)
+strListAdd(String &str, const char *item, const size_t itemSize, const char delimiter)
 {
-    assert(str && item);
-    const auto itemSize = strlen(item);
-    if (str->size()) {
-        char buf[3];
-        buf[0] = del;
-        buf[1] = ' ';
-        buf[2] = '\0';
-        Must(str->canGrowBy(2));
-        str->append(buf, 2);
+    if (str.size()) {
+        const char buf[] = { delimiter, ' ' };
+        const auto bufSize = sizeof(buf);
+        Must(str.canGrowBy(bufSize));
+        str.append(buf, bufSize);
     }
-    Must(str->canGrowBy(itemSize));
-    str->append(item, itemSize);
+    Must(str.canGrowBy(itemSize));
+    str.append(item, itemSize);
+}
+
+void
+strListAdd(String *str, const char *item, const char delimiter)
+{
+    assert(str);
+    assert(item);
+    strListAdd(*str, item, strlen(item), delimiter);
+}
+
+void
+strListAdd(String &str, const SBuf &item, char delimiter)
+{
+    strListAdd(str, item.rawContent(), item.length(), delimiter);
 }
 
 /** returns true iff "m" is a member of the list */
index 3fad8af8844ad91798073f9e955c3558f76237f3..d0476815eb404808a0743986cca931cbe7475f75 100644 (file)
 
 #include "sbuf/forward.h"
 
+#include <iterator>
+
 class String;
 
+/// Appends the given item to a delimiter-separated list in str.
 void strListAdd(String * str, const char *item, char del);
+
+/// Appends the given item of a given size to a delimiter-separated list in str.
+void strListAdd(String &str, const char *item, const size_t itemSize, const char del = ',');
+
+/// Appends the given item to a delimiter-separated list in str.
+/// Use strListAdd(c-string) for c-string items with unknown length.
+void strListAdd(String &str, const SBuf &item, char delimiter = ',');
+
 int strListIsMember(const String * str, const SBuf &item, char del);
 int strListIsSubstr(const String * list, const char *s, char del);
+/// Iterates through delimiter-separated and optionally "quoted" list members.
+/// Follows HTTP #rule, including skipping OWS and empty members.
 int strListGetItem(const String * str, char del, const char **item, int *ilen, const char **pos);
 /// Searches for the first matching key=value pair
 /// within a delimiter-separated list of items.
index 0cc5d80c9a59a424ea95cde17921a032cc970f35..2b5e00ebeef7ca5a636fa79226426e70780a8fc5 100644 (file)
@@ -118,7 +118,7 @@ public:
     // not explicit: allow "aclMatchCode to Acl::Answer" conversions (for now)
     Answer(const aclMatchCode aCode, int aKind = 0): code(aCode), kind(aKind) {}
 
-    Answer(): code(ACCESS_DUNNO), kind(0) {}
+    Answer() = default;
 
     bool operator ==(const aclMatchCode aCode) const {
         return code == aCode;
@@ -151,8 +151,13 @@ public:
     /// whether Squid is uncertain about the allowed() or denied() answer
     bool conflicted() const { return !allowed() && !denied(); }
 
-    aclMatchCode code; ///< ACCESS_* code
-    int kind; ///< which custom access list verb matched
+    aclMatchCode code = ACCESS_DUNNO; ///< ACCESS_* code
+
+    /// the matched custom access list verb (or zero)
+    int kind = 0;
+
+    /// whether we were computed by the "negate the last explicit action" rule
+    bool implicit = false;
 };
 
 } // namespace Acl
index 976d329bd4cbf7197781c76db0ab29c477ba5265..90576cd50903f8bf26f4ffd9697aa0f0fff88c3c 100644 (file)
@@ -379,6 +379,7 @@ ACLChecklist::calcImplicitAnswer()
         implicitRuleAnswer = Acl::Answer(ACCESS_DENIED);
     // else we saw no rules and will respond with ACCESS_DUNNO
 
+    implicitRuleAnswer.implicit = true;
     debugs(28, 3, HERE << this << " NO match found, last action " <<
            lastAction << " so returning " << implicitRuleAnswer);
     markFinished(implicitRuleAnswer, "implicit rule won");
index a4b22e22d2dbc334e8fb325786f210546fe16baf..a73f32c547e39599c667992b168fad22f706aa23 100644 (file)
@@ -1611,6 +1611,13 @@ Adaptation::Icap::ModXact::encapsulateHead(MemBuf &icapBuf, const char *section,
     headClone->header.delById(Http::HdrType::PROXY_AUTHENTICATE);
     headClone->header.removeHopByHopEntries();
 
+    // TODO: modify HttpHeader::removeHopByHopEntries to accept a list of
+    // excluded hop-by-hop headers
+    if (head->header.has(Http::HdrType::UPGRADE)) {
+        const auto upgrade = head->header.getList(Http::HdrType::UPGRADE);
+        headClone->header.putStr(Http::HdrType::UPGRADE, upgrade.termedBuf());
+    }
+
     // pack polished HTTP header
     packHead(httpBuf, headClone.getRaw());
 
index b022270a608a46fee8e4f4c381ce769970fb0a9b..ef154fd0a4f912a450649c223109cf46ec5cb572 100644 (file)
@@ -33,6 +33,7 @@
 #include "ftp/Elements.h"
 #include "globals.h"
 #include "HttpHeaderTools.h"
+#include "HttpUpgradeProtocolAccess.h"
 #include "icmp/IcmpConfig.h"
 #include "ident/Config.h"
 #include "ip/Intercept.h"
@@ -251,6 +252,9 @@ static void parse_on_unsupported_protocol(acl_access **access);
 static void dump_on_unsupported_protocol(StoreEntry *entry, const char *name, acl_access *access);
 static void free_on_unsupported_protocol(acl_access **access);
 static void ParseAclWithAction(acl_access **access, const Acl::Answer &action, const char *desc, ACL *acl = nullptr);
+static void parse_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuards);
+static void dump_http_upgrade_request_protocols(StoreEntry *entry, const char *name, HttpUpgradeProtocolAccess *protoGuards);
+static void free_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuards);
 
 /*
  * LegacyParser is a parser for legacy code that uses the global
@@ -5113,3 +5117,37 @@ free_on_unsupported_protocol(acl_access **access)
     free_acl_access(access);
 }
 
+static void
+parse_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuardsPtr)
+{
+    assert(protoGuardsPtr);
+    auto &protoGuards = *protoGuardsPtr;
+    if (!protoGuards)
+        protoGuards = new HttpUpgradeProtocolAccess();
+    protoGuards->configureGuard(LegacyParser);
+}
+
+static void
+dump_http_upgrade_request_protocols(StoreEntry *entry, const char *rawName, HttpUpgradeProtocolAccess *protoGuards)
+{
+    assert(protoGuards);
+    const SBuf name(rawName);
+    protoGuards->forEach([entry,&name](const SBuf &proto, const acl_access *acls) {
+        SBufList line;
+        line.push_back(name);
+        line.push_back(proto);
+        const auto acld = acls->treeDump("", &Acl::AllowOrDeny);
+        line.insert(line.end(), acld.begin(), acld.end());
+        dump_SBufList(entry, line);
+    });
+}
+
+static void
+free_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuardsPtr)
+{
+    assert(protoGuardsPtr);
+    auto &protoGuards = *protoGuardsPtr;
+    delete protoGuards;
+    protoGuards = nullptr;
+}
+
index 2dc12a137721f0a9f66ffa329c24af0efc8f3752..04c4a338c85fb36248c75e69fe2ebe8d7e29ff44 100644 (file)
@@ -43,6 +43,7 @@ hostdomain            cache_peer
 hostdomaintype         cache_peer
 http_header_access     acl
 http_header_replace
+http_upgrade_request_protocols acl
 HeaderWithAclList      acl
 adaptation_access_type adaptation_service_set adaptation_service_chain acl icap_service icap_class
 adaptation_service_set_type    icap_service ecap_service
index dcc96fbafa07d47526b4a91d3db169fbe4bdb0f4..74e3b301c93c31c18f2137cb8dcb50fcffbc5433 100644 (file)
@@ -10472,6 +10472,103 @@ DOC_START
        that the request body is needed. Delaying is the default behavior.
 DOC_END
 
+NAME: http_upgrade_request_protocols
+TYPE: http_upgrade_request_protocols
+LOC: Config.http_upgrade_request_protocols
+DEFAULT: none
+DEFAULT_DOC: Upgrade header dropped, effectively blocking an upgrade attempt.
+DOC_START
+       Controls client-initiated and server-confirmed switching from HTTP to
+       another protocol (or to several protocols) using HTTP Upgrade mechanism
+       defined in RFC 7230 Section 6.7. Squid itself does not understand the
+       protocols being upgraded to and participates in the upgraded
+       communication only as a dumb TCP proxy. Admins should not allow
+       upgrading to protocols that require a more meaningful proxy
+       participation.
+
+       Usage: http_upgrade_request_protocols <protocol> allow|deny [!]acl ...
+
+       The required "protocol" parameter is either an all-caps word OTHER or an
+       explicit protocol name (e.g. "WebSocket") optionally followed by a slash
+       and a version token (e.g. "HTTP/3"). Explicit protocol names and
+       versions are case sensitive.
+
+       When an HTTP client sends an Upgrade request header, Squid iterates over
+       the client-offered protocols and, for each protocol P (with an optional
+       version V), evaluates the first non-empty set of
+       http_upgrade_request_protocols rules (if any) from the following list:
+
+               * All rules with an explicit protocol name equal to P.
+               * All rules that use OTHER instead of a protocol name.
+
+       In other words, rules using OTHER are considered for protocol P if and
+       only if there are no rules mentioning P by name.
+
+       If both of the above sets are empty, then Squid removes protocol P from
+       the Upgrade offer.
+
+       If the client sent a versioned protocol offer P/X, then explicit rules
+       referring to the same-name but different-version protocol P/Y are
+       declared inapplicable. Inapplicable rules are not evaluated (i.e. are
+       ignored). However, inapplicable rules still belong to the first set of
+       rules for P.
+
+       Within the applicable rule subset, individual rules are evaluated in
+       their configuration order. If all ACLs of an applicable "allow" rule
+       match, then the protocol offered by the client is forwarded to the next
+       hop as is. If all ACLs of an applicable "deny" rule match, then the
+       offer is dropped. If no applicable rules have matching ACLs, then the
+       offer is also dropped. The first matching rule also ends rules
+       evaluation for the offered protocol.
+
+       If all client-offered protocols are removed, then Squid forwards the
+       client request without the Upgrade header. Squid never sends an empty
+       Upgrade request header.
+
+       An Upgrade request header with a value violating HTTP syntax is dropped
+       and ignored without an attempt to use extractable individual protocol
+       offers.
+
+       Upon receiving an HTTP 101 (Switching Protocols) control message, Squid
+       checks that the server listed at least one protocol name and sent a
+       Connection:upgrade response header. Squid does not understand individual
+       protocol naming and versioning concepts enough to implement stricter
+       checks, but an admin can restrict HTTP 101 (Switching Protocols)
+       responses further using http_reply_access. Responses denied by
+       http_reply_access rules and responses flagged by the internal Upgrade
+       checks result in HTTP 502 (Bad Gateway) ERR_INVALID_RESP errors and
+       Squid-to-server connection closures.
+
+       If Squid sends an Upgrade request header, and the next hop (e.g., the
+       origin server) responds with an acceptable HTTP 101 (Switching
+       Protocols), then Squid forwards that message to the client and becomes
+       a TCP tunnel.
+
+       The presence of an Upgrade request header alone does not preclude cache
+       lookups. In other words, an Upgrade request might be satisfied from the
+       cache, using regular HTTP caching rules.
+
+       This clause only supports fast acl types.
+       See http://wiki.squid-cache.org/SquidFaq/SquidAcl for details.
+
+       Each of the following groups of configuration lines represents a
+       separate configuration example:
+
+       # never upgrade to protocol Foo; all others are OK
+       http_upgrade_request_protocols Foo deny all
+       http_upgrade_request_protocols OTHER allow all
+
+       # only allow upgrades to protocol Bar (except for its first version)
+       http_upgrade_request_protocols Bar/1 deny all
+       http_upgrade_request_protocols Bar allow all
+       http_upgrade_request_protocols OTHER deny all # this rule is optional
+
+       # only allow upgrades to protocol Baz, and only if Baz is the only offer
+       acl UpgradeHeaderHasMultipleOffers ...
+       http_upgrade_request_protocols Baz deny UpgradeHeaderHasMultipleOffers
+       http_upgrade_request_protocols Baz allow all
+DOC_END
+
 NAME: server_pconn_for_nonretriable
 TYPE: acl_access
 DEFAULT: none
index 58468491f85ed372be3899138720a3ebd0d9564b..fb5342ab3e116f5e5ce5eaa7d80b719785b6b937 100644 (file)
@@ -4089,3 +4089,9 @@ operator <<(std::ostream &os, const ConnStateData::PinnedIdleContext &pic)
     return os << pic.connection << ", request=" << pic.request;
 }
 
+std::ostream &
+operator <<(std::ostream &os, const ConnStateData::ServerConnectionContext &scc)
+{
+    return os << scc.conn_ << ", srv_bytes=" << scc.preReadServerBytes.length();
+}
+
index cfa9ec08cffe013201d24837f329a68ef3143675..8dc34cda36d722bf439533a76da63e378e13cdca 100644 (file)
@@ -199,6 +199,25 @@ public:
     // pining related comm callbacks
     virtual void clientPinnedConnectionClosed(const CommCloseCbParams &io);
 
+    /// noteTakeServerConnectionControl() callback parameter
+    class ServerConnectionContext {
+    public:
+        ServerConnectionContext(const Comm::ConnectionPointer &conn, const HttpRequest::Pointer &req, const SBuf &post101Bytes): preReadServerBytes(post101Bytes), conn_(conn) { conn_->enterOrphanage(); }
+
+        /// gives to-server connection to the new owner
+        Comm::ConnectionPointer connection() { conn_->leaveOrphanage(); return conn_; }
+
+        SBuf preReadServerBytes; ///< post-101 bytes received from the server
+
+    private:
+        friend std::ostream &operator <<(std::ostream &, const ServerConnectionContext &);
+        Comm::ConnectionPointer conn_; ///< to-server connection
+    };
+
+    /// Gives us the control of the Squid-to-server connection.
+    /// Used, for example, to initiate a TCP tunnel after protocol switching.
+    virtual void noteTakeServerConnectionControl(ServerConnectionContext) {}
+
     // comm callbacks
     void clientReadFtpData(const CommIoCbParams &io);
     void connStateClosed(const CommCloseCbParams &io);
@@ -470,6 +489,7 @@ void clientProcessRequest(ConnStateData *, const Http1::RequestParserPointer &,
 void clientPostHttpsAccept(ConnStateData *);
 
 std::ostream &operator <<(std::ostream &os, const ConnStateData::PinnedIdleContext &pic);
+std::ostream &operator <<(std::ostream &, const ConnStateData::ServerConnectionContext &);
 
 #endif /* SQUID_CLIENTSIDE_H */
 
index 3e36be06e3662a4f75a7a16c3369de36f9016333..6f4a9a50ae80d6a8dc27edf9219a49e3265f39e9 100644 (file)
@@ -41,12 +41,16 @@ Comm::Connection::Connection() :
     *rfc931 = 0; // quick init the head. the rest does not matter.
 }
 
-static int64_t lost_conn = 0;
 Comm::Connection::~Connection()
 {
     if (fd >= 0) {
-        debugs(5, 4, "BUG #3329: Orphan Comm::Connection: " << *this);
-        debugs(5, 4, "NOTE: " << ++lost_conn << " Orphans since last started.");
+        if (flags & COMM_ORPHANED) {
+            debugs(5, 5, "closing orphan: " << *this);
+        } else {
+            static uint64_t losses = 0;
+            ++losses;
+            debugs(5, 4, "BUG #3329: Lost orphan #" << losses << ": " << *this);
+        }
         close();
     }
 
index 290bcf18de885bff61282df22b194a11dc7e6723..6beea7c6d9fc50f6f68ea9591a9060d9ef1bbd36 100644 (file)
@@ -50,6 +50,8 @@ namespace Comm
 #define COMM_TRANSPARENT        0x10  // arrived via TPROXY
 #define COMM_INTERCEPTION       0x20  // arrived via NAT
 #define COMM_REUSEPORT          0x40 //< needs SO_REUSEPORT
+/// not registered with Comm and not owned by any connection-closing code
+#define COMM_ORPHANED           0x40
 
 /**
  * Store data about the physical and logical attributes of a connection.
@@ -80,6 +82,11 @@ public:
      */
     ConnectionPointer copyDetails() const;
 
+    /// close the still-open connection when its last reference is gone
+    void enterOrphanage() { flags |= COMM_ORPHANED; }
+    /// resume relying on owner(s) to initiate an explicit connection closure
+    void leaveOrphanage() { flags &= ~COMM_ORPHANED; }
+
     /** Close any open socket. */
     void close();
 
index 4d1d9834613c5d10f4229e1a31806546673aecf0..950487834ed80e0650c1d8f20574c21bc5a27d53 100644 (file)
@@ -41,6 +41,7 @@
 #include "HttpHeaderTools.h"
 #include "HttpReply.h"
 #include "HttpRequest.h"
+#include "HttpUpgradeProtocolAccess.h"
 #include "log/access_log.h"
 #include "MemBuf.h"
 #include "MemObject.h"
@@ -137,6 +138,8 @@ HttpStateData::~HttpStateData()
 
     cbdataReferenceDone(_peer);
 
+    delete upgradeHeaderOut;
+
     debugs(11,5, HERE << "HttpStateData " << this << " destroyed; " << serverConnection);
 }
 
@@ -797,11 +800,18 @@ HttpStateData::handle1xx(HttpReply *reply)
     Must(!flags.handling1xx);
     flags.handling1xx = true;
 
-    if (!request->canHandle1xx() || request->forcedBodyContinuation) {
-        debugs(11, 2, "ignoring 1xx because it is " << (request->forcedBodyContinuation ? "already sent" : "not supported by client"));
-        proceedAfter1xx();
-        return;
-    }
+    const auto statusCode = reply->sline.status();
+
+    // drop1xx() needs to handle HTTP 101 (Switching Protocols) responses
+    // specially because they indicate that the server has stopped speaking HTTP
+    Must(!flags.serverSwitchedProtocols);
+    flags.serverSwitchedProtocols = (statusCode == Http::scSwitchingProtocols);
+
+    if (statusCode == Http::scContinue && request->forcedBodyContinuation)
+        return drop1xx("we have sent it already");
+
+    if (!request->canHandle1xx())
+        return drop1xx("the client does not support it");
 
 #if USE_HTTP_VIOLATIONS
     // check whether the 1xx response forwarding is allowed by squid.conf
@@ -811,14 +821,16 @@ HttpStateData::handle1xx(HttpReply *reply)
         ch.reply = reply;
         ch.syncAle(originalRequest().getRaw(), nullptr);
         HTTPMSGLOCK(ch.reply);
-        if (!ch.fastCheck().allowed()) { // TODO: support slow lookups?
-            debugs(11, 3, HERE << "ignoring denied 1xx");
-            proceedAfter1xx();
-            return;
-        }
+        if (!ch.fastCheck().allowed()) // TODO: support slow lookups?
+            return drop1xx("http_reply_access blocked it");
     }
 #endif // USE_HTTP_VIOLATIONS
 
+    if (flags.serverSwitchedProtocols) {
+        if (const auto reason = blockSwitchingProtocols(*reply))
+            return drop1xx(reason);
+    }
+
     debugs(11, 2, HERE << "forwarding 1xx to client");
 
     // the Sink will use this to call us back after writing 1xx to the client
@@ -833,11 +845,75 @@ HttpStateData::handle1xx(HttpReply *reply)
     // for similar reasons without a 1xx response.
 }
 
+/// if possible, safely ignores the received 1xx control message
+/// otherwise, terminates the server connection
+void
+HttpStateData::drop1xx(const char *reason)
+{
+    if (flags.serverSwitchedProtocols) {
+        debugs(11, 2, "bad 101 because " << reason);
+        const auto err = new ErrorState(ERR_INVALID_RESP, Http::scBadGateway, request.getRaw(), fwd->al);
+        fwd->fail(err);
+        closeServer();
+        mustStop("prohibited HTTP/101 response");
+        return;
+    }
+
+    debugs(11, 2, "ignoring 1xx because " << reason);
+    proceedAfter1xx();
+}
+
+/// \retval nil if the HTTP/101 (Switching Protocols) reply should be forwarded
+/// \retval reason why an attempt to switch protocols should be stopped
+const char *
+HttpStateData::blockSwitchingProtocols(const HttpReply &reply) const
+{
+    if (!upgradeHeaderOut)
+        return "Squid offered no Upgrade at all, but server switched to a tunnel";
+
+    // See RFC 7230 section 6.7 for the corresponding MUSTs
+
+    if (!reply.header.has(Http::HdrType::UPGRADE))
+        return "server did not send an Upgrade header field";
+
+    if (!reply.header.hasListMember(Http::HdrType::CONNECTION, "upgrade", ','))
+        return "server did not send 'Connection: upgrade'";
+
+    const auto acceptedProtos = reply.header.getList(Http::HdrType::UPGRADE);
+    const char *pos = nullptr;
+    const char *accepted = nullptr;
+    int acceptedLen = 0;
+    while (strListGetItem(&acceptedProtos, ',', &accepted, &acceptedLen, &pos)) {
+        debugs(11, 5, "server accepted at least" << Raw(nullptr, accepted, acceptedLen));
+        return nullptr; // OK: let the client validate server's selection
+    }
+
+    return "server sent an essentially empty Upgrade header field";
+}
+
 /// restores state and resumes processing after 1xx is ignored or forwarded
 void
 HttpStateData::proceedAfter1xx()
 {
     Must(flags.handling1xx);
+
+    if (flags.serverSwitchedProtocols) {
+        // pass server connection ownership to request->clientConnectionManager
+        ConnStateData::ServerConnectionContext scc(serverConnection, request, inBuf);
+        typedef UnaryMemFunT<ConnStateData, ConnStateData::ServerConnectionContext> MyDialer;
+        AsyncCall::Pointer call = asyncCall(11, 3, "ConnStateData::noteTakeServerConnectionControl",
+                                            MyDialer(request->clientConnectionManager,
+                                                    &ConnStateData::noteTakeServerConnectionControl, scc));
+        ScheduleCallHere(call);
+        fwd->unregister(serverConnection);
+        comm_remove_close_handler(serverConnection->fd, closeHandler);
+        closeHandler = nullptr;
+        serverConnection = nullptr;
+        doneWithFwd = "switched protocols";
+        mustStop(doneWithFwd);
+        return;
+    }
+
     debugs(11, 2, "continuing with " << payloadSeen << " bytes in buffer after 1xx");
     CallJobHere(11, 3, this, HttpStateData, HttpStateData::processReply);
 }
@@ -1956,6 +2032,66 @@ HttpStateData::httpBuildRequestHeader(HttpRequest * request,
     strConnection.clean();
 }
 
+/// copies from-client Upgrade info into the given to-server header while
+/// honoring configuration filters and following HTTP requirements
+void
+HttpStateData::forwardUpgrade(HttpHeader &hdrOut)
+{
+    if (!Config.http_upgrade_request_protocols)
+        return; // forward nothing by default
+
+    /* RFC 7230 section 6.7 paragraph 10:
+     * A server MUST ignore an Upgrade header field that is received in
+     * an HTTP/1.0 request.
+     */
+    if (request->http_ver == Http::ProtocolVersion(1,0))
+        return;
+
+    const auto &hdrIn = request->header;
+    if (!hdrIn.has(Http::HdrType::UPGRADE))
+        return;
+    const auto upgradeIn = hdrIn.getList(Http::HdrType::UPGRADE);
+
+    String upgradeOut;
+
+    ACLFilledChecklist ch(nullptr, request.getRaw());
+    ch.al = fwd->al;
+    const char *pos = nullptr;
+    const char *offeredStr = nullptr;
+    int offeredStrLen = 0;
+    while (strListGetItem(&upgradeIn, ',', &offeredStr, &offeredStrLen, &pos)) {
+        const ProtocolView offeredProto(offeredStr, offeredStrLen);
+        debugs(11, 5, "checks all rules applicable to " << offeredProto);
+        Config.http_upgrade_request_protocols->forApplicable(offeredProto, [&ch, offeredStr, offeredStrLen, &upgradeOut] (const SBuf &cfgProto, const acl_access *guard) {
+            debugs(11, 5, "checks " << cfgProto << " rule(s)");
+            ch.changeAcl(guard);
+            const auto answer = ch.fastCheck();
+            if (answer.implicit)
+                return false; // keep looking for an explicit rule match
+            if (answer.allowed())
+                strListAdd(upgradeOut, offeredStr, offeredStrLen);
+            // else drop the offer (explicitly denied cases and ACL errors)
+            return true; // stop after an explicit rule match or an error
+        });
+    }
+
+    if (upgradeOut.size()) {
+        hdrOut.putStr(Http::HdrType::UPGRADE, upgradeOut.termedBuf());
+
+        /* RFC 7230 section 6.7 paragraph 10:
+         * When Upgrade is sent, the sender MUST also send a Connection header
+         * field that contains an "upgrade" connection option, in
+         * order to prevent Upgrade from being accidentally forwarded by
+         * intermediaries that might not implement the listed protocols.
+         *
+         * NP: Squid does not truly implement the protocol(s) in this Upgrade.
+         * For now we are treating an explicit blind tunnel as "implemented"
+         * regardless of the security implications.
+         */
+        hdrOut.putStr(Http::HdrType::CONNECTION, "upgrade");
+    }
+}
+
 /**
  * Decides whether a particular header may be cloned from the received Clients request
  * to our outgoing fetch request.
@@ -1989,10 +2125,13 @@ copyOneHeaderFromClientsideRequestToUpstreamRequest(const HttpHeaderEntry *e, co
     case Http::HdrType::KEEP_ALIVE:          /** \par Keep-Alive: */
     case Http::HdrType::PROXY_AUTHENTICATE:  /** \par Proxy-Authenticate: */
     case Http::HdrType::TRAILER:             /** \par Trailer: */
-    case Http::HdrType::UPGRADE:             /** \par Upgrade: */
     case Http::HdrType::TRANSFER_ENCODING:   /** \par Transfer-Encoding: */
         break;
 
+    /// \par Upgrade is hop-by-hop but forwardUpgrade() may send a filtered one
+    case Http::HdrType::UPGRADE:
+        break;
+
     /** \par OTHER headers I haven't bothered to track down yet. */
 
     case Http::HdrType::AUTHORIZATION:
@@ -2185,6 +2324,7 @@ HttpStateData::buildRequestPrefix(MemBuf * mb)
     /* build and pack headers */
     {
         HttpHeader hdr(hoRequest);
+        forwardUpgrade(hdr);
         httpBuildRequestHeader(request.getRaw(), entry, fwd->al, &hdr, flags);
 
         if (request->flags.pinned && request->flags.connectionAuth)
@@ -2192,6 +2332,13 @@ HttpStateData::buildRequestPrefix(MemBuf * mb)
         else if (hdr.has(Http::HdrType::AUTHORIZATION))
             request->flags.authSent = true;
 
+        // The late placement of this check supports reply_header_add mangling,
+        // but also complicates optimizing upgradeHeaderOut-like lookups.
+        if (hdr.has(Http::HdrType::UPGRADE)) {
+            assert(!upgradeHeaderOut);
+            upgradeHeaderOut = new String(hdr.getList(Http::HdrType::UPGRADE));
+        }
+
         hdr.packInto(mb);
         hdr.clean();
     }
index 77cc3763a1e9088fab067eab88995f272a99e28e..47ec720213c724c70fb87afaf507cdacbec0d154 100644 (file)
@@ -17,6 +17,7 @@
 
 class FwdState;
 class HttpHeader;
+class String;
 
 class HttpStateData : public Client
 {
@@ -69,12 +70,16 @@ public:
     bool ignoreCacheControl;
     bool surrogateNoStore;
 
+    /// Upgrade header value sent to the origin server or cache peer.
+    String *upgradeHeaderOut = nullptr;
+
     void processSurrogateControl(HttpReply *);
 
 protected:
     void processReply();
     void proceedAfter1xx();
     void handle1xx(HttpReply *msg);
+    void drop1xx(const char *reason);
 
 private:
     /**
@@ -135,8 +140,10 @@ private:
     void httpTimeout(const CommTimeoutCbParams &params);
 
     mb_size_t buildRequestPrefix(MemBuf * mb);
+    void forwardUpgrade(HttpHeader&);
     static bool decideIfWeDoRanges (HttpRequest * orig_request);
     bool peerSupportsConnectionPinning() const;
+    const char *blockSwitchingProtocols(const HttpReply&) const;
 
     /// Parser being used at present to parse the HTTP/ICY server response.
     Http1::ResponseParserPointer hp;
index e4d3eda109378186e84fcfc46f906380be2f82e4..499cfc7068b484dcd2946e2b23741dac42ee9b3f 100644 (file)
@@ -18,7 +18,15 @@ public:
     unsigned int front_end_https = 0; ///< send "Front-End-Https: On" header (off/on/auto=2)
     bool keepalive = false; ///< whether to keep the connection persistent
     bool only_if_cached = false;
-    bool handling1xx = false;       ///< we are ignoring or forwarding 1xx response
+
+    /// Whether we are processing an HTTP 1xx control message.
+    bool handling1xx = false;
+
+    /// Whether we received an HTTP 101 (Switching Protocols) control message.
+    /// Implies true handling1xx, but the serverSwitchedProtocols state is
+    /// permanent/final while handling of other control messages usually stops.
+    bool serverSwitchedProtocols = false;
+
     bool headers_parsed = false;
 
     /// Whether the next TCP hop is a cache_peer, including originserver
index d6d577e1af3263f1b73692c1815528049266ab64..8a9439bb6543698f1265a0d43b4a77bcecd2ca54 100644 (file)
@@ -233,6 +233,17 @@ Http::One::Server::proceedAfterBodyContinuation(Http::StreamPointer context)
     clientProcessRequest(this, parser_, context.getRaw());
 }
 
+int
+Http::One::Server::pipelinePrefetchMax() const
+{
+    const auto context = pipeline.back();
+    const auto request = (context && context->http) ? context->http->request : nullptr;
+    if (request && request->header.has(Http::HdrType::UPGRADE))
+        return 0;
+
+    return ConnStateData::pipelinePrefetchMax();
+}
+
 void
 Http::One::Server::processParsedRequest(Http::StreamPointer &context)
 {
@@ -335,12 +346,25 @@ Http::One::Server::writeControlMsgAndCall(HttpReply *rep, AsyncCall::Pointer &ca
 
     const ClientHttpRequest *http = context->http;
 
+    // remember Upgrade header; removeHopByHopEntries() will remove it
+    String upgradeHeader;
+    const auto switching = (rep->sline.status() == Http::scSwitchingProtocols);
+    if (switching)
+        upgradeHeader = rep->header.getList(Http::HdrType::UPGRADE);
+
     // apply selected clientReplyContext::buildReplyHeader() mods
     // it is not clear what headers are required for control messages
     rep->header.removeHopByHopEntries();
     // paranoid: ContentLengthInterpreter has cleaned non-generated replies
     rep->removeIrrelevantContentLength();
-    rep->header.putStr(Http::HdrType::CONNECTION, "keep-alive");
+
+    if (switching && /* paranoid: */ upgradeHeader.size()) {
+        rep->header.putStr(Http::HdrType::UPGRADE, upgradeHeader.termedBuf());
+        rep->header.putStr(Http::HdrType::CONNECTION, "upgrade, keep-alive");
+    } else {
+        rep->header.putStr(Http::HdrType::CONNECTION, "keep-alive");
+    }
+
     httpHdrMangleList(&rep->header, http->request, http->al, ROR_REPLY);
 
     MemBuf *mb = rep->pack();
@@ -354,6 +378,24 @@ Http::One::Server::writeControlMsgAndCall(HttpReply *rep, AsyncCall::Pointer &ca
     return true;
 }
 
+void switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData);
+
+void
+Http::One::Server::noteTakeServerConnectionControl(ServerConnectionContext server)
+{
+    const auto context = pipeline.front();
+    assert(context);
+    const auto http = context->http;
+    assert(http);
+    assert(http->request);
+
+    stopReading();
+    Must(!writer);
+
+    switchToTunnel(http->request, clientConnection,
+                   server.connection(), server.preReadServerBytes);
+}
+
 ConnStateData *
 Http::NewServer(MasterXactionPointer &xact)
 {
index 88e3ef0f3ac26b6da1af093ddc25996e210f8985..244fee9077b92751664580611d1b2a7536663465 100644 (file)
@@ -33,7 +33,9 @@ protected:
     virtual void processParsedRequest(Http::StreamPointer &context);
     virtual void handleReply(HttpReply *rep, StoreIOBuffer receivedData);
     virtual bool writeControlMsgAndCall(HttpReply *rep, AsyncCall::Pointer &call);
+    virtual int pipelinePrefetchMax() const;
     virtual time_t idleTimeout() const;
+    virtual void noteTakeServerConnectionControl(ServerConnectionContext);
 
     /* BodyPipe API */
     virtual void noteMoreBodySpaceAvailable(BodyPipe::Pointer);
index 6973b79ee39e5b98aee3d2f15041247ea76b2e81..e48e39b35b174f62c82286eef9a07f92b1c1c6a1 100644 (file)
@@ -23,7 +23,7 @@
 
 CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeekingPeerConnector);
 
-void switchToTunnel(HttpRequest *request, Comm::ConnectionPointer & clientConn, Comm::ConnectionPointer &srvConn);
+void switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData);
 
 void
 Ssl::PeekingPeerConnector::cbCheckForPeekAndSpliceDone(Acl::Answer answer, void *data)
@@ -252,12 +252,28 @@ Ssl::PeekingPeerConnector::noteNegotiationDone(ErrorState *error)
                 bail(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
                 throw TextException("from-client connection gone", Here());
             }
-            switchToTunnel(request.getRaw(), clientConn, serverConn);
-            tunnelInsteadOfNegotiating();
+            startTunneling();
         }
     }
 }
 
+void
+Ssl::PeekingPeerConnector::startTunneling()
+{
+    // switchToTunnel() drains any already buffered from-server data (rBufData)
+    fd_table[serverConn->fd].useDefaultIo();
+    // tunnelStartShoveling() drains any buffered from-client data (inBuf)
+    fd_table[clientConn->fd].useDefaultIo();
+
+    // TODO: Encapsulate this frequently repeated logic into a method.
+    const auto session = fd_table[serverConn->fd].ssl;
+    auto b = SSL_get_rbio(session.get());
+    auto srvBio = static_cast<Ssl::ServerBio*>(BIO_get_data(b));
+
+    switchToTunnel(request.getRaw(), clientConn, serverConn, srvBio->rBufData());
+    tunnelInsteadOfNegotiating();
+}
+
 void
 Ssl::PeekingPeerConnector::noteWantWrite()
 {
index d69ff7826ca711fa004bf70943a8211ebf93c740..1987db83334cfff281e82b6569c2dc43294c3774 100644 (file)
@@ -63,6 +63,9 @@ public:
     /// connection manager members
     void serverCertificateVerified();
 
+    /// Abruptly stops TLS negotiation and starts tunneling.
+    void startTunneling();
+
     /// A wrapper function for checkForPeekAndSpliceDone for use with acl
     static void cbCheckForPeekAndSpliceDone(Acl::Answer answer, void *data);
 
diff --git a/src/tests/stub_HttpUpgradeProtocolAccess.cc b/src/tests/stub_HttpUpgradeProtocolAccess.cc
new file mode 100644 (file)
index 0000000..71e006f
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+#include "ConfigParser.h"
+
+#define STUB_API "HttpUpgradeProtocolAccess.cc"
+#include "STUB.h"
+
+#include "HttpUpgradeProtocolAccess.h"
+ProtocolView::ProtocolView(const char * const, const size_t) STUB
+ProtocolView::ProtocolView(SBuf const &) STUB
+std::ostream &operator <<(std::ostream &os, const ProtocolView &) STUB_RETVAL(os)
+HttpUpgradeProtocolAccess::~HttpUpgradeProtocolAccess() STUB
+const acl_access *HttpUpgradeProtocolAccess::findGuard(const SBuf &) const STUB_RETVAL(nullptr)
+void HttpUpgradeProtocolAccess::configureGuard(ConfigParser &) STUB
+const SBuf HttpUpgradeProtocolAccess::ProtoOther("STUB-OTHER");
+HttpUpgradeProtocolAccess::NamedGuard::~NamedGuard() STUB_NOP
+HttpUpgradeProtocolAccess::NamedGuard::NamedGuard(const char *, acl_access *): protocol("STUB-UNDEF"), proto(protocol) STUB_NOP
+
index 70a557fe492d25b5787c8eaaad0722eccd18847c..40edc5bfd9d392a94b3f90c2fcfe835cf95512b8 100644 (file)
@@ -25,6 +25,7 @@ void parse_wordlist(wordlist ** list) STUB
 void requirePathnameExists(const char *name, const char *path) STUB_NOP
 void parse_time_t(time_t * var) STUB
 void ConfigParser::ParseUShort(unsigned short *var) STUB
+void ConfigParser::ParseWordList(wordlist **) STUB
 void dump_acl_access(StoreEntry * entry, const char *name, acl_access * head) STUB
 void dump_acl_list(StoreEntry*, ACLList*) STUB
 
index 69f1facde83135f92055e26e4000e8f2bc1defde..545c967134d54083f5f99d13b320effb2a8deeac 100644 (file)
@@ -16,5 +16,5 @@ class ClientHttpRequest;
 
 void tunnelStart(ClientHttpRequest *) STUB
 
-void switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::ConnectionPointer &srvConn) STUB
+void switchToTunnel(HttpRequest *, const Comm::ConnectionPointer &, const Comm::ConnectionPointer &, const SBuf &) STUB
 
index 02472665ad8dbeb3f41628f0edf8eb25a1d7846f..dac7411844af2aa1de6a6487dcf026c9ca6406b9 100644 (file)
@@ -1068,8 +1068,8 @@ TunnelStateData::connectToPeer(const Comm::ConnectionPointer &conn)
     if (const auto p = conn->getPeer()) {
         if (p->secure.encryptTransport)
             return advanceDestination("secure connection to peer", conn, [this,&conn] {
-                secureConnectionToPeer(conn);
-            });
+            secureConnectionToPeer(conn);
+        });
     }
 
     connectedToPeer(conn);
@@ -1339,9 +1339,15 @@ TunnelStateData::notifyConnOpener()
     }
 }
 
-#if USE_OPENSSL
+/**
+ * Sets up a TCP tunnel through Squid and starts shoveling traffic.
+ * \param request the request that initiated/caused this tunnel
+ * \param clientConn the already accepted client-to-Squid TCP connection
+ * \param srvConn the already established Squid-to-server TCP connection
+ * \param preReadServerData server-sent bytes to be forwarded to the client
+ */
 void
-switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::ConnectionPointer &srvConn)
+switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData)
 {
     Must(Comm::IsConnOpen(clientConn));
     Must(Comm::IsConnOpen(srvConn));
@@ -1361,9 +1367,6 @@ switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::
 
     TunnelStateData *tunnelState = new TunnelStateData(context->http);
 
-    // tunnelStartShoveling() drains any buffered from-client data (inBuf)
-    fd_table[clientConn->fd].useDefaultIo();
-
     request->hier.resetPeerNotes(srvConn, tunnelState->getHost());
 
     tunnelState->server.conn = srvConn;
@@ -1383,15 +1386,8 @@ switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::
     else
         request->prepForDirect();
 
-    // we drain any already buffered from-server data below (rBufData)
-    fd_table[srvConn->fd].useDefaultIo();
+    tunnelState->preReadServerData = preReadServerData;
 
-    auto ssl = fd_table[srvConn->fd].ssl.get();
-    assert(ssl);
-    BIO *b = SSL_get_rbio(ssl);
-    Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
-    tunnelState->preReadServerData = srvBio->rBufData();
     tunnelStartShoveling(tunnelState);
 }
-#endif //USE_OPENSSL