From: Christos Tsantilas Date: Mon, 25 Mar 2019 09:37:42 +0000 (+0000) Subject: Peering support for SslBump (#380) X-Git-Tag: SQUID_5_0_1~112 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f5e179474d15c0b43e3454f4763f36fba611c99c;p=thirdparty%2Fsquid.git Peering support for SslBump (#380) 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. --- diff --git a/src/Debug.h b/src/Debug.h index 7fb1ed5494..756f498860 100644 --- a/src/Debug.h +++ b/src/Debug.h @@ -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 diff --git a/src/FwdState.cc b/src/FwdState.cc index 6c7a0147c4..9c4a5cb214 100644 --- a/src/FwdState.cc +++ b/src/FwdState.cc @@ -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::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 ********************************************/ /* diff --git a/src/FwdState.h b/src/FwdState.h index 57707026b7..8722fd41e0 100644 --- a/src/FwdState.h +++ b/src/FwdState.h @@ -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; diff --git a/src/HttpRequest.cc b/src/HttpRequest.cc index 80f7776873..5eaabe824a 100644 --- a/src/HttpRequest.cc +++ b/src/HttpRequest.cc @@ -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) { diff --git a/src/HttpRequest.h b/src/HttpRequest.h index 909a17ef64..13e9d4eee1 100644 --- a/src/HttpRequest.h +++ b/src/HttpRequest.h @@ -34,10 +34,11 @@ #include "eui/Eui64.h" #endif -class ConnStateData; -class Downloader; class AccessLogEntry; typedef RefCount 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 diff --git a/src/RequestFlags.h b/src/RequestFlags.h index 6b984e3635..b4a880c82c 100644 --- a/src/RequestFlags.h +++ b/src/RequestFlags.h @@ -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 */ diff --git a/src/client_side.cc b/src/client_side.cc index 5610bc4c08..4aaf8f6d66 100644 --- a/src/client_side.cc +++ b/src/client_side.cc @@ -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(); diff --git a/src/client_side.h b/src/client_side.h index ba955dc8ae..318730dd36 100644 --- a/src/client_side.h +++ b/src/client_side.h @@ -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;} diff --git a/src/client_side_request.cc b/src/client_side_request.cc index 0cc0a04125..e6bf50203a 100644 --- a/src/client_side_request.cc +++ b/src/client_side_request.cc @@ -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 index 0000000000..fb0c8d336f --- /dev/null +++ b/src/clients/HttpTunneler.cc @@ -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(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(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 ¶ms) +{ + 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 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 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 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 index 0000000000..4af96c5b70 --- /dev/null +++ b/src/clients/HttpTunneler.h @@ -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 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 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(*this) << ')'; + } + private: + CbcPointer 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 index 0000000000..66e605c6eb --- /dev/null +++ b/src/clients/HttpTunnelerAnswer.cc @@ -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 index 0000000000..b549dbbfa0 --- /dev/null +++ b/src/clients/HttpTunnelerAnswer.h @@ -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 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 */ + diff --git a/src/clients/Makefile.am b/src/clients/Makefile.am index 2b911de434..7e4203dac3 100644 --- a/src/clients/Makefile.am +++ b/src/clients/Makefile.am @@ -17,4 +17,8 @@ libclients_la_SOURCES = \ FtpClient.h \ FtpGateway.cc \ FtpRelay.cc \ + HttpTunneler.cc \ + HttpTunneler.h \ + HttpTunnelerAnswer.cc \ + HttpTunnelerAnswer.h \ forward.h diff --git a/src/clients/forward.h b/src/clients/forward.h index 0c0d916cab..81338c6591 100644 --- a/src/clients/forward.h +++ b/src/clients/forward.h @@ -18,6 +18,12 @@ class AsyncJob; template class CbcPointer; typedef CbcPointer AsyncJobPointer; +namespace Http +{ +class Tunneler; +class TunnelerAnswer; +} + namespace Ftp { diff --git a/src/comm/ConnOpener.cc b/src/comm/ConnOpener.cc index 9d41cbe3ea..d494a5a49d 100644 --- a/src/comm/ConnOpener.cc +++ b/src/comm/ConnOpener.cc @@ -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), diff --git a/src/comm/ConnOpener.h b/src/comm/ConnOpener.h index 350582f60c..ee459f6335 100644 --- a/src/comm/ConnOpener.h +++ b/src/comm/ConnOpener.h @@ -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 diff --git a/src/comm/Read.cc b/src/comm/Read.cc index c8f290885e..58f43158c1 100644 --- a/src/comm/Read.cc +++ b/src/comm/Read.cc @@ -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; +} + diff --git a/src/comm/Read.h b/src/comm/Read.h index ab65864752..7699f0db65 100644 --- a/src/comm/Read.h +++ b/src/comm/Read.h @@ -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 diff --git a/src/debug.cc b/src/debug.cc index 58205af1e1..a93cfd4989 100644 --- a/src/debug.cc +++ b/src/debug.cc @@ -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); diff --git a/src/err_type.h b/src/err_type.h index 97e774008d..c6aa94426b 100644 --- a/src/err_type.h +++ b/src/err_type.h @@ -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, diff --git a/src/errorpage.cc b/src/errorpage.cc index 7e867e877f..1d696609d5 100644 --- a/src/errorpage.cc +++ b/src/errorpage.cc @@ -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 */ diff --git a/src/errorpage.h b/src/errorpage.h index 002db460d6..9b7e6f2a1b 100644 --- a/src/errorpage.h +++ b/src/errorpage.h @@ -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); diff --git a/src/http.cc b/src/http.cc index 0a96bd950e..5f462ad80f 100644 --- a/src/http.cc +++ b/src/http.cc @@ -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: diff --git a/src/http/StateFlags.h b/src/http/StateFlags.h index cb7cc90fca..b6d87a2e98 100644 --- a/src/http/StateFlags.h +++ b/src/http/StateFlags.h @@ -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; diff --git a/src/security/BlindPeerConnector.cc b/src/security/BlindPeerConnector.cc index 2baf08b761..18aa92dbd5 100644 --- a/src/security/BlindPeerConnector.cc +++ b/src/security/BlindPeerConnector.cc @@ -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); } diff --git a/src/security/PeerConnector.cc b/src/security/PeerConnector.cc index 050c8a747d..f088371b2b 100644 --- a/src/security/PeerConnector.cc +++ b/src/security/PeerConnector.cc @@ -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(negotiationTimeout - timeUsed)); - timeToRead = min(static_cast(::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); } diff --git a/src/security/PeerConnector.h b/src/security/PeerConnector.h index 581503b6cf..63b48cbece 100644 --- a/src/security/PeerConnector.h +++ b/src/security/PeerConnector.h @@ -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 &); diff --git a/src/ssl/ServerBump.cc b/src/ssl/ServerBump.cc index 90a6062a35..4f520d1486 100644 --- a/src/ssl/ServerBump.cc +++ b/src/ssl/ServerBump.cc @@ -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" @@ -19,10 +20,11 @@ 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() diff --git a/src/ssl/ServerBump.h b/src/ssl/ServerBump.h index bb6b186f8b..5bb81b288d 100644 --- a/src/ssl/ServerBump.h +++ b/src/ssl/ServerBump.h @@ -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 diff --git a/src/tests/stub_client_side.cc b/src/tests/stub_client_side.cc index 75f2d8deec..3986130b7c 100644 --- a/src/tests/stub_client_side.cc +++ b/src/tests/stub_client_side.cc @@ -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 diff --git a/src/tests/stub_libcomm.cc b/src/tests/stub_libcomm.cc index 5cdf18c6e2..e0813e4ca8 100644 --- a/src/tests/stub_libcomm.cc +++ b/src/tests/stub_libcomm.cc @@ -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) diff --git a/src/tests/stub_libsecurity.cc b/src/tests/stub_libsecurity.cc index a917ecbaf2..63de368b36 100644 --- a/src/tests/stub_libsecurity.cc +++ b/src/tests/stub_libsecurity.cc @@ -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) diff --git a/src/tunnel.cc b/src/tunnel.cc index 826bc32b96..c3bd96baa9 100644 --- a/src/tunnel.cc +++ b/src/tunnel.cc @@ -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 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 ¶ms) @@ -308,9 +294,8 @@ tunnelClientClosed(const CommCloseCbParams ¶ms) } 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(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::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));