fwd->stopAndDestroy("store entry aborted");
}
+void
+FwdState::closePendingConnection(const Comm::ConnectionPointer &conn, const char *reason)
+{
+ debugs(17, 3, "because " << reason << "; " << conn);
+ assert(!serverConn);
+ assert(!closeHandler);
+ if (IsConnOpen(conn)) {
+ fwdPconnPool->noteUses(fd_table[conn->fd].pconn.uses);
+ conn->close();
+ }
+}
+
void
FwdState::closeServerConnection(const char *reason)
{
retryOrBail();
}
+/// starts a preparation step for an established connection; retries on failures
+template <typename StepStart>
+void
+FwdState::advanceDestination(const char *stepDescription, const Comm::ConnectionPointer &conn, const StepStart &startStep)
+{
+ // TODO: Extract destination-specific handling from FwdState so that all the
+ // awkward, limited-scope advanceDestination() calls can be replaced with a
+ // single simple try/catch,retry block.
+ try {
+ startStep();
+ // now wait for the step callback
+ } catch (...) {
+ debugs (17, 2, "exception while trying to " << stepDescription << ": " << CurrentException);
+ closePendingConnection(conn, "connection preparation exception");
+ retryOrBail();
+ }
+}
+
/// called when a to-peer connection has been successfully obtained or
/// when all candidate destinations have been tried and all have failed
void
Must(n_tries <= answer.n_tries); // n_tries cannot decrease
n_tries = answer.n_tries;
- if (const auto error = answer.error.get()) {
+ ErrorState *error = nullptr;
+ if ((error = answer.error.get())) {
flags.dont_retry = true; // or HappyConnOpener would not have given up
syncHierNote(answer.conn, request->url.host());
- fail(error);
+ Must(!Comm::IsConnOpen(answer.conn));
answer.error.clear(); // preserve error for errorSendComplete()
+ } else if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ syncHierNote(answer.conn, request->url.host());
+ closePendingConnection(answer.conn, "conn was closed while waiting for noteConnection");
+ error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request, al);
+ }
+
+ if (error) {
+ fail(error);
retryOrBail(); // will notice flags.dont_retry and bail
return;
}
- syncWithServerConn(answer.conn, request->url.host(), answer.reused);
-
- if (answer.reused)
+ if (answer.reused) {
+ syncWithServerConn(answer.conn, request->url.host(), answer.reused);
return dispatch();
+ }
// Check if we need to TLS before use
- if (const CachePeer *peer = serverConnection()->getPeer()) {
+ if (const auto *peer = answer.conn->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.
if (originWantsEncryptedTraffic && // the "encrypted traffic" part
!peer->options.originserver && // the "through a proxy" part
!peer->secure.encryptTransport) // the "exclude HTTPS proxies" part
- return establishTunnelThruProxy();
+ return advanceDestination("establish tunnel through proxy", answer.conn, [this,&answer] {
+ establishTunnelThruProxy(answer.conn);
+ });
}
- secureConnectionToPeerIfNeeded();
+ secureConnectionToPeerIfNeeded(answer.conn);
}
void
-FwdState::establishTunnelThruProxy()
+FwdState::establishTunnelThruProxy(const Comm::ConnectionPointer &conn)
{
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);
+ const auto tunneler = new Http::Tunneler(conn, requestPointer, callback, connectingTimeout(serverConnection()), al);
+
+ // TODO: Replace this hack with proper Comm::Connection-Pool association
+ // that is not tied to fwdPconnPool and can handle disappearing pools.
+ tunneler->noteFwdPconnUse = true;
+
#if USE_DELAY_POOLS
Must(serverConnection()->getPeer());
if (!serverConnection()->getPeer()->options.no_delay)
void
FwdState::tunnelEstablishmentDone(Http::TunnelerAnswer &answer)
{
- if (answer.positive()) {
- if (answer.leftovers.isEmpty()) {
- secureConnectionToPeerIfNeeded();
- return;
- }
+ ErrorState *error = nullptr;
+ if (!answer.positive()) {
+ Must(!Comm::IsConnOpen(answer.conn));
+ error = answer.squidError.get();
+ Must(error);
+ answer.squidError.clear(); // preserve error for fail()
+ } else if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ // The socket could get closed while our callback was queued.
+ // We close Connection here to sync Connection::fd.
+ closePendingConnection(answer.conn, "conn was closed while waiting for tunnelEstablishmentDone");
+ error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request, al);
+ } else if (!answer.leftovers.isEmpty()) {
// 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().
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");
+ "Closing " << answer.conn);
+ error = new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request, al);
+ closePendingConnection(answer.conn, "server spoke before tunnelEstablishmentDone");
+ }
+ if (error) {
+ fail(error);
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();
+ secureConnectionToPeerIfNeeded(answer.conn);
}
/// handles an established TCP connection to peer (including origin servers)
void
-FwdState::secureConnectionToPeerIfNeeded()
+FwdState::secureConnectionToPeerIfNeeded(const Comm::ConnectionPointer &conn)
{
assert(!request->flags.pinned);
- const CachePeer *p = serverConnection()->getPeer();
+ const auto p = conn->getPeer();
const bool peerWantsTls = p && p->secure.encryptTransport;
// userWillTlsToPeerForUs assumes CONNECT == HTTPS
const bool userWillTlsToPeerForUs = p && p->options.originserver &&
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));
- const auto sslNegotiationTimeout = connectingTimeout(serverConnection());
- Security::PeerConnector *connector = nullptr;
-#if USE_OPENSSL
- if (request->flags.sslPeek)
- connector = new Ssl::PeekingPeerConnector(requestPointer, serverConnection(), clientConn, callback, al, sslNegotiationTimeout);
- else
-#endif
- connector = new Security::BlindPeerConnector(requestPointer, serverConnection(), callback, al, sslNegotiationTimeout);
- AsyncJob::Start(connector); // will call our callback
- return;
+ return advanceDestination("secure connection to peer", conn, [this,&conn] {
+ secureConnectionToPeer(conn);
+ });
}
// if not encrypting just run the post-connect actions
- successfullyConnectedToPeer();
+ successfullyConnectedToPeer(conn);
+}
+
+/// encrypts an established TCP connection to peer (including origin servers)
+void
+FwdState::secureConnectionToPeer(const Comm::ConnectionPointer &conn)
+{
+ HttpRequest::Pointer requestPointer = request;
+ AsyncCall::Pointer callback = asyncCall(17,4,
+ "FwdState::ConnectedToPeer",
+ FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this));
+ const auto sslNegotiationTimeout = connectingTimeout(conn);
+ Security::PeerConnector *connector = nullptr;
+#if USE_OPENSSL
+ if (request->flags.sslPeek)
+ connector = new Ssl::PeekingPeerConnector(requestPointer, conn, clientConn, callback, al, sslNegotiationTimeout);
+ else
+#endif
+ connector = new Security::BlindPeerConnector(requestPointer, conn, callback, al, sslNegotiationTimeout);
+ connector->noteFwdPconnUse = true;
+ AsyncJob::Start(connector); // will call our callback
}
/// called when all negotiations with the TLS-speaking peer have been completed
void
FwdState::connectedToPeer(Security::EncryptorAnswer &answer)
{
- if (ErrorState *error = answer.error.get()) {
- fail(error);
+ ErrorState *error = nullptr;
+ if ((error = answer.error.get())) {
+ Must(!Comm::IsConnOpen(answer.conn));
answer.error.clear(); // preserve error for errorSendComplete()
- if (CachePeer *p = serverConnection()->getPeer())
- peerConnectFailed(p);
- serverConnection()->close();
- return;
- }
-
- if (answer.tunneled) {
+ } else if (answer.tunneled) {
// TODO: When ConnStateData establishes tunnels, its state changes
// [in ways that may affect logging?]. Consider informing
// ConnStateData about our tunnel or otherwise unifying tunnel
// establishment [side effects].
- unregister(serverConn); // async call owns it now
complete(); // destroys us
return;
+ } else if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ closePendingConnection(answer.conn, "conn was closed while waiting for connectedToPeer");
+ error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request, al);
}
- successfullyConnectedToPeer();
+ if (error) {
+ fail(error);
+ retryOrBail();
+ return;
+ }
+
+ successfullyConnectedToPeer(answer.conn);
}
/// called when all negotiations with the peer have been completed
void
-FwdState::successfullyConnectedToPeer()
+FwdState::successfullyConnectedToPeer(const Comm::ConnectionPointer &conn)
{
+ syncWithServerConn(conn, request->url.host(), false);
+
// should reach ConnStateData before the dispatched Client job starts
CallJobHere1(17, 4, request->clientConnectionManager, ConnStateData,
ConnStateData::notePeerConnection, serverConnection());
// Set the dont_retry flag because this is not a transient (network) error.
flags.dont_retry = true;
if (Comm::IsConnOpen(serverConn)) {
- serverConn->close();
+ serverConn->close(); // trigger cleanup
}
break;
}
void dontRetry(bool val) { flags.dont_retry = val; }
+ /// get rid of a to-server connection that failed to become serverConn
+ void closePendingConnection(const Comm::ConnectionPointer &conn, const char *reason);
+
/** return a ConnectionPointer to the current server connection (may or may not be open) */
Comm::ConnectionPointer const & serverConnection() const { return serverConn; };
/// (in order to retry or reforward a failed request)
bool pinnedCanRetry() const;
+ template <typename StepStart>
+ void advanceDestination(const char *stepDescription, const Comm::ConnectionPointer &conn, const StepStart &startStep);
+
ErrorState *makeConnectingError(const err_type type) const;
void connectedToPeer(Security::EncryptorAnswer &answer);
static void RegisterWithCacheManager(void);
- void establishTunnelThruProxy();
+ void establishTunnelThruProxy(const Comm::ConnectionPointer &);
void tunnelEstablishmentDone(Http::TunnelerAnswer &answer);
- void secureConnectionToPeerIfNeeded();
- void successfullyConnectedToPeer();
+ void secureConnectionToPeerIfNeeded(const Comm::ConnectionPointer &);
+ void secureConnectionToPeer(const Comm::ConnectionPointer &);
+ void successfullyConnectedToPeer(const Comm::ConnectionPointer &);
/// stops monitoring server connection for closure and updates pconn stats
void closeServerConnection(const char *reason);
#include "http/one/ResponseParser.h"
#include "http/StateFlags.h"
#include "HttpRequest.h"
+#include "neighbors.h"
+#include "pconn.h"
#include "SquidConfig.h"
#include "StatCounters.h"
Http::Tunneler::Tunneler(const Comm::ConnectionPointer &conn, const HttpRequest::Pointer &req, AsyncCall::Pointer &aCallback, time_t timeout, const AccessLogEntryPointer &alp):
AsyncJob("Http::Tunneler"),
+ noteFwdPconnUse(false),
connection(conn),
request(req),
callback(aCallback),
assert(callback);
assert(dynamic_cast<Http::TunnelerAnswer *>(callback->getDialer()));
url = request->url.authority();
+ watchForClosures();
}
Http::Tunneler::~Tunneler()
Must(url.length());
Must(lifetimeLimit >= 0);
+ // we own this Comm::Connection object and its fd exclusively, but must bail
+ // if others started closing the socket while we were waiting to start()
+ assert(Comm::IsConnOpen(connection));
+ if (fd_table[connection->fd].closing()) {
+ bailWith(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw(), al));
+ return;
+ }
+
const auto peer = connection->getPeer();
- Must(peer); // bail if our peer was reconfigured away
+ // bail if our peer was reconfigured away
+ if (!peer) {
+ bailWith(new ErrorState(ERR_CONNECT_FAIL, Http::scInternalServerError, request.getRaw(), al));
+ return;
+ }
request->prepForPeering(*peer);
- watchForClosures();
writeRequest();
startReadingResponse();
}
void
Http::Tunneler::handleConnectionClosure(const CommCloseCbParams ¶ms)
{
- mustStop("server connection gone");
- callback = nullptr; // the caller must monitor closures
+ closer = nullptr;
+ bailWith(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw(), al));
}
/// make sure we quit if/when the connection is gone
comm_add_close_handler(connection->fd, closer);
}
+/// The connection read timeout callback handler.
void
-Http::Tunneler::handleException(const std::exception& e)
+Http::Tunneler::handleTimeout(const CommTimeoutCbParams &)
{
- debugs(83, 2, e.what() << status());
- connection->close();
- bailWith(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
+ bailWith(new ErrorState(ERR_CONNECT_FAIL, Http::scGatewayTimeout, request.getRaw(), al));
}
void
Comm::Read(connection, reader);
AsyncCall::Pointer nil;
+ typedef CommCbMemFunT<Http::Tunneler, CommTimeoutCbParams> TimeoutDialer;
+ AsyncCall::Pointer timeoutCall = JobCallback(93, 5,
+ TimeoutDialer, this, Http::Tunneler::handleTimeout);
const auto timeout = Comm::MortalReadTimeout(startTime, lifetimeLimit);
- commSetConnTimeout(connection, timeout, nil);
+ commSetConnTimeout(connection, timeout, timeoutCall);
}
/// Parses [possibly incomplete] CONNECT response and reacts to it.
{
Must(error);
answer().squidError = error;
+
+ if (const auto p = connection->getPeer())
+ peerConnectFailed(p);
+
callBack();
+ disconnect();
+
+ if (noteFwdPconnUse)
+ fwdPconnPool->noteUses(fd_table[connection->fd].pconn.uses);
+ // TODO: Reuse to-peer connections after a CONNECT error response.
+ connection->close();
+ connection = nullptr;
+}
+
+void
+Http::Tunneler::sendSuccess()
+{
+ assert(answer().positive());
+ callBack();
+ disconnect();
+}
+
+void
+Http::Tunneler::disconnect()
+{
+ if (closer) {
+ comm_remove_close_handler(connection->fd, closer);
+ closer = nullptr;
+ }
+
+ if (reader) {
+ Comm::ReadCancel(connection->fd, reader);
+ reader = nullptr;
+ }
+
+ // remove connection timeout handler
+ commUnsetConnTimeout(connection);
}
void
Http::Tunneler::callBack()
{
debugs(83, 5, connection << status());
+ if (answer().positive())
+ answer().conn = connection;
auto cb = callback;
callback = nullptr;
ScheduleCallHere(cb);
if (callback) {
if (requestWritten && tunnelEstablished) {
- assert(answer().positive());
- callBack(); // success
+ sendSuccess();
} 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());
}
assert(!callback);
}
-
- if (closer) {
- comm_remove_close_handler(connection->fd, closer);
- closer = nullptr;
- }
-
- if (reader) {
- Comm::ReadCancel(connection->fd, reader);
- reader = nullptr;
- }
}
const char *
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.
+/// Negotiates an HTTP CONNECT tunnel through a forward proxy using a given
+/// (open and, if needed, encrypted) TCP connection to that proxy. Owns the
+/// connection during these negotiations. The caller receives TunnelerAnswer.
class Tunneler: virtual public AsyncJob
{
CBDATA_CLASS(Tunneler);
void setDelayId(DelayId delay_id) {delayId = delay_id;}
#endif
+ /// hack: whether the connection requires fwdPconnPool->noteUses()
+ bool noteFwdPconnUse;
+
protected:
/* AsyncJob API */
virtual ~Tunneler();
void handleConnectionClosure(const CommCloseCbParams&);
void watchForClosures();
- void handleException(const std::exception&);
+ void handleTimeout(const CommTimeoutCbParams &);
void startReadingResponse();
void writeRequest();
void handleWrittenRequest(const CommIoCbParams&);
void readMore();
void handleResponse(const bool eof);
void bailOnResponseError(const char *error, HttpReply *);
+
+ /// sends the given error to the initiator
void bailWith(ErrorState*);
+
+ /// sends the ready-to-use tunnel to the initiator
+ void sendSuccess();
+
+ /// a bailWith(), sendSuccess() helper: sends results to the initiator
void callBack();
+ /// a bailWith(), sendSuccess() helper: stops monitoring the connection
+ void disconnect();
+
TunnelerAnswer &answer();
private:
if (answer.peerResponseStatus != Http::scNone)
os << ' ' << answer.peerResponseStatus;
+ if (answer.conn)
+ os << ' ' << answer.conn;
+
os << ']';
return os;
}
/// the status code of the successfully parsed CONNECT response (or scNone)
StatusCode peerResponseStatus = scNone;
+
+ Comm::ConnectionPointer conn;
};
std::ostream &operator <<(std::ostream &, const Http::TunnelerAnswer &);
*/
#include "squid.h"
+#include "AccessLogEntry.h"
#include "CachePeer.h"
#include "comm/Connection.h"
#include "errorpage.h"
#include "Downloader.h"
#include "errorpage.h"
#include "fde.h"
+#include "FwdState.h"
#include "http/Stream.h"
#include "HttpRequest.h"
+#include "neighbors.h"
+#include "pconn.h"
#include "security/NegotiationHistory.h"
#include "security/PeerConnector.h"
#include "SquidConfig.h"
Security::PeerConnector::PeerConnector(const Comm::ConnectionPointer &aServerConn, AsyncCall::Pointer &aCallback, const AccessLogEntryPointer &alp, const time_t timeout) :
AsyncJob("Security::PeerConnector"),
+ noteFwdPconnUse(false),
serverConn(aServerConn),
al(alp),
callback(aCallback),
useCertValidator_(true),
certsDownloads(0)
{
- debugs(83, 5, "Security::PeerConnector constructed, this=" << (void*)this);
+ debugs(83, 5, serverConn);
+
// if this throws, the caller's cb dialer is not our CbDialer
Must(dynamic_cast<CbDialer*>(callback->getDialer()));
-}
-Security::PeerConnector::~PeerConnector()
-{
- debugs(83, 5, "Security::PeerConnector destructed, this=" << (void*)this);
+ // watch for external connection closures
+ Must(Comm::IsConnOpen(serverConn));
+ Must(!fd_table[serverConn->fd].closing());
+ typedef CommCbMemFunT<Security::PeerConnector, CommCloseCbParams> Dialer;
+ closeHandler = JobCallback(9, 5, Dialer, this, Security::PeerConnector::commCloseHandler);
+ comm_add_close_handler(serverConn->fd, closeHandler);
}
bool Security::PeerConnector::doneAll() const
AsyncJob::start();
debugs(83, 5, "this=" << (void*)this);
+ // we own this Comm::Connection object and its fd exclusively, but must bail
+ // if others started closing the socket while we were waiting to start()
+ assert(Comm::IsConnOpen(serverConn));
+ if (fd_table[serverConn->fd].closing()) {
+ bail(new ErrorState(ERR_CONNECT_FAIL, Http::scBadGateway, request.getRaw(), al));
+ return;
+ }
+
Security::SessionPointer tmp;
- if (prepareSocket() && initialize(tmp))
+ if (initialize(tmp))
negotiate();
else
mustStop("Security::PeerConnector TLS socket initialize failed");
void
Security::PeerConnector::commCloseHandler(const CommCloseCbParams ¶ms)
{
+ closeHandler = nullptr;
+
debugs(83, 5, "FD " << params.fd << ", Security::PeerConnector=" << params.data);
- connectionClosed("Security::PeerConnector::commCloseHandler");
+ const auto err = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scServiceUnavailable, request.getRaw(), al);
+#if USE_OPENSSL
+ err->detail = new Ssl::ErrorDetail(SQUID_ERR_SSL_HANDSHAKE, nullptr, nullptr);
+#endif
+ bail(err);
}
void
-Security::PeerConnector::connectionClosed(const char *reason)
+Security::PeerConnector::commTimeoutHandler(const CommTimeoutCbParams &)
{
- debugs(83, 5, reason << " socket closed/closing. this=" << (void*)this);
- mustStop(reason);
- callback = NULL;
-}
-
-bool
-Security::PeerConnector::prepareSocket()
-{
- debugs(83, 5, serverConnection() << ", this=" << (void*)this);
- if (!Comm::IsConnOpen(serverConnection()) || fd_table[serverConnection()->fd].closing()) {
- connectionClosed("Security::PeerConnector::prepareSocket");
- return false;
- }
-
- debugs(83, 5, serverConnection());
-
- // watch for external connection closures
- typedef CommCbMemFunT<Security::PeerConnector, CommCloseCbParams> Dialer;
- closeHandler = JobCallback(9, 5, Dialer, this, Security::PeerConnector::commCloseHandler);
- comm_add_close_handler(serverConnection()->fd, closeHandler);
- return true;
+ debugs(83, 5, serverConnection() << " timedout. this=" << (void*)this);
+ const auto err = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scGatewayTimeout, request.getRaw(), al);
+#if USE_OPENSSL
+ err->detail = new Ssl::ErrorDetail(SQUID_ERR_SSL_HANDSHAKE, nullptr, nullptr);
+#endif
+ bail(err);
}
bool
void
Security::PeerConnector::negotiate()
{
- if (!Comm::IsConnOpen(serverConnection()))
- return;
-
const int fd = serverConnection()->fd;
if (fd_table[fd].closing())
return;
if (!sslFinalized())
return;
- callBack();
+ sendSuccess();
}
bool
noteNegotiationDone(anErr);
bail(anErr);
- serverConn->close();
return true;
}
}
Ssl::ErrorDetail *errDetails = NULL;
bool validatorFailed = false;
- if (!Comm::IsConnOpen(serverConnection())) {
- return;
- }
if (Debug::Enabled(83, 5)) {
Security::SessionPointer ssl(fd_table[serverConnection()->fd].ssl);
if (!errDetails && !validatorFailed) {
noteNegotiationDone(NULL);
- callBack();
+ sendSuccess();
return;
}
noteNegotiationDone(anErr);
bail(anErr);
- serverConn->close();
return;
}
#endif
#endif
// read timeout to avoid getting stuck while reading from a silent server
- AsyncCall::Pointer nil;
+ typedef CommCbMemFunT<Security::PeerConnector, CommTimeoutCbParams> TimeoutDialer;
+ AsyncCall::Pointer timeoutCall = JobCallback(83, 5,
+ TimeoutDialer, this, Security::PeerConnector::commTimeoutHandler);
const auto timeout = Comm::MortalReadTimeout(startTime, negotiationTimeout);
- commSetConnTimeout(serverConnection(), timeout, nil);
+ commSetConnTimeout(serverConnection(), timeout, timeoutCall);
Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, new Pointer(this), 0);
}
Must(dialer);
dialer->answer().error = error;
+ if (const auto p = serverConnection()->getPeer())
+ peerConnectFailed(p);
+
+ callBack();
+ disconnect();
+
+ if (noteFwdPconnUse)
+ fwdPconnPool->noteUses(fd_table[serverConn->fd].pconn.uses);
+ serverConn->close();
+ serverConn = nullptr;
+}
+
+void
+Security::PeerConnector::sendSuccess()
+{
callBack();
- // Our job is done. The callabck recipient will probably close the failed
- // peer connection and try another peer or go direct (if possible). We
- // can close the connection ourselves (our error notification would reach
- // the recipient before the fd-closure notification), but we would rather
- // minimize the number of fd-closure notifications and let the recipient
- // manage the TCP state of the connection.
+ disconnect();
+}
+
+void
+Security::PeerConnector::disconnect()
+{
+ if (closeHandler) {
+ comm_remove_close_handler(serverConnection()->fd, closeHandler);
+ closeHandler = nullptr;
+ }
+
+ commUnsetConnTimeout(serverConnection());
}
void
// Do this now so that if we throw below, swanSong() assert that we _tried_
// to call back holds.
callback = NULL; // this should make done() true
-
- // remove close handler
- comm_remove_close_handler(serverConnection()->fd, closeHandler);
-
CbDialer *dialer = dynamic_cast<CbDialer*>(cb->getDialer());
Must(dialer);
dialer->answer().conn = serverConnection();
{
/**
- * Initiates encryption on a connection to peers or servers.
- * Despite its name does not perform any connect(2) operations.
+ * Initiates encryption of a given open TCP connection to a peer or server.
+ * Despite its name does not perform any connect(2) operations. Owns the
+ * connection during TLS negotiations. The caller receives EncryptorAnswer.
*
* Contains common code and interfaces of various specialized PeerConnector's,
* including peer certificate validation code.
- \par
- * The caller receives a call back with Security::EncryptorAnswer. If answer.error
- * is not nil, then there was an error and the encryption to the peer or server
- * was not fully established. The error object is suitable for error response
- * generation.
- \par
- * The caller must monitor the connection for closure because this
- * job will not inform the caller about such events.
- \par
- * PeerConnector class currently supports a form of TLS negotiation timeout,
- * which is accounted only when sets the read timeout from encrypted peers/servers.
- * For a complete solution, the caller must monitor the overall connection
- * establishment timeout and close the connection on timeouts. This is probably
- * better than having dedicated (or none at all!) timeouts for peer selection,
- * DNS lookup, TCP handshake, SSL handshake, etc. Some steps may have their
- * own timeout, but not all steps should be forced to have theirs.
- * XXX: tunnel.cc and probably other subsystems do not have an "overall
- * connection establishment" timeout. We need to change their code so that they
- * start monitoring earlier and close on timeouts. This change may need to be
- * discussed on squid-dev.
- \par
- * 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 PeerConnector: virtual public AsyncJob
{
AsyncCall::Pointer &aCallback,
const AccessLogEntryPointer &alp,
const time_t timeout = 0);
- virtual ~PeerConnector();
+ virtual ~PeerConnector() = default;
+
+ /// hack: whether the connection requires fwdPconnPool->noteUses()
+ bool noteFwdPconnUse;
protected:
// AsyncJob API
virtual void swanSong();
virtual const char *status() const;
+ /// The connection read timeout callback handler.
+ void commTimeoutHandler(const CommTimeoutCbParams &);
+
/// The comm_close callback handler.
void commCloseHandler(const CommCloseCbParams ¶ms);
- /// Inform us that the connection is closed. Does the required clean-up.
- void connectionClosed(const char *reason);
-
- /// Sets up TCP socket-related notification callbacks if things go wrong.
- /// If socket already closed return false, else install the comm_close
- /// handler to monitor the socket.
- bool prepareSocket();
-
/// \returns true on successful TLS session initialization
virtual bool initialize(Security::SessionPointer &);
/// mimics FwdState to minimize changes to FwdState::initiate/negotiateSsl
Comm::ConnectionPointer const &serverConnection() const { return serverConn; }
- void bail(ErrorState *error); ///< Return an error to the PeerConnector caller
+ /// sends the given error to the initiator
+ void bail(ErrorState *error);
+
+ /// sends the encrypted connection to the initiator
+ void sendSuccess();
- /// Callback the caller class, and pass the ready to communicate secure
- /// connection or an error if PeerConnector failed.
+ /// a bail(), sendSuccess() helper: sends results to the initiator
void callBack();
+ /// a bail(), sendSuccess() helper: stops monitoring the connection
+ void disconnect();
+
/// If called the certificates validator will not used
void bypassCertValidator() {useCertValidator_ = false;}
al->ssl.bumpMode = finalAction;
if (finalAction == Ssl::bumpTerminate) {
- serverConn->close();
+ bail(new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scForbidden, request.getRaw(), al));
clientConn->close();
+ clientConn = nullptr;
} else if (finalAction != Ssl::bumpSplice) {
//Allow write, proceed with the connection
srvBio->holdWrite(false);
if (!Security::PeerConnector::initialize(serverSession))
return false;
+ // client connection supplies TLS client details and is also used if we
+ // need to splice or terminate the client and server connections
+ if (!Comm::IsConnOpen(clientConn))
+ return false;
+
if (ConnStateData *csd = request->clientConnectionManager.valid()) {
- // client connection is required in the case we need to splice
- // or terminate client and server connections
- assert(clientConn != NULL);
SBuf *hostName = NULL;
//Enable Status_request TLS extension, required to bump some clients
if (!error) {
serverCertificateVerified();
if (splice) {
+ if (!Comm::IsConnOpen(clientConn)) {
+ bail(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
+ throw TextException("from-client connection gone", Here());
+ }
switchToTunnel(request.getRaw(), clientConn, serverConn);
tunnelInsteadOfNegotiating();
}
{
PeerConnector::PeerConnector(const Comm::ConnectionPointer &, AsyncCall::Pointer &, const AccessLogEntryPointer &, const time_t) :
AsyncJob("Security::PeerConnector") {STUB}
-PeerConnector::~PeerConnector() {STUB}
void PeerConnector::start() STUB
bool PeerConnector::doneAll() const STUB_RETVAL(true)
void PeerConnector::swanSong() STUB
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::commTimeoutHandler(const CommTimeoutCbParams &) STUB
bool PeerConnector::initialize(Security::SessionPointer &) STUB_RETVAL(false)
void PeerConnector::negotiate() STUB
bool PeerConnector::sslFinalized() STUB_RETVAL(false)
void PeerConnector::noteNegotiationError(const int, const int, const int) STUB
// virtual Security::ContextPointer getTlsContext() = 0;
void PeerConnector::bail(ErrorState *) STUB
+void PeerConnector::sendSuccess() STUB
void PeerConnector::callBack() STUB
+void PeerConnector::disconnect() STUB
void PeerConnector::recordNegotiationDetails() STUB
}
/// starts connecting to the next hop, either for the first time or while
/// recovering from the previous connect failure
void startConnecting();
+ void closePendingConnection(const Comm::ConnectionPointer &conn, const char *reason);
+
+ void retryOrBail();
/// called when negotiations with the peer have been successfully completed
- void notePeerReadyToShovel();
+ void notePeerReadyToShovel(const Comm::ConnectionPointer &);
class Connection
{
void copyRead(Connection &from, IOCB *completion);
/// continue to set up connection to a peer, going async for SSL peers
- void connectToPeer();
+ void connectToPeer(const Comm::ConnectionPointer &);
+ void secureConnectionToPeer(const Comm::ConnectionPointer &);
/* PeerSelectionInitiator API */
virtual void noteDestination(Comm::ConnectionPointer conn) override;
void usePinned();
- /// callback handler after connection setup (including any encryption)
- void connectedToPeer(Security::EncryptorAnswer &answer);
+ /// callback handler for the Security::PeerConnector encryptor
+ void noteSecurityPeerConnectorAnswer(Security::EncryptorAnswer &);
+
+ /// called after connection setup (including any encryption)
+ void connectedToPeer(const Comm::ConnectionPointer &);
+ void establishTunnelThruProxy(const Comm::ConnectionPointer &);
+
+ template <typename StepStart>
+ void advanceDestination(const char *stepDescription, const Comm::ConnectionPointer &conn, const StepStart &startStep);
/// details of the "last tunneling attempt" failure (if it failed)
ErrorState *savedError = nullptr;
tunnelState->server.closeIfOpen();
}
+void
+TunnelStateData::closePendingConnection(const Comm::ConnectionPointer &conn, const char *reason)
+{
+ debugs(26, 3, "because " << reason << "; " << conn);
+ assert(!server.conn);
+ if (IsConnOpen(conn))
+ conn->close();
+}
+
void
TunnelStateData::Connection::closeIfOpen()
{
tunnelStartShoveling(TunnelStateData *tunnelState)
{
assert(!tunnelState->waitingForConnectExchange);
+ assert(tunnelState->server.conn);
+ AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
+ CommTimeoutCbPtrFun(tunnelTimeout, tunnelState));
+ commSetConnTimeout(tunnelState->server.conn, Config.Timeout.read, timeoutCall);
+
*tunnelState->status_ptr = Http::scOkay;
if (tunnelState->logTag_ptr)
tunnelState->logTag_ptr->update(LOG_TCP_TUNNEL);
waitingForConnectExchange = false;
- if (answer.positive()) {
+ auto sawProblem = false;
+
+ if (!answer.positive()) {
+ sawProblem = true;
+ Must(!Comm::IsConnOpen(answer.conn));
+ } else if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ sawProblem = true;
+ closePendingConnection(answer.conn, "conn was closed while waiting for tunnelEstablishmentDone");
+ }
+
+ if (!sawProblem) {
+ assert(answer.positive()); // paranoid
// copy any post-200 OK bytes to our buffer
preReadServerData = answer.leftovers;
- notePeerReadyToShovel();
+ notePeerReadyToShovel(answer.conn);
return;
}
- // TODO: Reuse to-peer connections after a CONNECT error response.
-
- // TODO: We can and, hence, should close now, but tunnelServerClosed()
- // cannot yet tell whether ErrorState is still writing an error response.
- // server.closeIfOpen();
-
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();
+ debugs(50, 3, client.conn << " closing on CONNECT-to-peer error");
+ client.closeIfOpen();
return;
}
- ErrorState *error = answer.squidError.get();
- Must(error);
- answer.squidError.clear(); // preserve error for errorSendComplete()
- sendError(error, "tunneler returns error");
+ ErrorState *error = nullptr;
+ if (answer.positive()) {
+ error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw(), al);
+ } else {
+ error = answer.squidError.get();
+ Must(error);
+ answer.squidError.clear(); // preserve error for errorSendComplete()
+ }
+ assert(error);
+ saveError(error);
+ retryOrBail();
}
void
-TunnelStateData::notePeerReadyToShovel()
+TunnelStateData::notePeerReadyToShovel(const Comm::ConnectionPointer &conn)
{
+ server.conn = conn;
+ // Start monitoring for server-side connection problems
+ comm_add_close_handler(server.conn->fd, tunnelServerClosed, this);
+
if (!clientExpectsConnectResponse())
tunnelStartShoveling(this); // ssl-bumped connection, be quiet
else {
tunnelState->server.conn->close();
}
+/// reacts to the current destination failure
+void
+TunnelStateData::retryOrBail()
+{
+ // Since no TCP payload has been passed to client or server, we may
+ // TCP-connect to other destinations (including alternate IPs).
+
+ if (!FwdState::EnoughTimeToReForward(startTime))
+ return sendError(savedError, "forwarding timeout");
+
+ if (!destinations->empty())
+ return startConnecting();
+
+ if (!PeerSelectionInitiator::subscribed)
+ return sendError(savedError, "tried all destinations");
+}
+
void
TunnelStateData::noteConnection(HappyConnOpener::Answer &answer)
{
calls.connector = nullptr;
connOpener.clear();
- if (const auto error = answer.error.get()) {
+ ErrorState *error = nullptr;
+ if ((error = answer.error.get())) {
+ Must(!Comm::IsConnOpen(answer.conn));
syncHierNote(answer.conn, request->url.host());
+ answer.error.clear();
+ } else if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ error = new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw(), al);
+ closePendingConnection(answer.conn, "conn was closed while waiting for noteConnection");
+ }
+
+ if (error) {
saveError(error);
- answer.error.clear(); // savedError has it now
- sendError(savedError, "tried all destinations");
+ retryOrBail();
return;
}
TunnelStateData::connectDone(const Comm::ConnectionPointer &conn, const char *origin, const bool reused)
{
Must(Comm::IsConnOpen(conn));
- server.conn = conn;
if (reused)
ResetMarkingsToServer(request.getRaw(), *conn);
// else Comm::ConnOpener already applied proper/current markings
- syncHierNote(server.conn, request->url.host());
-
- request->hier.resetPeerNotes(conn, origin);
- if (al)
- al->hier.resetPeerNotes(conn, origin);
+ syncHierNote(conn, origin);
#if USE_DELAY_POOLS
/* no point using the delayIsNoDelay stuff since tunnel is nice and simple */
netdbPingSite(request->url.host());
request->peer_host = conn->getPeer() ? conn->getPeer()->host : nullptr;
- comm_add_close_handler(conn->fd, tunnelServerClosed, this);
bool toOrigin = false; // same semantics as StateFlags::toOrigin
if (const auto * const peer = conn->getPeer()) {
}
if (!toOrigin)
- connectToPeer();
+ connectToPeer(conn);
else {
- notePeerReadyToShovel();
+ notePeerReadyToShovel(conn);
}
-
- AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
- CommTimeoutCbPtrFun(tunnelTimeout, this));
- commSetConnTimeout(conn, Config.Timeout.read, timeoutCall);
}
void
}
void
-TunnelStateData::connectToPeer()
+TunnelStateData::connectToPeer(const Comm::ConnectionPointer &conn)
{
- if (CachePeer *p = server.conn->getPeer()) {
- if (p->secure.encryptTransport) {
- AsyncCall::Pointer callback = asyncCall(5,4,
- "TunnelStateData::ConnectedToPeer",
- MyAnswerDialer(&TunnelStateData::connectedToPeer, this));
- auto *connector = new Security::BlindPeerConnector(request, server.conn, callback, al);
- AsyncJob::Start(connector); // will call our callback
- return;
- }
+ if (const auto p = conn->getPeer()) {
+ if (p->secure.encryptTransport)
+ return advanceDestination("secure connection to peer", conn, [this,&conn] {
+ secureConnectionToPeer(conn);
+ });
}
- Security::EncryptorAnswer nil;
- connectedToPeer(nil);
+ connectedToPeer(conn);
}
+/// encrypts an established TCP connection to peer
void
-TunnelStateData::connectedToPeer(Security::EncryptorAnswer &answer)
+TunnelStateData::secureConnectionToPeer(const Comm::ConnectionPointer &conn)
+{
+ AsyncCall::Pointer callback = asyncCall(5,4, "TunnelStateData::noteSecurityPeerConnectorAnswer",
+ MyAnswerDialer(&TunnelStateData::noteSecurityPeerConnectorAnswer, this));
+ const auto connector = new Security::BlindPeerConnector(request, conn, callback, al);
+ AsyncJob::Start(connector); // will call our callback
+}
+
+/// starts a preparation step for an established connection; retries on failures
+template <typename StepStart>
+void
+TunnelStateData::advanceDestination(const char *stepDescription, const Comm::ConnectionPointer &conn, const StepStart &startStep)
+{
+ // TODO: Extract destination-specific handling from TunnelStateData so that
+ // all the awkward, limited-scope advanceDestination() calls can be replaced
+ // with a single simple try/catch,retry block.
+ try {
+ startStep();
+ // now wait for the step callback
+ } catch (...) {
+ debugs (26, 2, "exception while trying to " << stepDescription << ": " << CurrentException);
+ closePendingConnection(conn, "connection preparation exception");
+ if (!savedError)
+ saveError(new ErrorState(ERR_CANNOT_FORWARD, Http::scInternalServerError, request.getRaw(), al));
+ retryOrBail();
+ }
+}
+
+/// callback handler for the connection encryptor
+void
+TunnelStateData::noteSecurityPeerConnectorAnswer(Security::EncryptorAnswer &answer)
{
if (ErrorState *error = answer.error.get()) {
+ Must(!Comm::IsConnOpen(answer.conn));
answer.error.clear(); // sendError() will own the error
sendError(error, "TLS peer connection error");
return;
}
+ if (!Comm::IsConnOpen(answer.conn) || fd_table[answer.conn->fd].closing()) {
+ sendError(new ErrorState(ERR_CANNOT_FORWARD, Http::scServiceUnavailable, request.getRaw(), al), "connecion gone");
+ closePendingConnection(answer.conn, "conn was closed while waiting for noteSecurityPeerConnectorAnswer");
+ return;
+ }
+
+ connectedToPeer(answer.conn);
+}
+
+void
+TunnelStateData::connectedToPeer(const Comm::ConnectionPointer &conn)
+{
+ advanceDestination("establish tunnel through proxy", conn, [this,&conn] {
+ establishTunnelThruProxy(conn);
+ });
+}
+
+void
+TunnelStateData::establishTunnelThruProxy(const Comm::ConnectionPointer &conn)
+{
assert(!waitingForConnectExchange);
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);
+ const auto tunneler = new Http::Tunneler(conn, request, callback, Config.Timeout.lifetime, al);
#if USE_DELAY_POOLS
tunneler->setDelayId(server.delayId);
#endif
void
switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::ConnectionPointer &srvConn)
{
+ Must(Comm::IsConnOpen(clientConn));
+ Must(Comm::IsConnOpen(srvConn));
+
debugs(26,5, "Revert to tunnel FD " << clientConn->fd << " with FD " << srvConn->fd);
/* Create state structure. */
else
request->prepForDirect();
- AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
- CommTimeoutCbPtrFun(tunnelTimeout, tunnelState));
- commSetConnTimeout(srvConn, Config.Timeout.read, timeoutCall);
-
// we drain any already buffered from-server data below (rBufData)
fd_table[srvConn->fd].useDefaultIo();