]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Peering support for SslBump (#380)
authorChristos Tsantilas <christos@chtsanti.net>
Mon, 25 Mar 2019 09:37:42 +0000 (09:37 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Sun, 31 Mar 2019 07:37:55 +0000 (07:37 +0000)
Support forwarding of bumped, re­encrypted HTTPS requests through a
cache_peer using a standard HTTP CONNECT tunnel.

The new Http::Tunneler class establishes HTTP CONNECT tunnels through
forward proxies. It is used by TunnelStateData and FwdState classes.

Just like before these changes, when a cache_peer replies to CONNECT
with an error response, only the HTTP response headers are forwarded to
the client, and then the connection is closed.

No support for triggering client authentication when a cache_peer
configuration instructs the bumping Squid to relay authentication info
contained in client CONNECT request. The bumping Squid still responds
with HTTP 200 (Connection Established) to the client CONNECT request (to
see TLS client handshake) _before_ selecting the cache_peer.

HTTPS cache_peers are not yet supported primarily because Squid cannot
do TLS-in-TLS with a single fde::ssl state; SslBump and the HTTPS proxy
client/tunneling code would need a dedicated TLS connection each.

Also fixed delay pools for tunneled traffic.

This is a Measurement Factory project.

34 files changed:
src/Debug.h
src/FwdState.cc
src/FwdState.h
src/HttpRequest.cc
src/HttpRequest.h
src/RequestFlags.h
src/client_side.cc
src/client_side.h
src/client_side_request.cc
src/clients/HttpTunneler.cc [new file with mode: 0644]
src/clients/HttpTunneler.h [new file with mode: 0644]
src/clients/HttpTunnelerAnswer.cc [new file with mode: 0644]
src/clients/HttpTunnelerAnswer.h [new file with mode: 0644]
src/clients/Makefile.am
src/clients/forward.h
src/comm/ConnOpener.cc
src/comm/ConnOpener.h
src/comm/Read.cc
src/comm/Read.h
src/debug.cc
src/err_type.h
src/errorpage.cc
src/errorpage.h
src/http.cc
src/http/StateFlags.h
src/security/BlindPeerConnector.cc
src/security/PeerConnector.cc
src/security/PeerConnector.h
src/ssl/ServerBump.cc
src/ssl/ServerBump.h
src/tests/stub_client_side.cc
src/tests/stub_libcomm.cc
src/tests/stub_libsecurity.cc
src/tunnel.cc

index 7fb1ed54944da7caf0bc7784ec7b67d6b362c726..756f498860e537f9ddee030bf40f135b038c574d 100644 (file)
@@ -185,7 +185,7 @@ class Raw
 {
 public:
     Raw(const char *label, const char *data, const size_t size):
-        level(-1), label_(label), data_(data), size_(size), useHex_(false) {}
+        level(-1), label_(label), data_(data), size_(size), useHex_(false), useGap_(true) {}
 
     /// limit data printing to at least the given debugging level
     Raw &minLevel(const int aLevel) { level = aLevel; return *this; }
@@ -193,6 +193,8 @@ public:
     /// print data using two hex digits per byte (decoder: xxd -r -p)
     Raw &hex() { useHex_ = true; return *this; }
 
+    Raw &gap(bool useGap = true) { useGap_ = useGap; return *this; }
+
     /// If debugging is prohibited by the current debugs() or section level,
     /// prints nothing. Otherwise, dumps data using one of these formats:
     ///   " label[size]=data" if label was set and data size is positive
@@ -213,6 +215,7 @@ private:
     const char *data_; ///< raw data to be printed
     size_t size_; ///< data length
     bool useHex_; ///< whether hex() has been called
+    bool useGap_; ///< whether to print leading space if label is missing
 };
 
 inline
index 6c7a0147c47dacf91b4dd283cae21310411bb825..9c4a5cb214b18eef0a38d31b27d777f7a9d7de7f 100644 (file)
@@ -18,6 +18,7 @@
 #include "CachePeer.h"
 #include "client_side.h"
 #include "clients/forward.h"
+#include "clients/HttpTunneler.h"
 #include "comm/Connection.h"
 #include "comm/ConnOpener.h"
 #include "comm/Loops.h"
@@ -288,11 +289,6 @@ FwdState::~FwdState()
 
     entry = NULL;
 
-    if (calls.connector != NULL) {
-        calls.connector->cancel("FwdState destructed");
-        calls.connector = NULL;
-    }
-
     if (Comm::IsConnOpen(serverConn))
         closeServerConnection("~FwdState");
 
@@ -709,6 +705,7 @@ FwdState::handleUnregisteredServerEnd()
     retryOrBail();
 }
 
+/// handles an established TCP connection to peer (including origin servers)
 void
 FwdState::connectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, int xerrno)
 {
@@ -737,21 +734,104 @@ FwdState::connectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, in
     // only set when we dispatch the request to an existing (pinned) connection.
     assert(!request->flags.pinned);
 
+    if (const CachePeer *peer = serverConnection()->getPeer()) {
+        // Assume that it is only possible for the client-first from the
+        // bumping modes to try connect to a remote server. The bumped
+        // requests with other modes are using pinned connections or fails.
+        const bool clientFirstBump = request->flags.sslBumped;
+        // We need a CONNECT tunnel to send encrypted traffic through a proxy,
+        // but we do not support TLS inside TLS, so we exclude HTTPS proxies.
+        const bool originWantsEncryptedTraffic =
+            request->method == Http::METHOD_CONNECT ||
+            request->flags.sslPeek ||
+            clientFirstBump;
+        if (originWantsEncryptedTraffic && // the "encrypted traffic" part
+                !peer->options.originserver && // the "through a proxy" part
+                !peer->secure.encryptTransport) // the "exclude HTTPS proxies" part
+            return establishTunnelThruProxy();
+    }
+
+    secureConnectionToPeerIfNeeded();
+}
+
+void
+FwdState::establishTunnelThruProxy()
+{
+    AsyncCall::Pointer callback = asyncCall(17,4,
+                                            "FwdState::tunnelEstablishmentDone",
+                                            Http::Tunneler::CbDialer<FwdState>(&FwdState::tunnelEstablishmentDone, this));
+    HttpRequest::Pointer requestPointer = request;
+    const auto tunneler = new Http::Tunneler(serverConnection(), requestPointer, callback, connectingTimeout(serverConnection()), al);
+#if USE_DELAY_POOLS
+    Must(serverConnection()->getPeer());
+    if (!serverConnection()->getPeer()->options.no_delay)
+        tunneler->setDelayId(entry->mem_obj->mostBytesAllowed());
+#endif
+    AsyncJob::Start(tunneler);
+    // and wait for the tunnelEstablishmentDone() call
+}
+
+/// resumes operations after the (possibly failed) HTTP CONNECT exchange
+void
+FwdState::tunnelEstablishmentDone(Http::TunnelerAnswer &answer)
+{
+    if (answer.positive()) {
+        if (answer.leftovers.isEmpty()) {
+            secureConnectionToPeerIfNeeded();
+            return;
+        }
+        // This should not happen because TLS servers do not speak first. If we
+        // have to handle this, then pass answer.leftovers via a PeerConnector
+        // to ServerBio. See ClientBio::setReadBufData().
+        static int occurrences = 0;
+        const auto level = (occurrences++ < 100) ? DBG_IMPORTANT : 2;
+        debugs(17, level, "ERROR: Early data after CONNECT response. " <<
+               "Found " << answer.leftovers.length() << " bytes. " <<
+               "Closing " << serverConnection());
+        fail(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request, al));
+        closeServerConnection("found early data after CONNECT response");
+        retryOrBail();
+        return;
+    }
+
+    // TODO: Reuse to-peer connections after a CONNECT error response.
+
+    if (const auto peer = serverConnection()->getPeer())
+        peerConnectFailed(peer);
+
+    const auto error = answer.squidError.get();
+    Must(error);
+    answer.squidError.clear(); // preserve error for fail()
+    fail(error);
+    closeServerConnection("Squid-generated CONNECT error");
+    retryOrBail();
+}
+
+/// handles an established TCP connection to peer (including origin servers)
+void
+FwdState::secureConnectionToPeerIfNeeded()
+{
+    assert(!request->flags.pinned);
+
     const CachePeer *p = serverConnection()->getPeer();
     const bool peerWantsTls = p && p->secure.encryptTransport;
     // userWillTlsToPeerForUs assumes CONNECT == HTTPS
     const bool userWillTlsToPeerForUs = p && p->options.originserver &&
                                         request->method == Http::METHOD_CONNECT;
     const bool needTlsToPeer = peerWantsTls && !userWillTlsToPeerForUs;
-    const bool needTlsToOrigin = !p && request->url.getScheme() == AnyP::PROTO_HTTPS;
-    if (needTlsToPeer || needTlsToOrigin || request->flags.sslPeek) {
+    const bool clientFirstBump = request->flags.sslBumped; // client-first (already) bumped connection
+    const bool needsBump = request->flags.sslPeek || clientFirstBump;
+
+    // 'GET https://...' requests. If a peer is used the request is forwarded
+    // as is
+    const bool needTlsToOrigin = !p && request->url.getScheme() == AnyP::PROTO_HTTPS && !clientFirstBump;
+
+    if (needTlsToPeer || needTlsToOrigin || needsBump) {
         HttpRequest::Pointer requestPointer = request;
         AsyncCall::Pointer callback = asyncCall(17,4,
                                                 "FwdState::ConnectedToPeer",
                                                 FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this));
-        // Use positive timeout when less than one second is left.
-        const time_t connTimeout = serverDestinations[0]->connectTimeout(start_t);
-        const time_t sslNegotiationTimeout = positiveTimeout(connTimeout);
+        const auto sslNegotiationTimeout = connectingTimeout(serverDestinations[0]);
         Security::PeerConnector *connector = nullptr;
 #if USE_OPENSSL
         if (request->flags.sslPeek)
@@ -764,10 +844,10 @@ FwdState::connectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, in
     }
 
     // if not encrypting just run the post-connect actions
-    Security::EncryptorAnswer nil;
-    connectedToPeer(nil);
+    successfullyConnectedToPeer();
 }
 
+/// called when all negotiations with the TLS-speaking peer have been completed
 void
 FwdState::connectedToPeer(Security::EncryptorAnswer &answer)
 {
@@ -790,6 +870,13 @@ FwdState::connectedToPeer(Security::EncryptorAnswer &answer)
         return;
     }
 
+    successfullyConnectedToPeer();
+}
+
+/// called when all negotiations with the peer have been completed
+void
+FwdState::successfullyConnectedToPeer()
+{
     // should reach ConnStateData before the dispatched Client job starts
     CallJobHere1(17, 4, request->clientConnectionManager, ConnStateData,
                  ConnStateData::notePeerConnection, serverConnection());
@@ -877,13 +964,27 @@ FwdState::connectStart()
 
     request->hier.startPeerClock();
 
-    // Do not fowrward bumped connections to parent proxy unless it is an
-    // origin server
-    if (serverDestinations[0]->getPeer() && !serverDestinations[0]->getPeer()->options.originserver && request->flags.sslBumped) {
-        debugs(50, 4, "fwdConnectStart: Ssl bumped connections through parent proxy are not allowed");
-        const auto anErr = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request, al);
-        fail(anErr);
-        stopAndDestroy("SslBump misconfiguration");
+    // Requests bumped at step2+ require their pinned connection. Since we
+    // failed to reuse the pinned connection, we now must terminate the
+    // bumped request. For client-first and step1 bumped requests, the
+    // from-client connection is already bumped, but the connection to the
+    // server is not established/pinned so they must be excluded. We can
+    // recognize step1 bumping by nil ConnStateData::serverBump().
+#if USE_OPENSSL
+    const auto clientFirstBump = request->clientConnectionManager.valid() &&
+        (request->clientConnectionManager->sslBumpMode == Ssl::bumpClientFirst ||
+         (request->clientConnectionManager->sslBumpMode == Ssl::bumpBump && !request->clientConnectionManager->serverBump())
+            );
+#else
+    const auto clientFirstBump = false;
+#endif /* USE_OPENSSL */
+    if (request->flags.sslBumped && !clientFirstBump) {
+        // TODO: Factor out/reuse as Occasionally(DBG_IMPORTANT, 2[, occurrences]).
+        static int occurrences = 0;
+        const auto level = (occurrences++ < 100) ? DBG_IMPORTANT : 2;
+        debugs(17, level, "BUG: Lost previously bumped from-Squid connection. Rejecting bumped request.");
+        fail(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request, al));
+        self = nullptr; // refcounted
         return;
     }
 
@@ -892,11 +993,12 @@ FwdState::connectStart()
     if (!serverDestinations[0]->getPeer())
         host = request->url.host();
 
+    bool bumpThroughPeer = request->flags.sslBumped && serverDestinations[0]->getPeer();
     Comm::ConnectionPointer temp;
     // Avoid pconns after races so that the same client does not suffer twice.
     // This does not increase the total number of connections because we just
     // closed the connection that failed the race. And re-pinning assumes this.
-    if (pconnRace != raceHappened)
+    if (pconnRace != raceHappened && !bumpThroughPeer)
         temp = pconnPop(serverDestinations[0], host);
 
     const bool openedPconn = Comm::IsConnOpen(temp);
@@ -929,9 +1031,9 @@ FwdState::connectStart()
 
     GetMarkingsToServer(request, *serverDestinations[0]);
 
-    calls.connector = commCbCall(17,3, "fwdConnectDoneWrapper", CommConnectCbPtrFun(fwdConnectDoneWrapper, this));
-    const time_t connTimeout = serverDestinations[0]->connectTimeout(start_t);
-    Comm::ConnOpener *cs = new Comm::ConnOpener(serverDestinations[0], calls.connector, connTimeout);
+    const AsyncCall::Pointer connector = commCbCall(17,3, "fwdConnectDoneWrapper", CommConnectCbPtrFun(fwdConnectDoneWrapper, this));
+    const auto connTimeout = connectingTimeout(serverDestinations[0]);
+    const auto cs = new Comm::ConnOpener(serverDestinations[0], connector, connTimeout);
     if (host)
         cs->setHost(host);
     ++n_tries;
@@ -1041,17 +1143,13 @@ FwdState::dispatch()
     }
 #endif
 
-    if (serverConnection()->getPeer() != NULL) {
-        ++ serverConnection()->getPeer()->stats.fetches;
-        request->peer_login = serverConnection()->getPeer()->login;
-        request->peer_domain = serverConnection()->getPeer()->domain;
-        request->flags.auth_no_keytab = serverConnection()->getPeer()->options.auth_no_keytab;
+    if (const auto peer = serverConnection()->getPeer()) {
+        ++peer->stats.fetches;
+        request->prepForPeering(*peer);
         httpStart(this);
     } else {
         assert(!request->flags.sslPeek);
-        request->peer_login = NULL;
-        request->peer_domain = NULL;
-        request->flags.auth_no_keytab = 0;
+        request->prepForDirect();
 
         switch (request->url.getScheme()) {
 
@@ -1315,6 +1413,13 @@ FwdState::pinnedCanRetry() const
     return true;
 }
 
+time_t
+FwdState::connectingTimeout(const Comm::ConnectionPointer &conn) const
+{
+    const auto connTimeout = conn->connectTimeout(start_t);
+    return positiveTimeout(connTimeout);
+}
+
 /**** PRIVATE NON-MEMBER FUNCTIONS ********************************************/
 
 /*
index 57707026b720c06ffa4d7486058931c3965077d6..8722fd41e0fbb07c5a51873f60605e82eb316c6a 100644 (file)
@@ -10,6 +10,7 @@
 #define SQUID_FORWARD_H
 
 #include "base/RefCount.h"
+#include "clients/forward.h"
 #include "comm.h"
 #include "comm/Connection.h"
 #include "err_type.h"
@@ -134,6 +135,11 @@ private:
     void connectedToPeer(Security::EncryptorAnswer &answer);
     static void RegisterWithCacheManager(void);
 
+    void establishTunnelThruProxy();
+    void tunnelEstablishmentDone(Http::TunnelerAnswer &answer);
+    void secureConnectionToPeerIfNeeded();
+    void successfullyConnectedToPeer();
+
     /// stops monitoring server connection for closure and updates pconn stats
     void closeServerConnection(const char *reason);
 
@@ -143,6 +149,9 @@ private:
     /// whether we have used up all permitted forwarding attempts
     bool exhaustedTries() const;
 
+    /// \returns the time left for this connection to become connected or 1 second if it is less than one second left
+    time_t connectingTimeout(const Comm::ConnectionPointer &conn) const;
+
 public:
     StoreEntry *entry;
     HttpRequest *request;
@@ -157,11 +166,6 @@ private:
     time_t start_t;
     int n_tries; ///< the number of forwarding attempts so far
 
-    // AsyncCalls which we set and may need cancelling.
-    struct {
-        AsyncCall::Pointer connector;  ///< a call linking us to the ConnOpener producing serverConn.
-    } calls;
-
     struct {
         bool connected_okay; ///< TCP link ever opened properly. This affects retry of POST,PUT,CONNECT,etc
         bool dont_retry;
index 80f77768738f7d0959459c02cd993c5defe13da8..5eaabe824af74c205bc34fbbd8f379601d301a95 100644 (file)
@@ -12,6 +12,7 @@
 #include "AccessLogEntry.h"
 #include "acl/AclSizeLimit.h"
 #include "acl/FilledChecklist.h"
+#include "CachePeer.h"
 #include "client_side.h"
 #include "client_side_request.h"
 #include "dns/LookupDetails.h"
@@ -452,6 +453,25 @@ HttpRequest::bodyNibbled() const
     return body_pipe != NULL && body_pipe->consumedSize() > 0;
 }
 
+void
+HttpRequest::prepForPeering(const CachePeer &peer)
+{
+    // XXX: Saving two pointers to memory controlled by an independent object.
+    peer_login = peer.login;
+    peer_domain = peer.domain;
+    flags.auth_no_keytab = peer.options.auth_no_keytab;
+    debugs(11, 4, this << " to " << peer.host << (!peer.options.originserver ? " proxy" : " origin"));
+}
+
+void
+HttpRequest::prepForDirect()
+{
+    peer_login = nullptr;
+    peer_domain = nullptr;
+    flags.auth_no_keytab = false;
+    debugs(11, 4, this);
+}
+
 void
 HttpRequest::detailError(err_type aType, int aDetail)
 {
index 909a17ef642273baaf1ca430f4208d02af9bc0fe..13e9d4eee1f150aefcbc3aae6c1c88658750d97d 100644 (file)
 #include "eui/Eui64.h"
 #endif
 
-class ConnStateData;
-class Downloader;
 class AccessLogEntry;
 typedef RefCount<AccessLogEntry> AccessLogEntryPointer;
+class CachePeer;
+class ConnStateData;
+class Downloader;
 
 /*  Http Request */
 void httpRequestPack(void *obj, Packable *p);
@@ -87,6 +88,13 @@ public:
     Adaptation::Icap::History::Pointer icapHistory() const;
 #endif
 
+    /* If a request goes through several destinations, then the following two
+     * methods will be called several times, in destinations-dependent order. */
+    /// get ready to be sent to the given cache_peer, including originserver
+    void prepForPeering(const CachePeer &peer);
+    /// get ready to be sent directly to an origin server, excluding originserver
+    void prepForDirect();
+
     void recordLookup(const Dns::LookupDetails &detail);
 
     /// sets error detail if no earlier detail was available
index 6b984e36357291b0b44bb6d8a43eb3037e55aa5d..b4a880c82cbbce65f40fb035e62b424ee6c670ae 100644 (file)
@@ -36,8 +36,6 @@ public:
     bool loopDetected = false;
     /** the connection can be kept alive */
     bool proxyKeepalive = false;
-    /* this should be killed, also in httpstateflags */
-    bool proxying = false;
     /** content has expired, need to refresh it */
     bool refresh = false;
     /** request was redirected by redirectors */
index 5610bc4c0862c9caf2bc0516e12300b3d17de698..4aaf8f6d668122a330d73bc6b7c09ab3fb90656c 100644 (file)
@@ -2913,9 +2913,11 @@ ConnStateData::getSslContextDone(Security::ContextPointer &ctx)
 }
 
 void
-ConnStateData::switchToHttps(HttpRequest *request, Ssl::BumpMode bumpServerMode)
+ConnStateData::switchToHttps(ClientHttpRequest *http, Ssl::BumpMode bumpServerMode)
 {
     assert(!switchedToHttps_);
+    Must(http->request);
+    auto &request = http->request;
 
     sslConnectHostOrIp = request->url.host();
     tlsConnectPort = request->url.port();
@@ -2934,10 +2936,10 @@ ConnStateData::switchToHttps(HttpRequest *request, Ssl::BumpMode bumpServerMode)
     // without even peeking at the origin server certificate.
     if (bumpServerMode == Ssl::bumpServerFirst && !sslServerBump) {
         request->flags.sslPeek = true;
-        sslServerBump = new Ssl::ServerBump(request);
+        sslServerBump = new Ssl::ServerBump(http);
     } else if (bumpServerMode == Ssl::bumpPeek || bumpServerMode == Ssl::bumpStare) {
         request->flags.sslPeek = true;
-        sslServerBump = new Ssl::ServerBump(request, NULL, bumpServerMode);
+        sslServerBump = new Ssl::ServerBump(http, nullptr, bumpServerMode);
     }
 
     // commSetConnTimeout() was called for this request before we switched.
@@ -3010,8 +3012,10 @@ ConnStateData::parseTlsHandshake()
         getSslContextStart();
         return;
     } else if (sslServerBump->act.step1 == Ssl::bumpServerFirst) {
+        Http::StreamPointer context = pipeline.front();
+        ClientHttpRequest *http = context ? context->http : nullptr;
         // will call httpsPeeked() with certificate and connection, eventually
-        FwdState::fwdStart(clientConnection, sslServerBump->entry, sslServerBump->request.getRaw());
+        FwdState::Start(clientConnection, sslServerBump->entry, sslServerBump->request.getRaw(), http ? http->al : nullptr);
     } else {
         Must(sslServerBump->act.step1 == Ssl::bumpPeek || sslServerBump->act.step1 == Ssl::bumpStare);
         startPeekAndSplice();
index ba955dc8ae6ed380c37285c9d8c9cacfe0af559d..318730dd36bf080954b8cef6c9f46933642f0c50 100644 (file)
@@ -247,7 +247,7 @@ public:
     /// Proccess response from ssl_crtd.
     void sslCrtdHandleReply(const Helper::Reply &reply);
 
-    void switchToHttps(HttpRequest *request, Ssl::BumpMode bumpServerMode);
+    void switchToHttps(ClientHttpRequest *, Ssl::BumpMode bumpServerMode);
     void parseTlsHandshake();
     bool switchedToHttps() const { return switchedToHttps_; }
     Ssl::ServerBump *serverBump() {return sslServerBump;}
index 0cc0a0412580eda370e356f296af434944f2dc3a..e6bf50203a4812e9f7cfd161767dc3ab6044f8ef 100644 (file)
@@ -1584,7 +1584,7 @@ ClientHttpRequest::sslBumpEstablish(Comm::Flag errflag)
 #endif
 
     assert(sslBumpNeeded());
-    getConn()->switchToHttps(request, sslBumpNeed_);
+    getConn()->switchToHttps(this, sslBumpNeed_);
 }
 
 void
@@ -1849,7 +1849,7 @@ ClientHttpRequest::doCallouts()
             // We have to serve an error, so bump the client first.
             sslBumpNeed(Ssl::bumpClientFirst);
             // set final error but delay sending until we bump
-            Ssl::ServerBump *srvBump = new Ssl::ServerBump(request, e, Ssl::bumpClientFirst);
+            Ssl::ServerBump *srvBump = new Ssl::ServerBump(this, e, Ssl::bumpClientFirst);
             errorAppendEntry(e, calloutContext->error);
             calloutContext->error = NULL;
             getConn()->setServerBump(srvBump);
diff --git a/src/clients/HttpTunneler.cc b/src/clients/HttpTunneler.cc
new file mode 100644 (file)
index 0000000..fb0c8d3
--- /dev/null
@@ -0,0 +1,395 @@
+/*
+ * 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 "CachePeer.h"
+#include "clients/HttpTunneler.h"
+#include "comm/Read.h"
+#include "comm/Write.h"
+#include "errorpage.h"
+#include "fd.h"
+#include "fde.h"
+#include "http.h"
+#include "http/one/ResponseParser.h"
+#include "http/StateFlags.h"
+#include "HttpRequest.h"
+#include "StatCounters.h"
+#include "SquidConfig.h"
+
+CBDATA_NAMESPACED_CLASS_INIT(Http, Tunneler);
+
+Http::Tunneler::Tunneler(const Comm::ConnectionPointer &conn, const HttpRequest::Pointer &req, AsyncCall::Pointer &aCallback, time_t timeout, const AccessLogEntryPointer &alp):
+    AsyncJob("Http::Tunneler"),
+    connection(conn),
+    request(req),
+    callback(aCallback),
+    lifetimeLimit(timeout),
+    al(alp),
+    startTime(squid_curtime),
+    requestWritten(false),
+    tunnelEstablished(false)
+{
+    debugs(83, 5, "Http::Tunneler constructed, this=" << (void*)this);
+    // detect callers supplying cb dialers that are not our CbDialer
+    assert(request);
+    assert(connection);
+    assert(callback);
+    assert(dynamic_cast<Http::TunnelerAnswer *>(callback->getDialer()));
+    url = request->url.authority();
+}
+
+Http::Tunneler::~Tunneler()
+{
+    debugs(83, 5, "Http::Tunneler destructed, this=" << (void*)this);
+}
+
+bool
+Http::Tunneler::doneAll() const
+{
+    return !callback || (requestWritten && tunnelEstablished);
+}
+
+/// convenience method to get to the answer fields
+Http::TunnelerAnswer &
+Http::Tunneler::answer()
+{
+    Must(callback);
+    const auto tunnelerAnswer = dynamic_cast<Http::TunnelerAnswer *>(callback->getDialer());
+    Must(tunnelerAnswer);
+    return *tunnelerAnswer;
+}
+
+void
+Http::Tunneler::start()
+{
+    AsyncJob::start();
+
+    Must(al);
+    Must(url.length());
+    Must(lifetimeLimit >= 0);
+
+    const auto peer = connection->getPeer();
+    Must(peer); // bail if our peer was reconfigured away
+    request->prepForPeering(*peer);
+
+    watchForClosures();
+    writeRequest();
+    startReadingResponse();
+}
+
+void
+Http::Tunneler::handleConnectionClosure(const CommCloseCbParams &params)
+{
+    mustStop("server connection gone");
+    callback = nullptr; // the caller must monitor closures
+}
+
+/// make sure we quit if/when the connection is gone
+void
+Http::Tunneler::watchForClosures()
+{
+    Must(Comm::IsConnOpen(connection));
+    Must(!fd_table[connection->fd].closing());
+
+    debugs(83, 5, connection);
+
+    Must(!closer);
+    typedef CommCbMemFunT<Http::Tunneler, CommCloseCbParams> Dialer;
+    closer = JobCallback(9, 5, Dialer, this, Http::Tunneler::handleConnectionClosure);
+    comm_add_close_handler(connection->fd, closer);
+}
+
+void
+Http::Tunneler::handleException(const std::exception& e)
+{
+    debugs(83, 2, e.what() << status());
+    connection->close();
+    bailWith(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
+}
+
+void
+Http::Tunneler::startReadingResponse()
+{
+    debugs(83, 5, connection << status());
+
+    readBuf.reserveCapacity(SQUID_TCP_SO_RCVBUF);
+    readMore();
+}
+
+void
+Http::Tunneler::writeRequest()
+{
+    debugs(83, 5, connection);
+
+    HttpHeader hdr_out(hoRequest);
+    Http::StateFlags flags;
+    flags.peering = true;
+    // flags.tunneling = false; // the CONNECT request itself is not tunneled
+    // flags.toOrigin = false; // the next HTTP hop is a non-originserver peer
+    MemBuf mb;
+    mb.init();
+    mb.appendf("CONNECT %s HTTP/1.1\r\n", url.c_str());
+    HttpStateData::httpBuildRequestHeader(request.getRaw(),
+                                          nullptr, // StoreEntry
+                                          al,
+                                          &hdr_out,
+                                          flags);
+    hdr_out.packInto(&mb);
+    hdr_out.clean();
+    mb.append("\r\n", 2);
+
+    debugs(11, 2, "Tunnel Server REQUEST: " << connection <<
+           ":\n----------\n" << mb.buf << "\n----------");
+    fd_note(connection->fd, "Tunnel Server CONNECT");
+
+    typedef CommCbMemFunT<Http::Tunneler, CommIoCbParams> Dialer;
+    writer = JobCallback(5, 5, Dialer, this, Http::Tunneler::handleWrittenRequest);
+    Comm::Write(connection, &mb, writer);
+}
+
+/// Called when we are done writing a CONNECT request header to a peer.
+void
+Http::Tunneler::handleWrittenRequest(const CommIoCbParams &io)
+{
+    Must(writer);
+    writer = nullptr;
+
+    if (io.flag == Comm::ERR_CLOSING)
+        return;
+
+    request->hier.notePeerWrite();
+
+    if (io.flag != Comm::OK) {
+        const auto error = new ErrorState(ERR_WRITE_ERROR, Http::scBadGateway, request.getRaw(), al);
+        error->xerrno = io.xerrno;
+        bailWith(error);
+        return;
+    }
+
+    statCounter.server.all.kbytes_out += io.size;
+    statCounter.server.other.kbytes_out += io.size;
+    requestWritten = true;
+    debugs(83, 5, status());
+}
+
+/// Called when we read [a part of] CONNECT response from the peer
+void
+Http::Tunneler::handleReadyRead(const CommIoCbParams &io)
+{
+    Must(reader);
+    reader = nullptr;
+
+    if (io.flag == Comm::ERR_CLOSING)
+        return;
+
+    CommIoCbParams rd(this);
+    rd.conn = io.conn;
+#if USE_DELAY_POOLS
+    rd.size = delayId.bytesWanted(1, readBuf.spaceSize());
+#else
+    rd.size = readBuf.spaceSize();
+#endif
+
+    switch (Comm::ReadNow(rd, readBuf)) {
+    case Comm::INPROGRESS:
+        readMore();
+        return;
+
+    case Comm::OK: {
+#if USE_DELAY_POOLS
+        delayId.bytesIn(rd.size);
+#endif
+        statCounter.server.all.kbytes_in += rd.size;
+        statCounter.server.other.kbytes_in += rd.size; // TODO: other or http?
+        request->hier.notePeerRead();
+        handleResponse(false);
+        return;
+    }
+
+    case Comm::ENDFILE: {
+        // TODO: Should we (and everybody else) call request->hier.notePeerRead() on zero reads?
+        handleResponse(true);
+        return;
+    }
+
+    // case Comm::COMM_ERROR:
+    default: // no other flags should ever occur
+    {
+        const auto error = new ErrorState(ERR_READ_ERROR, Http::scBadGateway, request.getRaw(), al);
+        error->xerrno = rd.xerrno;
+        bailWith(error);
+        return;
+    }
+    }
+
+    assert(false); // not reached
+}
+
+void
+Http::Tunneler::readMore()
+{
+    Must(Comm::IsConnOpen(connection));
+    Must(!fd_table[connection->fd].closing());
+    Must(!reader);
+
+    typedef CommCbMemFunT<Http::Tunneler, CommIoCbParams> Dialer;
+    reader = JobCallback(93, 3, Dialer, this, Http::Tunneler::handleReadyRead);
+    Comm::Read(connection, reader);
+
+    AsyncCall::Pointer nil;
+    const auto timeout = Comm::MortalReadTimeout(startTime, lifetimeLimit);
+    commSetConnTimeout(connection, timeout, nil);
+}
+
+/// Parses [possibly incomplete] CONNECT response and reacts to it.
+void
+Http::Tunneler::handleResponse(const bool eof)
+{
+    // mimic the basic parts of HttpStateData::processReplyHeader()
+    if (hp == nullptr)
+        hp = new Http1::ResponseParser;
+
+    auto parsedOk = hp->parse(readBuf); // may be refined below
+    readBuf = hp->remaining();
+    if (hp->needsMoreData()) {
+        if (!eof) {
+            if (readBuf.length() >= SQUID_TCP_SO_RCVBUF) {
+                bailOnResponseError("huge CONNECT response from peer", nullptr);
+                return;
+            }
+            readMore();
+            return;
+        }
+
+        //eof, handle truncated response
+        readBuf.append("\r\n\r\n", 4);
+        parsedOk = hp->parse(readBuf);
+        readBuf.clear();
+    }
+
+    if (!parsedOk) {
+        bailOnResponseError("malformed CONNECT response from peer", nullptr);
+        return;
+    }
+
+    HttpReply::Pointer rep = new HttpReply;
+    rep->sources |= Http::Message::srcHttp;
+    rep->sline.set(hp->messageProtocol(), hp->messageStatus());
+    if (!rep->parseHeader(*hp) && rep->sline.status() == Http::scOkay) {
+        bailOnResponseError("malformed CONNECT response from peer", nullptr);
+        return;
+    }
+
+    // CONNECT response was successfully parsed
+    auto &futureAnswer = answer();
+    futureAnswer.peerResponseStatus = rep->sline.status();
+    request->hier.peer_reply_status = rep->sline.status();
+
+    debugs(11, 2, "Tunnel Server " << connection);
+    debugs(11, 2, "Tunnel Server RESPONSE:\n---------\n" <<
+           Raw(nullptr, readBuf.rawContent(), rep->hdr_sz).minLevel(2).gap(false) <<
+           "----------");
+
+    // bail if we did not get an HTTP 200 (Connection Established) response
+    if (rep->sline.status() != Http::scOkay) {
+        // TODO: To reuse the connection, extract the whole error response.
+        bailOnResponseError("unsupported CONNECT response status code", rep.getRaw());
+        return;
+    }
+
+    // preserve any bytes sent by the server after the CONNECT response
+    futureAnswer.leftovers = readBuf;
+
+    tunnelEstablished = true;
+    debugs(83, 5, status());
+}
+
+void
+Http::Tunneler::bailOnResponseError(const char *error, HttpReply *errorReply)
+{
+    debugs(83, 3, error << status());
+
+    ErrorState *err;
+    if (errorReply) {
+        err = new ErrorState(request.getRaw(), errorReply);
+    } else {
+        // with no reply suitable for relaying, answer with 502 (Bad Gateway)
+        err = new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw(), al);
+    }
+    bailWith(err);
+}
+
+void
+Http::Tunneler::bailWith(ErrorState *error)
+{
+    Must(error);
+    answer().squidError = error;
+    callBack();
+}
+
+void
+Http::Tunneler::callBack()
+{
+    debugs(83, 5, connection << status());
+    auto cb = callback;
+    callback = nullptr;
+    ScheduleCallHere(cb);
+}
+
+void
+Http::Tunneler::swanSong()
+{
+    AsyncJob::swanSong();
+
+    if (callback) {
+        if (requestWritten && tunnelEstablished) {
+            assert(answer().positive());
+            callBack(); // success
+        } else {
+            // we should have bailed when we discovered the job-killing problem
+            debugs(83, DBG_IMPORTANT, "BUG: Unexpected state while establishing a CONNECT tunnel " << connection << status());
+            bailWith(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
+        }
+        assert(!callback);
+    }
+
+    if (closer) {
+        comm_remove_close_handler(connection->fd, closer);
+        closer = nullptr;
+    }
+
+    if (reader) {
+        Comm::ReadCancel(connection->fd, reader);
+        reader = nullptr;
+    }
+}
+
+const char *
+Http::Tunneler::status() const
+{
+    static MemBuf buf;
+    buf.reset();
+
+    // TODO: redesign AsyncJob::status() API to avoid
+    // id and stop reason reporting duplication.
+    buf.append(" [state:", 8);
+    if (requestWritten) buf.append("w", 1); // request sent
+    if (tunnelEstablished) buf.append("t", 1); // tunnel established
+    if (!callback) buf.append("x", 1); // caller informed
+    if (stopReason != nullptr) {
+        buf.append(" stopped, reason:", 16);
+        buf.appendf("%s",stopReason);
+    }
+    if (connection != nullptr)
+        buf.appendf(" FD %d", connection->fd);
+    buf.appendf(" %s%u]", id.prefix(), id.value);
+    buf.terminate();
+
+    return buf.content();
+}
+
diff --git a/src/clients/HttpTunneler.h b/src/clients/HttpTunneler.h
new file mode 100644 (file)
index 0000000..4af96c5
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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_SRC_CLIENTS_HTTP_TUNNELER_H
+#define SQUID_SRC_CLIENTS_HTTP_TUNNELER_H
+
+#include "base/AsyncCbdataCalls.h"
+#include "base/AsyncJob.h"
+#include "clients/forward.h"
+#include "clients/HttpTunnelerAnswer.h"
+#include "CommCalls.h"
+#if USE_DELAY_POOLS
+#include "DelayId.h"
+#endif
+#include "http/forward.h"
+
+class ErrorState;
+class AccessLogEntry;
+typedef RefCount<AccessLogEntry> AccessLogEntryPointer;
+
+namespace Http
+{
+
+/// Establishes an HTTP CONNECT tunnel through a forward proxy.
+///
+/// The caller receives a call back with Http::TunnelerAnswer.
+///
+/// The caller must monitor the connection for closure because this job will not
+/// inform the caller about such events.
+///
+/// This job never closes the connection, even on errors. If a 3rd-party closes
+/// the connection, this job simply quits without informing the caller.
+class Tunneler: virtual public AsyncJob
+{
+    CBDATA_CLASS(Tunneler);
+
+public:
+    /// Callback dialer API to allow Tunneler to set the answer.
+    template <class Initiator>
+    class CbDialer: public CallDialer, public Http::TunnelerAnswer
+    {
+    public:
+        // initiator method to receive our answer
+        typedef void (Initiator::*Method)(Http::TunnelerAnswer &);
+
+        CbDialer(Method method, Initiator *initiator): initiator_(initiator), method_(method) {}
+        virtual ~CbDialer() = default;
+
+        /* CallDialer API */
+        bool canDial(AsyncCall &) { return initiator_.valid(); }
+        void dial(AsyncCall &) {((*initiator_).*method_)(*this); }
+        virtual void print(std::ostream &os) const override {
+            os << '(' << static_cast<const Http::TunnelerAnswer&>(*this) << ')';
+        }
+    private:
+        CbcPointer<Initiator> initiator_; ///< object to deliver the answer to
+        Method method_; ///< initiator_ method to call with the answer
+    };
+
+public:
+    Tunneler(const Comm::ConnectionPointer &conn, const HttpRequestPointer &req, AsyncCall::Pointer &aCallback, time_t timeout, const AccessLogEntryPointer &alp);
+    Tunneler(const Tunneler &) = delete;
+    Tunneler &operator =(const Tunneler &) = delete;
+
+#if USE_DELAY_POOLS
+    void setDelayId(DelayId delay_id) {delayId = delay_id;}
+#endif
+
+protected:
+    /* AsyncJob API */
+    virtual ~Tunneler();
+    virtual void start();
+    virtual bool doneAll() const;
+    virtual void swanSong();
+    virtual const char *status() const;
+
+    void handleConnectionClosure(const CommCloseCbParams&);
+    void watchForClosures();
+    void handleException(const std::exception&);
+    void startReadingResponse();
+    void writeRequest();
+    void handleWrittenRequest(const CommIoCbParams&);
+    void handleReadyRead(const CommIoCbParams&);
+    void readMore();
+    void handleResponse(const bool eof);
+    void bailOnResponseError(const char *error, HttpReply *);
+    void bailWith(ErrorState*);
+    void callBack();
+
+    TunnelerAnswer &answer();
+
+private:
+    AsyncCall::Pointer writer; ///< called when the request has been written
+    AsyncCall::Pointer reader; ///< called when the response should be read
+    AsyncCall::Pointer closer; ///< called when the connection is being closed
+
+    Comm::ConnectionPointer connection; ///< TCP connection to the cache_peer
+    HttpRequestPointer request; ///< peer connection trigger or cause
+    AsyncCall::Pointer callback; ///< we call this with the results
+    SBuf url; ///< request-target for the CONNECT request
+    time_t lifetimeLimit; ///< do not run longer than this
+    AccessLogEntryPointer al; ///< info for the future access.log entry
+#if USE_DELAY_POOLS
+    DelayId delayId;
+#endif
+
+    SBuf readBuf; ///< either unparsed response or post-response bytes
+    /// Parser being used at present to parse the HTTP peer response.
+    Http1::ResponseParserPointer hp;
+
+    const time_t startTime; ///< when the tunnel establishment started
+
+    bool requestWritten; ///< whether we successfully wrote the request
+    bool tunnelEstablished; ///< whether we got a 200 OK response
+};
+
+} // namespace Http
+
+#endif /* SQUID_SRC_CLIENTS_HTTP_TUNNELER_H */
+
diff --git a/src/clients/HttpTunnelerAnswer.cc b/src/clients/HttpTunnelerAnswer.cc
new file mode 100644 (file)
index 0000000..66e605c
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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 "clients/HttpTunnelerAnswer.h"
+#include "comm/Connection.h"
+#include "errorpage.h"
+
+Http::TunnelerAnswer::~TunnelerAnswer()
+{
+    delete squidError.get();
+}
+
+std::ostream &
+Http::operator <<(std::ostream &os, const TunnelerAnswer &answer)
+{
+    os << '[';
+
+    if (const auto squidError = answer.squidError.get()) {
+        os << "SquidErr:" << squidError->page_id;
+    } else {
+        os << "OK";
+        if (const auto extraBytes = answer.leftovers.length())
+            os << '+' << extraBytes;
+    }
+
+    if (answer.peerResponseStatus != Http::scNone)
+        os << ' ' << answer.peerResponseStatus;
+
+    os << ']';
+    return os;
+}
+
diff --git a/src/clients/HttpTunnelerAnswer.h b/src/clients/HttpTunnelerAnswer.h
new file mode 100644 (file)
index 0000000..b549dbb
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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_SRC_CLIENTS_HTTP_TUNNELERANSWER_H
+#define SQUID_SRC_CLIENTS_HTTP_TUNNELERANSWER_H
+
+#include "base/CbcPointer.h"
+#include "comm/forward.h"
+#include "http/StatusCode.h"
+#include "sbuf/SBuf.h"
+
+class ErrorState;
+
+namespace Http {
+
+/// Three mutually exclusive answers are possible:
+///
+/// * Squid-generated error object (TunnelerAnswer::squidError);
+/// * peer-generated error message (TunnelerAnswer::peerError);
+/// * successful tunnel establishment (none of the above are present).
+///
+/// HTTP CONNECT tunnel setup results (supplied via a callback). The tunnel
+/// through the peer was established if and only if the error member is nil.
+class TunnelerAnswer
+{
+public:
+    TunnelerAnswer() {}
+    ~TunnelerAnswer(); ///< deletes squidError if it is still set
+
+    bool positive() const { return !squidError; }
+
+    // Destructor will erase squidError if it is still set. Answer recipients
+    // must clear this member to keep its info.
+    // XXX: We should refcount ErrorState instead of cbdata-protecting it.
+    CbcPointer<ErrorState> squidError; ///< problem details (or nil)
+
+    SBuf leftovers; ///< peer-generated bytes after a positive answer (or empty)
+
+    /// the status code of the successfully parsed CONNECT response (or scNone)
+    StatusCode peerResponseStatus = scNone;
+};
+
+std::ostream &operator <<(std::ostream &, const Http::TunnelerAnswer &);
+
+} // namepace Http
+
+#endif /* SQUID_SRC_CLIENTS_HTTP_TUNNELERANSWER_H */
+
index 2b911de43432e69893985b752caaea4353b13e69..7e4203dac38f1fb662564a2277627f9970db9bcf 100644 (file)
@@ -17,4 +17,8 @@ libclients_la_SOURCES = \
        FtpClient.h \
        FtpGateway.cc \
        FtpRelay.cc \
+       HttpTunneler.cc \
+       HttpTunneler.h \
+       HttpTunnelerAnswer.cc \
+       HttpTunnelerAnswer.h \
        forward.h
index 0c0d916cab8c5059cc7d6cf012d02550afbcfd97..81338c659165a038b789637eb17e8e4da5a7399c 100644 (file)
@@ -18,6 +18,12 @@ class AsyncJob;
 template <class Cbc> class CbcPointer;
 typedef CbcPointer<AsyncJob> AsyncJobPointer;
 
+namespace Http
+{
+class Tunneler;
+class TunnelerAnswer;
+}
+
 namespace Ftp
 {
 
index 9d41cbe3eaeda5848b0d98bbead64878f7d830fb..d494a5a49d798ac83fb5aac123579842b7c3335d 100644 (file)
@@ -30,7 +30,7 @@ class CachePeer;
 
 CBDATA_NAMESPACED_CLASS_INIT(Comm, ConnOpener);
 
-Comm::ConnOpener::ConnOpener(Comm::ConnectionPointer &c, AsyncCall::Pointer &handler, time_t ctimeout) :
+Comm::ConnOpener::ConnOpener(Comm::ConnectionPointer &c, const AsyncCall::Pointer &handler, time_t ctimeout) :
     AsyncJob("Comm::ConnOpener"),
     host_(NULL),
     temporaryFd_(-1),
index 350582f60caf0e5cbbbbbef2b321f9d206e66db5..ee459f6335216caa0995a01fe9c6bbdfae023d81 100644 (file)
@@ -33,7 +33,7 @@ public:
 
     virtual bool doneAll() const;
 
-    ConnOpener(Comm::ConnectionPointer &, AsyncCall::Pointer &handler, time_t connect_timeout);
+    ConnOpener(Comm::ConnectionPointer &, const AsyncCall::Pointer &handler, time_t connect_timeout);
     ~ConnOpener();
 
     void setHost(const char *);    ///< set the hostname note for this connection
index c8f290885e1e75d74ce05cb1d8c1ddf2d1ce8e3f..58f43158c1932c5a6a390adaab144e5ddff98f2e 100644 (file)
@@ -19,6 +19,7 @@
 #include "fd.h"
 #include "fde.h"
 #include "sbuf/SBuf.h"
+#include "SquidConfig.h"
 #include "StatCounters.h"
 
 // Does comm check this fd for read readiness?
@@ -241,3 +242,14 @@ Comm::ReadCancel(int fd, AsyncCall::Pointer &callback)
     Comm::SetSelect(fd, COMM_SELECT_READ, NULL, NULL, 0);
 }
 
+time_t
+Comm::MortalReadTimeout(const time_t startTime, const time_t lifetimeLimit)
+{
+    if (lifetimeLimit > 0) {
+        const time_t timeUsed = (squid_curtime > startTime) ? (squid_curtime - startTime) : 0;
+        const time_t timeLeft = (lifetimeLimit > timeUsed) ? (lifetimeLimit - timeUsed) : 0;
+        return min(::Config.Timeout.read, timeLeft);
+    } else
+        return ::Config.Timeout.read;
+}
+
index ab65864752e2ae6d2f199e4dc03f072347af6c2f..7699f0db65d220f2d93c73e19488c00ab3117fe0 100644 (file)
@@ -50,6 +50,8 @@ void ReadCancel(int fd, AsyncCall::Pointer &callback);
 /// callback handler to process an FD which is available for reading
 extern PF HandleRead;
 
+/// maximum read delay for readers with limited lifetime
+time_t MortalReadTimeout(const time_t startTime, const time_t lifetimeLimit);
 } // namespace Comm
 
 // Legacy API to be removed
index 58205af1e16f7dcc6ac0b4dd83260d2013e330f0..a93cfd498995124728c91cc4db228ea22a5795da 100644 (file)
@@ -895,7 +895,10 @@ Raw::print(std::ostream &os) const
     const int finalLevel = (level >= 0) ? level :
                            (size_ > 40 ? DBG_DATA : Debug::SectionLevel());
     if (finalLevel <= Debug::SectionLevel()) {
-        os << (label_ ? '=' : ' ');
+        if (label_)
+            os << '=';
+        else if (useGap_)
+            os << ' ';
         if (data_) {
             if (useHex_)
                 printHex(os);
index 97e774008d19b7344d55a066f4343c1f1fd5ad84..c6aa94426b854bbac7d1ec79f59dfbd7e82b7a8a 100644 (file)
@@ -76,6 +76,7 @@ typedef enum {
 
     ERR_SECURE_ACCEPT_FAIL, // Rejects the SSL connection intead of error page
     ERR_REQUEST_START_TIMEOUT, // Aborts the connection instead of error page
+    ERR_RELAY_REMOTE, // Sends server reply instead of error page
 
     /* Cache Manager GUI can install a manager index/home page */
     MGR_INDEX,
index 7e867e877fe3ef3ed02ca6bcda9530d0e8fc4eea..1d696609d5c0542cee03d7bb7a8718b2b89a4496 100644 (file)
@@ -671,15 +671,35 @@ ErrorState::NewForwarding(err_type type, HttpRequestPointer &request, const Acce
     return new ErrorState(type, status, request.getRaw(), ale);
 }
 
-ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req, const AccessLogEntry::Pointer &anAle) :
+ErrorState::ErrorState(err_type t) :
     type(t),
     page_id(t),
-    httpStatus(status),
-    callback(nullptr),
-    ale(anAle)
+    callback(nullptr)
+{
+}
+
+ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req, const AccessLogEntry::Pointer &anAle) :
+    ErrorState(t)
 {
     if (page_id >= ERR_MAX && ErrorDynamicPages[page_id - ERR_MAX]->page_redirect != Http::scNone)
         httpStatus = ErrorDynamicPages[page_id - ERR_MAX]->page_redirect;
+    else
+        httpStatus = status;
+
+    if (req) {
+        request = req;
+        src_addr = req->client_addr;
+    }
+
+    ale = anAle;
+}
+
+ErrorState::ErrorState(HttpRequest * req, HttpReply *errorReply) :
+    ErrorState(ERR_RELAY_REMOTE)
+{
+    Must(errorReply);
+    response_ = errorReply;
+    httpStatus = errorReply->sline.status();
 
     if (req) {
         request = req;
@@ -1260,6 +1280,9 @@ ErrorState::validate()
 HttpReply *
 ErrorState::BuildHttpReply()
 {
+    if (response_)
+        return response_.getRaw();
+
     HttpReply *rep = new HttpReply;
     const char *name = errorPageName(page_id);
     /* no LMT for error pages; error pages expire immediately */
index 002db460d65822a8375ccaa3fb3c61b8e5e62d2e..9b7e6f2a1b4078a84f4babb9c7b3f9598cb09439 100644 (file)
@@ -91,8 +91,13 @@ class ErrorState
     CBDATA_CLASS(ErrorState);
 
 public:
+    /// creates an error of type other than ERR_RELAY_REMOTE
     ErrorState(err_type type, Http::StatusCode, HttpRequest * request, const AccessLogEntryPointer &al);
     ErrorState() = delete; // not implemented.
+
+    /// creates an ERR_RELAY_REMOTE error
+    ErrorState(HttpRequest * request, HttpReply *);
+
     ~ErrorState();
 
     /// Creates a general request forwarding error with the right http_status.
@@ -115,6 +120,9 @@ public:
 private:
     typedef ErrorPage::Build Build;
 
+    /// initializations shared by public constructors
+    explicit ErrorState(err_type type);
+
     /// locates the right error page template for this error and compiles it
     SBuf buildBody();
 
@@ -199,6 +207,8 @@ public:
     /// overwrites xerrno; overwritten by detail, if any.
     int detailCode = ERR_DETAIL_NONE;
 
+    HttpReplyPointer response_;
+
 private:
     void noteBuildError_(const char *msg, const char *near, const bool forceBypass);
 
index 0a96bd950e7238607367974768bb03b8ef709d0b..5f462ad80f654698de187bb27c6e9a7b59230f5b 100644 (file)
@@ -99,14 +99,17 @@ HttpStateData::HttpStateData(FwdState *theFwdState) :
     if (fwd->serverConnection() != NULL)
         _peer = cbdataReference(fwd->serverConnection()->getPeer());         /* might be NULL */
 
+    flags.peering =  _peer;
+    flags.tunneling = (_peer && request->flags.sslBumped);
+    flags.toOrigin = (!_peer || _peer->options.originserver || request->flags.sslBumped);
+
     if (_peer) {
-        request->flags.proxying = true;
         /*
          * This NEIGHBOR_PROXY_ONLY check probably shouldn't be here.
          * We might end up getting the object from somewhere else if,
          * for example, the request to this neighbor fails.
          */
-        if (_peer->options.proxy_only)
+        if (!flags.tunneling && _peer->options.proxy_only)
             entry->releaseRequest(true);
 
 #if USE_DELAY_POOLS
@@ -624,11 +627,11 @@ void
 HttpStateData::keepaliveAccounting(HttpReply *reply)
 {
     if (flags.keepalive)
-        if (_peer)
+        if (flags.peering && !flags.tunneling)
             ++ _peer->stats.n_keepalives_sent;
 
     if (reply->keep_alive) {
-        if (_peer)
+        if (flags.peering && !flags.tunneling)
             ++ _peer->stats.n_keepalives_recv;
 
         if (Config.onoff.detect_broken_server_pconns
@@ -643,7 +646,7 @@ HttpStateData::keepaliveAccounting(HttpReply *reply)
 void
 HttpStateData::checkDateSkew(HttpReply *reply)
 {
-    if (reply->date > -1 && !_peer) {
+    if (reply->date > -1 && flags.toOrigin) {
         int skew = abs((int)(reply->date - squid_curtime));
 
         if (skew > 86400)
@@ -844,6 +847,10 @@ HttpStateData::peerSupportsConnectionPinning() const
     if (!_peer)
         return true;
 
+    // we are talking "through" rather than "to" our _peer
+    if (flags.tunneling)
+        return true;
+
     /*If this peer does not support connection pinning (authenticated
       connections) return false
      */
@@ -1654,16 +1661,19 @@ HttpStateData::doneWithServer() const
 static void
 httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHeader * hdr_out, const Http::StateFlags &flags)
 {
-    Http::HdrType header = flags.originpeer ? Http::HdrType::AUTHORIZATION : Http::HdrType::PROXY_AUTHORIZATION;
-
     /* Nothing to do unless we are forwarding to a peer */
-    if (!request->flags.proxying)
+    if (!flags.peering)
+        return;
+
+    // This request is going "through" rather than "to" our _peer.
+    if (flags.tunneling)
         return;
 
     /* Needs to be explicitly enabled */
     if (!request->peer_login)
         return;
 
+    const auto header = flags.toOrigin ? Http::HdrType::AUTHORIZATION : Http::HdrType::PROXY_AUTHORIZATION;
     /* Maybe already dealt with? */
     if (hdr_out->has(header))
         return;
@@ -1672,8 +1682,14 @@ httpFixupAuthentication(HttpRequest * request, const HttpHeader * hdr_in, HttpHe
     if (strcmp(request->peer_login, "PASSTHRU") == 0)
         return;
 
-    /* PROXYPASS is a special case, single-signon to servers with the proxy password (basic only) */
-    if (flags.originpeer && strcmp(request->peer_login, "PROXYPASS") == 0 && hdr_in->has(Http::HdrType::PROXY_AUTHORIZATION)) {
+    // Dangerous and undocumented PROXYPASS is a single-signon to servers with
+    // the proxy password. Only Basic Authentication can work this way. This
+    // statement forwards a "basic" Proxy-Authorization value from our client
+    // to an originserver peer. Other PROXYPASS cases are handled lower.
+    if (flags.toOrigin &&
+        strcmp(request->peer_login, "PROXYPASS") == 0 &&
+        hdr_in->has(Http::HdrType::PROXY_AUTHORIZATION)) {
+
         const char *auth = hdr_in->getStr(Http::HdrType::PROXY_AUTHORIZATION);
 
         if (auth && strncasecmp(auth, "basic ", 6) == 0) {
@@ -1866,7 +1882,7 @@ HttpStateData::httpBuildRequestHeader(HttpRequest * request,
 
     /* append Authorization if known in URL, not in header and going direct */
     if (!hdr_out->has(Http::HdrType::AUTHORIZATION)) {
-        if (!request->flags.proxying && !request->url.userInfo().isEmpty()) {
+        if (flags.toOrigin && !request->url.userInfo().isEmpty()) {
             static char result[base64_encode_len(MAX_URL*2)]; // should be big enough for a single URI segment
             struct base64_encode_ctx ctx;
             base64_encode_init(&ctx);
@@ -1951,7 +1967,7 @@ copyOneHeaderFromClientsideRequestToUpstreamRequest(const HttpHeaderEntry *e, co
          * Only pass on proxy authentication to peers for which
          * authentication forwarding is explicitly enabled
          */
-        if (!flags.originpeer && flags.proxying && request->peer_login &&
+        if (!flags.toOrigin && request->peer_login &&
                 (strcmp(request->peer_login, "PASS") == 0 ||
                  strcmp(request->peer_login, "PROXYPASS") == 0 ||
                  strcmp(request->peer_login, "PASSTHRU") == 0)) {
@@ -1976,10 +1992,11 @@ copyOneHeaderFromClientsideRequestToUpstreamRequest(const HttpHeaderEntry *e, co
         /** \par WWW-Authorization:
          * Pass on WWW authentication */
 
-        if (!flags.originpeer) {
+        if (!flags.toOriginPeer()) {
             hdr_out->addEntry(e->clone());
         } else {
-            /** \note In accelerators, only forward authentication if enabled
+            /** \note Assume that talking to a cache_peer originserver makes
+             * us a reverse proxy and only forward authentication if enabled
              * (see also httpFixupAuthentication for special cases)
              */
             if (request->peer_login &&
@@ -2152,7 +2169,7 @@ HttpStateData::buildRequestPrefix(MemBuf * mb)
      * not the one we are sending. Needs checking.
      */
     const AnyP::ProtocolVersion httpver = Http::ProtocolVersion();
-    const SBuf url(_peer && !_peer->options.originserver ? request->effectiveRequestUri() : request->url.path());
+    const SBuf url(flags.toOrigin ? request->url.path() : request->effectiveRequestUri());
     mb->appendf(SQUIDSBUFPH " " SQUIDSBUFPH " %s/%d.%d\r\n",
                 SQUIDSBUFPRINT(request->method.image()),
                 SQUIDSBUFPRINT(url),
@@ -2215,9 +2232,6 @@ HttpStateData::sendRequest()
                                     Dialer, this,  HttpStateData::wroteLast);
     }
 
-    flags.originpeer = (_peer != NULL && _peer->options.originserver);
-    flags.proxying = (_peer != NULL && !flags.originpeer);
-
     /*
      * Is keep-alive okay for all request methods?
      */
@@ -2227,6 +2241,9 @@ HttpStateData::sendRequest()
         flags.keepalive = request->persistent();
     else if (!Config.onoff.server_pconns)
         flags.keepalive = false;
+    else if (flags.tunneling)
+        // tunneled non pinned bumped requests must not keepalive
+        flags.keepalive = !request->flags.sslBumped;
     else if (_peer == NULL)
         flags.keepalive = true;
     else if (_peer->stats.n_keepalives_sent < 10)
@@ -2235,7 +2252,7 @@ HttpStateData::sendRequest()
              (double) _peer->stats.n_keepalives_sent > 0.50)
         flags.keepalive = true;
 
-    if (_peer) {
+    if (_peer && !flags.tunneling) {
         /*The old code here was
           if (neighborType(_peer, request->url) == PEER_SIBLING && ...
           which is equivalent to:
index cb7cc90fca931ffe2bbe3ec93361c8eece0ff3e8..b6d87a2e9818e7e912b9066e29a22eb0886d990a 100644 (file)
@@ -16,12 +16,30 @@ class StateFlags
 {
 public:
     unsigned int front_end_https = 0; ///< send "Front-End-Https: On" header (off/on/auto=2)
-    bool proxying = false;
     bool keepalive = false;
     bool only_if_cached = false;
     bool handling1xx = false;       ///< we are ignoring or forwarding 1xx response
     bool headers_parsed = false;
-    bool originpeer = false;
+
+    /// Whether the next TCP hop is a cache_peer, including originserver
+    bool peering = false;
+
+    /// Whether this request is being forwarded inside a CONNECT tunnel
+    /// through a [non-originserver] cache_peer; implies peering and toOrigin
+    bool tunneling = false;
+
+    /// Whether the next HTTP hop is an origin server, including an
+    /// originserver cache_peer. The three possible cases are:
+    /// -# a direct TCP/HTTP connection to an origin server,
+    /// -# a direct TCP/HTTP connection to an originserver cache_peer, and
+    /// -# a CONNECT tunnel through a [non-originserver] cache_peer [to an origin server]
+    /// Thus, toOrigin is false only when the HTTP request is sent over
+    ///    a direct TCP/HTTP connection to a non-originserver cache_peer.
+    bool toOrigin = false;
+
+    /// Whether the next TCP/HTTP hop is an originserver cache_peer.
+    bool toOriginPeer() const { return toOrigin && peering && !tunneling; }
+
     bool keepalive_broken = false;
     bool abuse_detected = false;
     bool request_sent = false;
index 2baf08b761bd68b4abd22f5b4e9cd294992c360c..18aa92dbd52349f8dded63667fd652adbf93c204 100644 (file)
@@ -22,10 +22,10 @@ CBDATA_NAMESPACED_CLASS_INIT(Security, BlindPeerConnector);
 Security::ContextPointer
 Security::BlindPeerConnector::getTlsContext()
 {
-    if (const CachePeer *peer = serverConnection()->getPeer()) {
-        assert(peer->secure.encryptTransport);
+    const CachePeer *peer = serverConnection()->getPeer();
+    if (peer && peer->secure.encryptTransport)
         return peer->sslContext;
-    }
+
     return ::Config.ssl_client.sslContext;
 }
 
@@ -37,7 +37,8 @@ Security::BlindPeerConnector::initialize(Security::SessionPointer &serverSession
         return false;
     }
 
-    if (const CachePeer *peer = serverConnection()->getPeer()) {
+    const CachePeer *peer = serverConnection()->getPeer();
+    if (peer && peer->secure.encryptTransport) {
         assert(peer);
 
         // NP: domain may be a raw-IP but it is now always set
@@ -64,6 +65,8 @@ Security::BlindPeerConnector::initialize(Security::SessionPointer &serverSession
 void
 Security::BlindPeerConnector::noteNegotiationDone(ErrorState *error)
 {
+    auto *peer = serverConnection()->getPeer();
+
     if (error) {
         debugs(83, 5, "error=" << (void*)error);
         // XXX: forward.cc calls peerConnectSucceeded() after an OK TCP connect but
@@ -71,12 +74,12 @@ Security::BlindPeerConnector::noteNegotiationDone(ErrorState *error)
         // It is not clear whether we should call peerConnectSucceeded/Failed()
         // based on TCP results, SSL results, or both. And the code is probably not
         // consistent in this aspect across tunnelling and forwarding modules.
-        if (CachePeer *p = serverConnection()->getPeer())
-            peerConnectFailed(p);
+        if (peer && peer->secure.encryptTransport)
+            peerConnectFailed(peer);
         return;
     }
 
-    if (auto *peer = serverConnection()->getPeer()) {
+    if (peer && peer->secure.encryptTransport) {
         const int fd = serverConnection()->fd;
         Security::MaybeGetSessionResumeData(fd_table[fd].ssl, peer->sslSession);
     }
index 050c8a747d780b881baaa07ba149adc599ec2ccc..f088371b2b014456f13cd575470c518ea027043d 100644 (file)
@@ -11,6 +11,7 @@
 #include "squid.h"
 #include "acl/FilledChecklist.h"
 #include "comm/Loops.h"
+#include "comm/Read.h"
 #include "Downloader.h"
 #include "errorpage.h"
 #include "fde.h"
@@ -141,20 +142,6 @@ Security::PeerConnector::initialize(Security::SessionPointer &serverSession)
     return true;
 }
 
-void
-Security::PeerConnector::setReadTimeout()
-{
-    int timeToRead;
-    if (negotiationTimeout) {
-        const int timeUsed = squid_curtime - startTime;
-        const int timeLeft = max(0, static_cast<int>(negotiationTimeout - timeUsed));
-        timeToRead = min(static_cast<int>(::Config.Timeout.read), timeLeft);
-    } else
-        timeToRead = ::Config.Timeout.read;
-    AsyncCall::Pointer nil;
-    commSetConnTimeout(serverConnection(), timeToRead, nil);
-}
-
 void
 Security::PeerConnector::recordNegotiationDetails()
 {
@@ -482,7 +469,12 @@ Security::PeerConnector::noteWantRead()
         }
     }
 #endif
-    setReadTimeout();
+
+    // read timeout to avoid getting stuck while reading from a silent server
+    AsyncCall::Pointer nil;
+    const auto timeout = Comm::MortalReadTimeout(startTime, negotiationTimeout);
+    commSetConnTimeout(serverConnection(), timeout, nil);
+
     Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, new Pointer(this), 0);
 }
 
index 581503b6cf79f72d4631007d166f5d096157521f..63b48cbece7229e9bdd758d4f28dc9796d09cd6b 100644 (file)
@@ -101,10 +101,6 @@ protected:
     /// handler to monitor the socket.
     bool prepareSocket();
 
-    /// Sets the read timeout to avoid getting stuck while reading from a
-    /// silent server
-    void setReadTimeout();
-
     /// \returns true on successful TLS session initialization
     virtual bool initialize(Security::SessionPointer &);
 
index 90a6062a350e5821361bee8a9d6351e09311452c..4f520d14866235d84792c093ce92ac48f2c8065e 100644 (file)
@@ -11,6 +11,7 @@
 #include "squid.h"
 #include "anyp/Uri.h"
 #include "client_side.h"
+#include "client_side_request.h"
 #include "FwdState.h"
 #include "http/Stream.h"
 #include "ssl/ServerBump.h"
 
 CBDATA_NAMESPACED_CLASS_INIT(Ssl, ServerBump);
 
-Ssl::ServerBump::ServerBump(HttpRequest *fakeRequest, StoreEntry *e, Ssl::BumpMode md):
-    request(fakeRequest),
+Ssl::ServerBump::ServerBump(ClientHttpRequest *http, StoreEntry *e, Ssl::BumpMode md):
     step(bumpStep1)
 {
+    assert(http->request);
+    request = http->request;
     debugs(33, 4, "will peek at " << request->url.authority(true));
     act.step1 = md;
     act.step2 = act.step3 = Ssl::bumpNone;
@@ -39,6 +41,9 @@ Ssl::ServerBump::ServerBump(HttpRequest *fakeRequest, StoreEntry *e, Ssl::BumpMo
     // We do not need to be a client because the error contents will be used
     // later, but an entry without any client will trim all its contents away.
     sc = storeClientListAdd(entry, this);
+#if USE_DELAY_POOLS
+    sc->setDelayId(DelayId::DelayClient(http));
+#endif
 }
 
 Ssl::ServerBump::~ServerBump()
index bb6b186f8bbec4b4b9f14c7096944dad7ef00e0d..5bb81b288d6ac691343a4e2f2c011ea3826bce51 100644 (file)
@@ -19,6 +19,7 @@
 
 class ConnStateData;
 class store_client;
+class ClientHttpRequest;
 
 namespace Ssl
 {
@@ -31,7 +32,7 @@ class ServerBump
     CBDATA_CLASS(ServerBump);
 
 public:
-    explicit ServerBump(HttpRequest *fakeRequest, StoreEntry *e = NULL, Ssl::BumpMode mode = Ssl::bumpServerFirst);
+    explicit ServerBump(ClientHttpRequest *http, StoreEntry *e = nullptr, Ssl::BumpMode mode = Ssl::bumpServerFirst);
     ~ServerBump();
     void attachServerSession(const Security::SessionPointer &); ///< Sets the server TLS session object
     const Security::CertErrors *sslErrors() const; ///< SSL [certificate validation] errors
index 75f2d8deec1792e6d932729fcb1b7d88c367807d..3986130b7cbeb2c1a7c76a831339febfb2bba1bd 100644 (file)
@@ -47,7 +47,7 @@ void ConnStateData::getSslContextStart() STUB
 void ConnStateData::getSslContextDone(Security::ContextPointer &) STUB
 void ConnStateData::sslCrtdHandleReplyWrapper(void *, const Helper::Reply &) STUB
 void ConnStateData::sslCrtdHandleReply(const Helper::Reply &) STUB
-void ConnStateData::switchToHttps(HttpRequest *, Ssl::BumpMode) STUB
+void ConnStateData::switchToHttps(ClientHttpRequest *, Ssl::BumpMode) STUB
 void ConnStateData::buildSslCertGenerationParams(Ssl::CertificateProperties &) STUB
 bool ConnStateData::serveDelayedError(Http::Stream *) STUB_RETVAL(false)
 #endif
index 5cdf18c6e2e5580e8bf5a946332c9af9b1224e59..e0813e4ca8af906b76b04c60fb6d36ca7bcf5b5b 100644 (file)
@@ -32,7 +32,7 @@ CBDATA_NAMESPACED_CLASS_INIT(Comm, ConnOpener);
 bool Comm::ConnOpener::doneAll() const STUB_RETVAL(false)
 void Comm::ConnOpener::start() STUB
 void Comm::ConnOpener::swanSong() STUB
-Comm::ConnOpener::ConnOpener(Comm::ConnectionPointer &, AsyncCall::Pointer &, time_t) : AsyncJob("STUB Comm::ConnOpener") STUB
+Comm::ConnOpener::ConnOpener(Comm::ConnectionPointer &, const AsyncCall::Pointer &, time_t) : AsyncJob("STUB Comm::ConnOpener") STUB
     Comm::ConnOpener::~ConnOpener() STUB
     void Comm::ConnOpener::setHost(const char *) STUB
     const char * Comm::ConnOpener::getHost() const STUB_RETVAL(NULL)
index a917ecbaf24cd162aadf878a9f36259698261dcf..63de368b369c657fba597eff998ae28485ff4078 100644 (file)
@@ -58,7 +58,6 @@ const char *PeerConnector::status() const STUB_RETVAL("")
 void PeerConnector::commCloseHandler(const CommCloseCbParams &) STUB
 void PeerConnector::connectionClosed(const char *) STUB
 bool PeerConnector::prepareSocket() STUB_RETVAL(false)
-void PeerConnector::setReadTimeout() STUB
 bool PeerConnector::initialize(Security::SessionPointer &) STUB_RETVAL(false)
 void PeerConnector::negotiate() STUB
 bool PeerConnector::sslFinalized() STUB_RETVAL(false)
index 826bc32b961fffd01ad17358a5a895d3c16f2249..c3bd96baa9c4f8012c84bf26645332f080c3c8b0 100644 (file)
@@ -15,6 +15,7 @@
 #include "cbdata.h"
 #include "client_side.h"
 #include "client_side_request.h"
+#include "clients/HttpTunneler.h"
 #include "comm.h"
 #include "comm/Connection.h"
 #include "comm/ConnOpener.h"
@@ -80,12 +81,6 @@ public:
     static void WriteClientDone(const Comm::ConnectionPointer &, char *buf, size_t len, Comm::Flag flag, int xerrno, void *data);
     static void WriteServerDone(const Comm::ConnectionPointer &, char *buf, size_t len, Comm::Flag flag, int xerrno, void *data);
 
-    /// Starts reading peer response to our CONNECT request.
-    void readConnectResponse();
-
-    /// Called when we may be done handling a CONNECT exchange with the peer.
-    void connectExchangeCheckpoint();
-
     bool noConnections() const;
     char *url;
     CbcPointer<ClientHttpRequest> http;
@@ -97,13 +92,6 @@ public:
         return (server.conn != NULL && server.conn->getPeer() ? server.conn->getPeer()->host : request->url.host());
     };
 
-    /// Whether we are writing a CONNECT request to a peer.
-    bool waitingForConnectRequest() const { return connectReqWriting; }
-    /// Whether we are reading a CONNECT response from a peer.
-    bool waitingForConnectResponse() const { return connectRespBuf; }
-    /// Whether we are waiting for the CONNECT request/response exchange with the peer.
-    bool waitingForConnectExchange() const { return waitingForConnectRequest() || waitingForConnectResponse(); }
-
     /// Whether the client sent a CONNECT request to us.
     bool clientExpectsConnectResponse() const {
         // If we are forcing a tunnel after receiving a client CONNECT, then we
@@ -119,16 +107,15 @@ public:
                  (request->flags.interceptTproxy || request->flags.intercepted));
     }
 
-    /// Sends "502 Bad Gateway" error response to the client,
-    /// if it is waiting for Squid CONNECT response, closing connections.
-    void informUserOfPeerError(const char *errMsg, size_t);
-
     /// starts connecting to the next hop, either for the first time or while
     /// recovering from the previous connect failure
     void startConnecting();
 
     void noteConnectFailure(const Comm::ConnectionPointer &conn);
 
+    /// called when negotiations with the peer have been successfully completed
+    void notePeerReadyToShovel();
+
     class Connection
     {
 
@@ -162,7 +149,7 @@ public:
         // XXX: make these an AsyncCall when event API can handle them
         TunnelStateData *readPending;
         EVH *readPendingFunc;
-    private:
+
 #if USE_DELAY_POOLS
 
         DelayId delayId;
@@ -173,11 +160,12 @@ public:
     Connection client, server;
     int *status_ptr;        ///< pointer for logging HTTP status
     LogTags *logTag_ptr;    ///< pointer for logging Squid processing code
-    MemBuf *connectRespBuf; ///< accumulates peer CONNECT response when we need it
-    bool connectReqWriting; ///< whether we are writing a CONNECT request to a peer
+
     SBuf preReadClientData;
     SBuf preReadServerData;
     time_t startTime; ///< object creation time, before any peer selection/connection attempts
+    /// Whether we are waiting for the CONNECT request/response exchange with the peer.
+    bool waitingForConnectExchange;
 
     void copyRead(Connection &from, IOCB *completion);
 
@@ -225,17 +213,17 @@ private:
     /// details of the "last tunneling attempt" failure (if it failed)
     ErrorState *savedError = nullptr;
 
+    /// resumes operations after the (possibly failed) HTTP CONNECT exchange
+    void tunnelEstablishmentDone(Http::TunnelerAnswer &answer);
+
 public:
     bool keepGoingAfterRead(size_t len, Comm::Flag errcode, int xerrno, Connection &from, Connection &to);
     void copy(size_t len, Connection &from, Connection &to, IOCB *);
-    void handleConnectResponse(const size_t chunkSize);
     void readServer(char *buf, size_t len, Comm::Flag errcode, int xerrno);
     void readClient(char *buf, size_t len, Comm::Flag errcode, int xerrno);
     void writeClientDone(char *buf, size_t len, Comm::Flag flag, int xerrno);
     void writeServerDone(char *buf, size_t len, Comm::Flag flag, int xerrno);
 
-    static void ReadConnectResponseDone(const Comm::ConnectionPointer &, char *buf, size_t len, Comm::Flag errcode, int xerrno, void *data);
-    void readConnectResponseDone(char *buf, size_t len, Comm::Flag errcode, int xerrno);
     void copyClientBytes();
     void copyServerBytes();
 };
@@ -249,8 +237,6 @@ static CLCB tunnelClientClosed;
 static CTCB tunnelTimeout;
 static EVH tunnelDelayedClientRead;
 static EVH tunnelDelayedServerRead;
-static void tunnelConnected(const Comm::ConnectionPointer &server, void *);
-static void tunnelRelayConnectRequest(const Comm::ConnectionPointer &server, void *);
 
 static void
 tunnelServerClosed(const CommCloseCbParams &params)
@@ -308,9 +294,8 @@ tunnelClientClosed(const CommCloseCbParams &params)
 }
 
 TunnelStateData::TunnelStateData(ClientHttpRequest *clientRequest) :
-    connectRespBuf(NULL),
-    connectReqWriting(false),
-    startTime(squid_curtime)
+    startTime(squid_curtime),
+    waitingForConnectExchange(false)
 {
     debugs(26, 3, "TunnelStateData constructed this=" << this);
     client.readPendingFunc = &tunnelDelayedClientRead;
@@ -340,7 +325,6 @@ TunnelStateData::~TunnelStateData()
     assert(noConnections());
     xfree(url);
     serverDestinations.clear();
-    delete connectRespBuf;
     delete savedError;
 }
 
@@ -426,131 +410,6 @@ TunnelStateData::readServer(char *, size_t len, Comm::Flag errcode, int xerrno)
         copy(len, server, client, WriteClientDone);
 }
 
-/// Called when we read [a part of] CONNECT response from the peer
-void
-TunnelStateData::readConnectResponseDone(char *, size_t len, Comm::Flag errcode, int xerrno)
-{
-    debugs(26, 3, server.conn << ", read " << len << " bytes, err=" << errcode);
-    assert(waitingForConnectResponse());
-
-    if (errcode == Comm::ERR_CLOSING)
-        return;
-
-    if (len > 0) {
-        connectRespBuf->appended(len);
-        server.bytesIn(len);
-        statCounter.server.all.kbytes_in += len;
-        statCounter.server.other.kbytes_in += len;
-        request->hier.notePeerRead();
-    }
-
-    if (keepGoingAfterRead(len, errcode, xerrno, server, client))
-        handleConnectResponse(len);
-}
-
-void
-TunnelStateData::informUserOfPeerError(const char *errMsg, const size_t sz)
-{
-    server.len = 0;
-
-    if (logTag_ptr)
-        logTag_ptr->update(LOG_TCP_TUNNEL);
-
-    if (!clientExpectsConnectResponse()) {
-        // closing the connection is the best we can do here
-        debugs(50, 3, server.conn << " closing on error: " << errMsg);
-        server.conn->close();
-        return;
-    }
-
-    // if we have no reply suitable to relay, use 502 Bad Gateway
-    if (!sz || sz > static_cast<size_t>(connectRespBuf->contentSize()))
-        return sendError(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw(), al),
-                         "peer error without reply");
-
-    // if we need to send back the server response. write its headers to the client
-    server.len = sz;
-    memcpy(server.buf, connectRespBuf->content(), server.len);
-    copy(server.len, server, client, TunnelStateData::WriteClientDone);
-    // then close the server FD to prevent any relayed keep-alive causing CVE-2015-5400
-    server.closeIfOpen();
-}
-
-/* Read from client side and queue it for writing to the server */
-void
-TunnelStateData::ReadConnectResponseDone(const Comm::ConnectionPointer &, char *buf, size_t len, Comm::Flag errcode, int xerrno, void *data)
-{
-    TunnelStateData *tunnelState = (TunnelStateData *)data;
-    assert (cbdataReferenceValid (tunnelState));
-
-    tunnelState->readConnectResponseDone(buf, len, errcode, xerrno);
-}
-
-/// Parses [possibly incomplete] CONNECT response and reacts to it.
-/// If the tunnel is being closed or more response data is needed, returns false.
-/// Otherwise, the caller should handle the remaining read data, if any.
-void
-TunnelStateData::handleConnectResponse(const size_t chunkSize)
-{
-    assert(waitingForConnectResponse());
-
-    // Ideally, client and server should use MemBuf or better, but current code
-    // never accumulates more than one read when shoveling data (XXX) so it does
-    // not need to deal with MemBuf complexity. To keep it simple, we use a
-    // dedicated MemBuf for accumulating CONNECT responses. TODO: When shoveling
-    // is optimized, reuse server.buf for CONNEC response accumulation instead.
-
-    /* mimic the basic parts of HttpStateData::processReplyHeader() */
-    HttpReply rep;
-    Http::StatusCode parseErr = Http::scNone;
-    const bool eof = !chunkSize;
-    connectRespBuf->terminate(); // Http::Message::parse requires terminated string
-    const bool parsed = rep.parse(connectRespBuf->content(), connectRespBuf->contentSize(), eof, &parseErr);
-    if (!parsed) {
-        if (parseErr > 0) { // unrecoverable parsing error
-            informUserOfPeerError("malformed CONNECT response from peer", 0);
-            return;
-        }
-
-        // need more data
-        assert(!eof);
-        assert(!parseErr);
-
-        if (!connectRespBuf->hasSpace()) {
-            informUserOfPeerError("huge CONNECT response from peer", 0);
-            return;
-        }
-
-        // keep reading
-        readConnectResponse();
-        return;
-    }
-
-    // CONNECT response was successfully parsed
-    request->hier.peer_reply_status = rep.sline.status();
-
-    // bail if we did not get an HTTP 200 (Connection Established) response
-    if (rep.sline.status() != Http::scOkay) {
-        // if we ever decide to reuse the peer connection, we must extract the error response first
-        *status_ptr = rep.sline.status(); // we are relaying peer response
-        informUserOfPeerError("unsupported CONNECT response status code", rep.hdr_sz);
-        return;
-    }
-
-    if (rep.hdr_sz < connectRespBuf->contentSize()) {
-        // preserve bytes that the server already sent after the CONNECT response
-        server.len = connectRespBuf->contentSize() - rep.hdr_sz;
-        memcpy(server.buf, connectRespBuf->content()+rep.hdr_sz, server.len);
-    } else {
-        // reset; delay pools were using this field to throttle CONNECT response
-        server.len = 0;
-    }
-
-    delete connectRespBuf;
-    connectRespBuf = NULL;
-    connectExchangeCheckpoint();
-}
-
 void
 TunnelStateData::Connection::error(int const xerrno)
 {
@@ -834,17 +693,6 @@ TunnelStateData::copyRead(Connection &from, IOCB *completion)
     comm_read(from.conn, from.buf, bw, call);
 }
 
-void
-TunnelStateData::readConnectResponse()
-{
-    assert(waitingForConnectResponse());
-
-    AsyncCall::Pointer call = commCbCall(5,4, "readConnectResponseDone",
-                                         CommIoCbPtrFun(ReadConnectResponseDone, this));
-    comm_read(server.conn, connectRespBuf->space(),
-              server.bytesWanted(1, connectRespBuf->spaceSize()), call);
-}
-
 void
 TunnelStateData::copyClientBytes()
 {
@@ -880,7 +728,7 @@ TunnelStateData::copyServerBytes()
 static void
 tunnelStartShoveling(TunnelStateData *tunnelState)
 {
-    assert(!tunnelState->waitingForConnectExchange());
+    assert(!tunnelState->waitingForConnectExchange);
     *tunnelState->status_ptr = Http::scOkay;
     if (tunnelState->logTag_ptr)
         tunnelState->logTag_ptr->update(LOG_TCP_TUNNEL);
@@ -930,60 +778,55 @@ tunnelConnectedWriteDone(const Comm::ConnectionPointer &conn, char *, size_t len
     tunnelStartShoveling(tunnelState);
 }
 
-/// Called when we are done writing CONNECT request to a peer.
-static void
-tunnelConnectReqWriteDone(const Comm::ConnectionPointer &conn, char *, size_t ioSize, Comm::Flag flag, int, void *data)
+void
+TunnelStateData::tunnelEstablishmentDone(Http::TunnelerAnswer &answer)
 {
-    TunnelStateData *tunnelState = (TunnelStateData *)data;
-    debugs(26, 3, conn << ", flag=" << flag);
-    tunnelState->server.writer = NULL;
-    assert(tunnelState->waitingForConnectRequest());
+    server.len = 0;
 
-    tunnelState->request->hier.notePeerWrite();
+    if (logTag_ptr)
+        logTag_ptr->update(LOG_TCP_TUNNEL);
 
-    if (flag != Comm::OK) {
-        *tunnelState->status_ptr = Http::scInternalServerError;
-        tunnelErrorComplete(conn->fd, data, 0);
+    if (answer.peerResponseStatus != Http::scNone)
+        *status_ptr = answer.peerResponseStatus;
+
+    waitingForConnectExchange = false;
+
+    if (answer.positive()) {
+        // copy any post-200 OK bytes to our buffer
+        preReadServerData = answer.leftovers;
+        notePeerReadyToShovel();
         return;
     }
 
-    statCounter.server.all.kbytes_out += ioSize;
-    statCounter.server.other.kbytes_out += ioSize;
+    // TODO: Reuse to-peer connections after a CONNECT error response.
 
-    tunnelState->connectReqWriting = false;
-    tunnelState->connectExchangeCheckpoint();
-}
+    // TODO: We can and, hence, should close now, but tunnelServerClosed()
+    // cannot yet tell whether ErrorState is still writing an error response.
+    // server.closeIfOpen();
 
-void
-TunnelStateData::connectExchangeCheckpoint()
-{
-    if (waitingForConnectResponse()) {
-        debugs(26, 5, "still reading CONNECT response on " << server.conn);
-    } else if (waitingForConnectRequest()) {
-        debugs(26, 5, "still writing CONNECT request on " << server.conn);
-    } else {
-        assert(!waitingForConnectExchange());
-        debugs(26, 3, "done with CONNECT exchange on " << server.conn);
-        tunnelConnected(server.conn, this);
+    if (!clientExpectsConnectResponse()) {
+        // closing the non-HTTP client connection is the best we can do
+        debugs(50, 3, server.conn << " closing on CONNECT-to-peer error");
+        server.closeIfOpen();
+        return;
     }
+
+    ErrorState *error = answer.squidError.get();
+    Must(error);
+    answer.squidError.clear(); // preserve error for errorSendComplete()
+    sendError(error, "tunneler returns error");
 }
 
-/*
- * handle the write completion from a proxy request to an upstream origin
- */
-static void
-tunnelConnected(const Comm::ConnectionPointer &server, void *data)
+void
+TunnelStateData::notePeerReadyToShovel()
 {
-    TunnelStateData *tunnelState = (TunnelStateData *)data;
-    debugs(26, 3, HERE << server << ", tunnelState=" << tunnelState);
-
-    if (!tunnelState->clientExpectsConnectResponse())
-        tunnelStartShoveling(tunnelState); // ssl-bumped connection, be quiet
+    if (!clientExpectsConnectResponse())
+        tunnelStartShoveling(this); // ssl-bumped connection, be quiet
     else {
-        *tunnelState->status_ptr = Http::scOkay;
+        *status_ptr = Http::scOkay;
         AsyncCall::Pointer call = commCbCall(5,5, "tunnelConnectedWriteDone",
-                                             CommIoCbPtrFun(tunnelConnectedWriteDone, tunnelState));
-        tunnelState->client.write(conn_established, strlen(conn_established), call, NULL);
+                                             CommIoCbPtrFun(tunnelConnectedWriteDone, this));
+        client.write(conn_established, strlen(conn_established), call, nullptr);
     }
 }
 
@@ -1061,23 +904,19 @@ tunnelConnectDone(const Comm::ConnectionPointer &conn, Comm::Flag status, int xe
     tunnelState->request->peer_host = conn->getPeer() ? conn->getPeer()->host : NULL;
     comm_add_close_handler(conn->fd, tunnelServerClosed, tunnelState);
 
-    debugs(26, 4, HERE << "determine post-connect handling pathway.");
-    if (conn->getPeer()) {
-        tunnelState->request->peer_login = conn->getPeer()->login;
-        tunnelState->request->peer_domain = conn->getPeer()->domain;
-        tunnelState->request->flags.auth_no_keytab = conn->getPeer()->options.auth_no_keytab;
-        tunnelState->request->flags.proxying = !(conn->getPeer()->options.originserver);
+    bool toOrigin = false; // same semantics as StateFlags::toOrigin
+    if (const auto * const peer = conn->getPeer()) {
+        tunnelState->request->prepForPeering(*peer);
+        toOrigin = peer->options.originserver;
     } else {
-        tunnelState->request->peer_login = NULL;
-        tunnelState->request->peer_domain = NULL;
-        tunnelState->request->flags.auth_no_keytab = false;
-        tunnelState->request->flags.proxying = false;
+        tunnelState->request->prepForDirect();
+        toOrigin = true;
     }
 
-    if (tunnelState->request->flags.proxying)
+    if (!toOrigin)
         tunnelState->connectToPeer();
     else {
-        tunnelConnected(conn, tunnelState);
+        tunnelState->notePeerReadyToShovel();
     }
 
     AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
@@ -1126,7 +965,7 @@ tunnelStart(ClientHttpRequest * http)
 
     tunnelState = new TunnelStateData(http);
 #if USE_DELAY_POOLS
-    //server.setDelayId called from tunnelConnectDone after server side connection established
+    tunnelState->server.setDelayId(DelayId::DelayClient(http));
 #endif
     tunnelState->startSelectingDestinations(request, http->al, nullptr);
 }
@@ -1158,52 +997,18 @@ TunnelStateData::connectedToPeer(Security::EncryptorAnswer &answer)
         return;
     }
 
-    tunnelRelayConnectRequest(server.conn, this);
-}
-
-static void
-tunnelRelayConnectRequest(const Comm::ConnectionPointer &srv, void *data)
-{
-    TunnelStateData *tunnelState = (TunnelStateData *)data;
-    assert(!tunnelState->waitingForConnectExchange());
-    HttpHeader hdr_out(hoRequest);
-    Http::StateFlags flags;
-    debugs(26, 3, HERE << srv << ", tunnelState=" << tunnelState);
-    flags.proxying = tunnelState->request->flags.proxying;
-    MemBuf mb;
-    mb.init();
-    mb.appendf("CONNECT %s HTTP/1.1\r\n", tunnelState->url);
-    HttpStateData::httpBuildRequestHeader(tunnelState->request.getRaw(),
-                                          NULL,         /* StoreEntry */
-                                          tunnelState->al,          /* AccessLogEntry */
-                                          &hdr_out,
-                                          flags);           /* flags */
-    hdr_out.packInto(&mb);
-    hdr_out.clean();
-    mb.append("\r\n", 2);
-
-    debugs(11, 2, "Tunnel Server REQUEST: " << tunnelState->server.conn <<
-           ":\n----------\n" << mb.buf << "\n----------");
-
-    AsyncCall::Pointer writeCall = commCbCall(5,5, "tunnelConnectReqWriteDone",
-                                   CommIoCbPtrFun(tunnelConnectReqWriteDone,
-                                           tunnelState));
-
-    tunnelState->server.write(mb.buf, mb.size, writeCall, mb.freeFunc());
-    tunnelState->connectReqWriting = true;
-
-    tunnelState->connectRespBuf = new MemBuf;
-    // SQUID_TCP_SO_RCVBUF: we should not accumulate more than regular I/O buffer
-    // can hold since any CONNECT response leftovers have to fit into server.buf.
-    // 2*SQUID_TCP_SO_RCVBUF: Http::Message::parse() zero-terminates, which uses space.
-    tunnelState->connectRespBuf->init(SQUID_TCP_SO_RCVBUF, 2*SQUID_TCP_SO_RCVBUF);
-    tunnelState->readConnectResponse();
-
-    assert(tunnelState->waitingForConnectExchange());
+    assert(!waitingForConnectExchange);
 
-    AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
-                                     CommTimeoutCbPtrFun(tunnelTimeout, tunnelState));
-    commSetConnTimeout(srv, Config.Timeout.read, timeoutCall);
+    AsyncCall::Pointer callback = asyncCall(5,4,
+                                            "TunnelStateData::tunnelEstablishmentDone",
+                                            Http::Tunneler::CbDialer<TunnelStateData>(&TunnelStateData::tunnelEstablishmentDone, this));
+    const auto tunneler = new Http::Tunneler(server.conn, request, callback, Config.Timeout.lifetime, al);
+#if USE_DELAY_POOLS
+    tunneler->setDelayId(server.delayId);
+#endif
+    AsyncJob::Start(tunneler);
+    waitingForConnectExchange = true;
+    // and wait for the tunnelEstablishmentDone() call
 }
 
 static Comm::ConnectionPointer
@@ -1378,7 +1183,7 @@ switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::
 
 #if USE_DELAY_POOLS
     /* no point using the delayIsNoDelay stuff since tunnel is nice and simple */
-    if (srvConn->getPeer() && srvConn->getPeer()->options.no_delay)
+    if (!srvConn->getPeer() || !srvConn->getPeer()->options.no_delay)
         tunnelState->server.setDelayId(DelayId::DelayClient(context->http));
 #endif
 
@@ -1386,17 +1191,10 @@ switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::
     comm_add_close_handler(srvConn->fd, tunnelServerClosed, tunnelState);
 
     debugs(26, 4, "determine post-connect handling pathway.");
-    if (srvConn->getPeer()) {
-        request->peer_login = srvConn->getPeer()->login;
-        request->peer_domain = srvConn->getPeer()->domain;
-        request->flags.auth_no_keytab = srvConn->getPeer()->options.auth_no_keytab;
-        request->flags.proxying = !(srvConn->getPeer()->options.originserver);
-    } else {
-        request->peer_login = nullptr;
-        request->peer_domain = nullptr;
-        request->flags.auth_no_keytab = false;
-        request->flags.proxying = false;
-    }
+    if (const auto peer = srvConn->getPeer())
+        request->prepForPeering(*peer);
+    else
+        request->prepForDirect();
 
     AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
                                      CommTimeoutCbPtrFun(tunnelTimeout, tunnelState));