isRetriable(true),
isRepeatable(true),
ignoreLastWrite(false),
- stopReason(NULL),
- connector(NULL),
- reader(NULL),
- writer(NULL),
- closer(NULL),
+ waitingForDns(false),
alep(new AccessLogEntry),
- al(*alep),
- cs(NULL)
+ al(*alep)
{
debugs(93,3, typeName << " constructed, this=" << this <<
" [icapx" << id << ']'); // we should not call virtual status() here
// TODO: service bypass status may differ from that of a transaction
typedef CommCbMemFunT<Adaptation::Icap::Xaction, CommConnectCbParams> ConnectDialer;
- connector = JobCallback(93,3, ConnectDialer, this, Adaptation::Icap::Xaction::noteCommConnected);
- cs = new Comm::ConnOpener(connection, connector, TheConfig.connect_timeout(service().cfg().bypass));
+ AsyncCall::Pointer callback = JobCallback(93, 3, ConnectDialer, this, Adaptation::Icap::Xaction::noteCommConnected);
+ const auto cs = new Comm::ConnOpener(conn, callback, TheConfig.connect_timeout(service().cfg().bypass));
cs->setHost(s.cfg().host.termedBuf());
- AsyncJob::Start(cs.get());
+ transportWait.start(cs, callback);
}
- /*
- * This event handler is necessary to work around the no-rentry policy
- * of Adaptation::Icap::Xaction::callStart()
- */
- #if 0
- void
- Adaptation::Icap::Xaction::reusedConnection(void *data)
- {
- debugs(93, 5, HERE << "reused connection");
- Adaptation::Icap::Xaction *x = (Adaptation::Icap::Xaction*)data;
- x->noteCommConnected(Comm::OK);
- }
- #endif
-
void Adaptation::Icap::Xaction::closeConnection()
{
if (haveConnection()) {
// unexpected connection close while talking to the ICAP service
void Adaptation::Icap::Xaction::noteCommClosed(const CommCloseCbParams &)
{
- if (securer != NULL) {
- securer->cancel("Connection closed before SSL negotiation finished");
- securer = NULL;
+ if (connection) {
+ connection->noteClosure();
+ connection = nullptr;
}
closer = NULL;
- detailError(ERR_DETAIL_ICAP_XACT_CLOSE);
- handleCommClosed();
-}
+
-void Adaptation::Icap::Xaction::handleCommClosed()
-{
+ static const auto d = MakeNamedErrorDetail("ICAP_XACT_CLOSE");
+ detailError(d);
mustStop("ICAP service connection externally closed");
}
bool Adaptation::Icap::Xaction::doneAll() const
{
- return !transportWait && !encryptionWait && !reader && !writer && Adaptation::Initiate::doneAll();
- return !waitingForDns && !connector && !securer && !reader && !writer &&
++ return !waitingForDns && !transportWait && !encryptionWait &&
++ !reader && !writer &&
+ Adaptation::Initiate::doneAll();
}
void Adaptation::Icap::Xaction::updateTimeout()
debugs(93, 5, "TLS negotiation to " << service().cfg().uri << " complete");
- service().noteConnectionUse(answer.conn);
+ assert(answer.conn);
+
+ // The socket could get closed while our callback was queued. Sync
+ // Connection. XXX: Connection::fd may already be stale/invalid here.
+ if (answer.conn->isOpen() && fd_table[answer.conn->fd].closing()) {
+ answer.conn->noteClosure();
+ service().noteConnectionFailed("external TLS connection closure");
- detailError(ERR_DETAIL_ICAP_XACT_SSL_START);
++ static const auto d = MakeNamedErrorDetail("ICAP_XACT_SSL_CLOSE");
++ detailError(d);
+ throw TexcHere("external closure of the TLS ICAP service connection");
+ }
- handleCommConnected();
+ useIcapConnection(answer.conn);
}
void
Security::PeerConnector::commCloseHandler(const CommCloseCbParams ¶ms)
{
+ debugs(83, 5, "FD " << params.fd << ", Security::PeerConnector=" << params.data);
+
closeHandler = nullptr;
+ if (serverConn) {
+ countFailingConnection();
+ serverConn->noteClosure();
+ serverConn = nullptr;
+ }
- debugs(83, 5, "FD " << params.fd << ", Security::PeerConnector=" << params.data);
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
+ static const auto d = MakeNamedErrorDetail("TLS_CONNECT_CLOSE");
+ err->detailError(d);
bail(err);
}
if (fd_table[fd].closing())
return;
+ const auto result = Security::Connect(*serverConnection());
+
#if USE_OPENSSL
- auto session = fd_table[fd].ssl.get();
- debugs(83, 5, "SSL_connect session=" << (void*)session);
- const int result = SSL_connect(session);
- if (result <= 0) {
- #elif USE_GNUTLS
- auto session = fd_table[fd].ssl.get();
- const int result = gnutls_handshake(session);
- debugs(83, 5, "gnutls_handshake session=" << (void*)session << ", result=" << result);
-
- if (result == GNUTLS_E_SUCCESS) {
- char *desc = gnutls_session_get_desc(session);
- debugs(83, 2, serverConnection() << " TLS Session info: " << desc);
- gnutls_free(desc);
+ auto &sconn = *fd_table[fd].ssl;
+
+ // log ASAP, even if the handshake has not completed (or failed)
+ keyLogger.checkpoint(sconn, *this);
+
+ // OpenSSL v1 APIs do not allow unthreaded applications like Squid to fetch
+ // missing certificates _during_ OpenSSL certificate validation. Our
+ // handling of X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY (abbreviated
+ // here as EUNABLE) approximates what would happen if we did (attempt to)
+ // fetch any missing certificates during OpenSSL certificate validation.
+ // * We did not hide EUNABLE; SSL_connect() was successful: Handle success.
+ // * We did not hide EUNABLE; SSL_connect() reported some error E: Honor E.
+ // * We hid EUNABLE; SSL_connect() was successful: Remember success and try
+ // to fetch the missing certificates. If all goes well, honor success.
+ // * We hid EUNABLE; SSL_connect() reported EUNABLE: Warn but honor EUNABLE.
+ // * We hid EUNABLE; SSL_connect() reported some EOTHER: Remember EOTHER and
+ // try to fetch the missing certificates. If all goes well, honor EOTHER.
+ // If fetching or post-fetching validation fails, then honor that failure
+ // because EOTHER would not have happened if we fetched during validation.
+ if (auto &hidMissingIssuer = Ssl::VerifyCallbackParameters::At(sconn).hidMissingIssuer) {
+ hidMissingIssuer = false; // prep for the next SSL_connect()
+
+ if (result.category == IoResult::ioSuccess ||
+ !(result.errorDetail && result.errorDetail->errorNo() == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY))
+ return handleMissingCertificates(result);
+
+ debugs(83, DBG_IMPORTANT, "BUG: Honoring unexpected SSL_connect() error: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY");
+ // fall through to regular error handling
}
-
- if (result != GNUTLS_E_SUCCESS) {
- // debug the TLS session state so far
- auto descIn = gnutls_handshake_get_last_in(session);
- debugs(83, 2, "handshake IN: " << gnutls_handshake_description_get_name(descIn));
- auto descOut = gnutls_handshake_get_last_out(session);
- debugs(83, 2, "handshake OUT: " << gnutls_handshake_description_get_name(descOut));
- #else
- if (const int result = -1) {
#endif
- handleNegotiateError(result);
- return; // we might be gone by now
- }
- recordNegotiationDetails();
+ handleNegotiationResult(result);
+ }
+
+ void
+ Security::PeerConnector::handleNegotiationResult(const Security::IoResult &result)
+ {
+ switch (result.category) {
+ case Security::IoResult::ioSuccess:
+ recordNegotiationDetails();
- if (sslFinalized())
++ if (sslFinalized() && callback)
+ sendSuccess();
+ return; // we may be gone by now
- if (!sslFinalized())
+ case Security::IoResult::ioWantRead:
+ noteWantRead();
+ return;
+
+ case Security::IoResult::ioWantWrite:
+ noteWantWrite();
return;
- if (callback) // true sslFinalized() may bail(), calling the callback
- sendSuccess();
+ case Security::IoResult::ioError:
+ break; // fall through to error handling
+ }
+
+ // TODO: Honor result.important when working in a reverse proxy role?
+ debugs(83, 2, "ERROR: " << result.errorDescription <<
+ " while establishing TLS connection on FD: " << serverConnection()->fd << result.errorDetail);
+ recordNegotiationDetails();
+ noteNegotiationError(result.errorDetail);
}
bool
Security::PeerConnector::sslCrtvdHandleReply(Ssl::CertValidationResponse::Pointer validationResponse)
{
Must(validationResponse != NULL);
+ Must(Comm::IsConnOpen(serverConnection()));
- Ssl::ErrorDetail *errDetails = NULL;
+ ErrorDetail::Pointer errDetails;
bool validatorFailed = false;
if (Debug::Enabled(83, 5)) {
/// The first honored error, if any, is returned via errDetails parameter.
/// The method returns all seen errors except SSL_ERROR_NONE as Security::CertErrors.
Security::CertErrors *
- Security::PeerConnector::sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &resp, Ssl::ErrorDetail *& errDetails)
+ Security::PeerConnector::sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &resp, ErrorDetail::Pointer &errDetails)
{
+ Must(Comm::IsConnOpen(serverConnection()));
+
ACLFilledChecklist *check = NULL;
Security::SessionPointer session(fd_table[serverConnection()->fd].ssl);
void
Security::PeerConnector::noteWantRead()
{
- const int fd = serverConnection()->fd;
debugs(83, 5, serverConnection());
- #if USE_OPENSSL
- Security::SessionPointer session(fd_table[fd].ssl);
- BIO *b = SSL_get_rbio(session.get());
- Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
- if (srvBio->holdRead()) {
- if (srvBio->gotHello()) {
- if (checkForMissingCertificates())
- return; // Wait to download certificates before proceed.
-
- srvBio->holdRead(false);
- // schedule a negotiateSSl to allow openSSL parse received data
- negotiateSsl();
- return;
- } else if (srvBio->gotHelloFailed()) {
- srvBio->holdRead(false);
- debugs(83, DBG_IMPORTANT, "Error parsing SSL Server Hello Message on FD " << fd);
- // schedule a negotiateSSl to allow openSSL parse received data
- negotiateSsl();
- return;
- }
- }
- #endif
+ Must(Comm::IsConnOpen(serverConnection()));
+ const int fd = serverConnection()->fd;
+
// read timeout to avoid getting stuck while reading from a silent server
typedef CommCbMemFunT<Security::PeerConnector, CommTimeoutCbParams> TimeoutDialer;
AsyncCall::Pointer timeoutCall = JobCallback(83, 5,
"Security::PeerConnector::certDownloadingDone",
PeerConnectorCertDownloaderDialer(&Security::PeerConnector::certDownloadingDone, this));
- const Downloader *csd = (request ? dynamic_cast<const Downloader*>(request->downloader.valid()) : nullptr);
- Downloader *dl = new Downloader(url, certCallback, XactionInitiator::initCertFetcher, csd ? csd->nestedLevel() + 1 : 1);
+ const auto dl = new Downloader(url, certCallback, XactionInitiator::initCertFetcher, certDownloadNestingLevel() + 1);
- AsyncJob::Start(dl);
+ certDownloadWait.start(dl, certCallback);
}
void
++certsDownloads;
debugs(81, 5, "Certificate downloading status: " << downloadStatus << " certificate size: " << obj.length());
- // get ServerBio from SSL object
+ Must(Comm::IsConnOpen(serverConnection()));
- const int fd = serverConnection()->fd;
- Security::SessionPointer session(fd_table[fd].ssl);
- BIO *b = SSL_get_rbio(session.get());
- Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
+ const auto &sconn = *fd_table[serverConnection()->fd].ssl;
// Parse Certificate. Assume that it is in DER format.
// According to RFC 4325:
return;
}
- srvBio->holdRead(false);
- negotiateSsl();
+ resumeNegotiation();
}
- bool
- Security::PeerConnector::checkForMissingCertificates()
+ void
+ Security::PeerConnector::handleMissingCertificates(const Security::IoResult &ioResult)
{
- // Check for nested SSL certificates downloads. For example when the
- // certificate located in an SSL site which requires to download a
- // a missing certificate (... from an SSL site which requires to ...).
++ Must(Comm::IsConnOpen(serverConnection()));
+ auto &sconn = *fd_table[serverConnection()->fd].ssl;
+
+ // We download the missing certificate(s) once. We would prefer to clear
+ // this right after the first validation, but that ideal place is _inside_
+ // OpenSSL if validation is triggered by SSL_connect(). That function and
+ // our OpenSSL verify_callback function (\ref OpenSSL_vcb_disambiguation)
+ // may be called multiple times, so we cannot reset there.
+ auto &callerHandlesMissingCertificates = Ssl::VerifyCallbackParameters::At(sconn).callerHandlesMissingCertificates;
+ Must(callerHandlesMissingCertificates);
+ callerHandlesMissingCertificates = false;
+
+ if (!computeMissingCertificateUrls(sconn))
+ return handleNegotiationResult(ioResult);
+
+ suspendNegotiation(ioResult);
- const Downloader *csd = (request ? request->downloader.get() : nullptr);
- if (csd && csd->nestedLevel() >= MaxNestedDownloads)
+ assert(!urlsOfMissingCerts.empty());
+ startCertDownloading(urlsOfMissingCerts.front());
+ urlsOfMissingCerts.pop();
+ }
+
+ /// finds URLs of (some) missing intermediate certificates or returns false
+ bool
+ Security::PeerConnector::computeMissingCertificateUrls(const Connection &sconn)
+ {
+ const auto certs = SSL_get_peer_cert_chain(&sconn);
+ if (!certs) {
+ debugs(83, 3, "nothing to bootstrap the fetch with");
return false;
+ }
+ debugs(83, 5, "server certificates: " << sk_X509_num(certs));
- Must(Comm::IsConnOpen(serverConnection()));
- const int fd = serverConnection()->fd;
- Security::SessionPointer session(fd_table[fd].ssl);
- BIO *b = SSL_get_rbio(session.get());
- Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
- const Security::CertList &certs = srvBio->serverCertificatesIfAny();
+ const auto ctx = getTlsContext();
+ if (!Ssl::missingChainCertificatesUrls(urlsOfMissingCerts, *certs, ctx))
+ return false; // missingChainCertificatesUrls() reports the exact reason
- if (certs.size()) {
- debugs(83, 5, "SSL server sent " << certs.size() << " certificates");
- ContextPointer ctx(getTlsContext());
- Ssl::missingChainCertificatesUrls(urlsOfMissingCerts, certs, ctx);
- if (urlsOfMissingCerts.size()) {
- startCertDownloading(urlsOfMissingCerts.front());
- urlsOfMissingCerts.pop();
- return true;
- }
+ debugs(83, 5, "URLs: " << urlsOfMissingCerts.size());
+ assert(!urlsOfMissingCerts.empty());
+ return true;
+ }
+
+ void
+ Security::PeerConnector::suspendNegotiation(const Security::IoResult &ioResult)
+ {
+ debugs(83, 5, "after " << ioResult);
+ Must(!isSuspended());
+ suspendedError_ = new Security::IoResult(ioResult);
+ Must(isSuspended());
+ // negotiations resume with a resumeNegotiation() call
+ }
+
+ void
+ Security::PeerConnector::resumeNegotiation()
+ {
+ Must(isSuspended());
+
+ auto lastError = suspendedError_; // may be reset below
+ suspendedError_ = nullptr;
+
+ auto &sconn = *fd_table[serverConnection()->fd].ssl;
+ if (!Ssl::VerifyConnCertificates(sconn, downloadedCerts)) {
+ // simulate an earlier SSL_connect() failure with a new error
+ // TODO: When we can use Security::ErrorDetail, we should resume with a
+ // detailed _validation_ error, not just a generic SSL_ERROR_SSL!
+ const ErrorDetail::Pointer errorDetail = new ErrorDetail(SQUID_TLS_ERR_CONNECT, SSL_ERROR_SSL, 0);
+ lastError = new Security::IoResult(errorDetail);
}
- return false;
+ handleNegotiationResult(*lastError);
}
+
#endif //USE_OPENSSL