author: Alex Rousskov , Christos Tsantilas Ssl::PeerConnector class This patch investigates the new Ssl::PeerConnector class. This class connects Squid client-side to a SSL cache_peer or SSL server. It is used by TunnelStateData and FwdState to initiate and establish the SSL connection. This class handles peer certificate validation. The caller receives a call back with PeerConnectorAnswer. In the SSL connection is not established because of an error, an error object suitable for error response generation is attached to PeerConnectorAnser The Ssl::PeerConnector class includes the old SSL initialization code from FwdState class. This is a Measurement Factory project === modified file 'src/FwdState.cc' --- src/FwdState.cc 2014-03-31 06:57:27 +0000 +++ src/FwdState.cc 2014-04-11 10:55:23 +0000 @@ -59,62 +59,86 @@ #include "internal.h" #include "ip/Intercept.h" #include "ip/QosConfig.h" #include "ip/tools.h" #include "MemObject.h" #include "mgr/Registration.h" #include "neighbors.h" #include "pconn.h" #include "PeerSelectState.h" #include "SquidConfig.h" #include "SquidTime.h" #include "Store.h" #include "StoreClient.h" #include "urn.h" #include "whois.h" #if USE_OPENSSL #include "ssl/cert_validate_message.h" #include "ssl/Config.h" #include "ssl/ErrorDetail.h" #include "ssl/helper.h" +#include "ssl/PeerConnector.h" #include "ssl/ServerBump.h" #include "ssl/support.h" #endif #if HAVE_ERRNO_H #include #endif static PSC fwdPeerSelectionCompleteWrapper; static CLCB fwdServerClosedWrapper; -#if USE_OPENSSL -static PF fwdNegotiateSSLWrapper; -#endif static CNCB fwdConnectDoneWrapper; static OBJH fwdStats; #define MAX_FWD_STATS_IDX 9 static int FwdReplyCodes[MAX_FWD_STATS_IDX + 1][Http::scInvalidHeader + 1]; static PconnPool *fwdPconnPool = new PconnPool("server-side"); CBDATA_CLASS_INIT(FwdState); +#if USE_OPENSSL +class FwdStatePeerAnswerDialer: public CallDialer, public Ssl::PeerConnector::CbDialer +{ +public: + typedef void (FwdState::*Method)(Ssl::PeerConnectorAnswer &); + + FwdStatePeerAnswerDialer(Method method, FwdState *fwd): + method_(method), fwd_(fwd), answer_() {} + + /* CallDialer API */ + virtual bool canDial(AsyncCall &call) { return fwd_.valid(); } + void dial(AsyncCall &call) { ((&(*fwd_))->*method_)(answer_); } + virtual void print(std::ostream &os) const { + os << '(' << fwd_.get() << ", " << answer_ << ')'; } + + /* Ssl::PeerConnector::CbDialer API */ + virtual Ssl::PeerConnectorAnswer &answer() { return answer_; } + +private: + Method method_; + CbcPointer fwd_; + Ssl::PeerConnectorAnswer answer_; +}; +#endif + + void FwdState::abort(void* d) { FwdState* fwd = (FwdState*)d; Pointer tmp = fwd; // Grab a temporary pointer to keep the object alive during our scope. if (Comm::IsConnOpen(fwd->serverConnection())) { comm_remove_close_handler(fwd->serverConnection()->fd, fwdServerClosedWrapper, fwd); debugs(17, 3, HERE << "store entry aborted; closing " << fwd->serverConnection()); fwd->serverConnection()->close(); } else { debugs(17, 7, HERE << "store entry aborted; no connection to close"); } fwd->serverDestinations.clear(); fwd->self = NULL; } /**** PUBLIC INTERFACE ********************************************************/ @@ -491,50 +515,40 @@ } /**** CALLBACK WRAPPERS ************************************************************/ static void fwdPeerSelectionCompleteWrapper(Comm::ConnectionList * unused, ErrorState *err, void *data) { FwdState *fwd = (FwdState *) data; if (err) fwd->fail(err); fwd->startConnectionOrFail(); } static void fwdServerClosedWrapper(const CommCloseCbParams ¶ms) { FwdState *fwd = (FwdState *)params.data; fwd->serverClosed(params.fd); } -#if USE_OPENSSL -static void -fwdNegotiateSSLWrapper(int fd, void *data) -{ - FwdState *fwd = (FwdState *) data; - fwd->negotiateSSL(fd); -} - -#endif - void fwdConnectDoneWrapper(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno, void *data) { FwdState *fwd = (FwdState *) data; fwd->connectDone(conn, status, xerrno); } /**** PRIVATE *****************************************************************/ /* * FwdState::checkRetry * * Return TRUE if the request SHOULD be retried. This method is * called when the HTTP connection fails, or when the connection * is closed before server-side read the end of HTTP headers. */ bool FwdState::checkRetry() { if (shutting_down) @@ -629,440 +643,105 @@ // If the Server quits before nibbling at the request body, the body sender // will not know (so that we can retry). Call this if we will not retry. We // will notify the sender so that it does not get stuck waiting for space. void FwdState::doneWithRetries() { if (request && request->body_pipe != NULL) request->body_pipe->expectNoConsumption(); } // called by the server that failed after calling unregister() void FwdState::handleUnregisteredServerEnd() { debugs(17, 2, HERE << "self=" << self << " err=" << err << ' ' << entry->url()); assert(!Comm::IsConnOpen(serverConn)); retryOrBail(); } -#if USE_OPENSSL -void -FwdState::negotiateSSL(int fd) -{ - unsigned long ssl_lib_error = SSL_ERROR_NONE; - SSL *ssl = fd_table[fd].ssl; - int ret; - - if ((ret = SSL_connect(ssl)) <= 0) { - int ssl_error = SSL_get_error(ssl, ret); -#ifdef EPROTO - int sysErrNo = EPROTO; -#else - int sysErrNo = EACCES; -#endif - - switch (ssl_error) { - - case SSL_ERROR_WANT_READ: - Comm::SetSelect(fd, COMM_SELECT_READ, fwdNegotiateSSLWrapper, this, 0); - return; - - case SSL_ERROR_WANT_WRITE: - Comm::SetSelect(fd, COMM_SELECT_WRITE, fwdNegotiateSSLWrapper, this, 0); - return; - - case SSL_ERROR_SSL: - case SSL_ERROR_SYSCALL: - ssl_lib_error = ERR_get_error(); - debugs(81, DBG_IMPORTANT, "fwdNegotiateSSL: Error negotiating SSL connection on FD " << fd << - ": " << ERR_error_string(ssl_lib_error, NULL) << " (" << ssl_error << - "/" << ret << "/" << errno << ")"); - - // store/report errno when ssl_error is SSL_ERROR_SYSCALL, ssl_lib_error is 0, and ret is -1 - if (ssl_error == SSL_ERROR_SYSCALL && ret == -1 && ssl_lib_error == 0) - sysErrNo = errno; - - // falling through to complete error handling - - default: - // TODO: move into a method before merge - Ssl::ErrorDetail *errDetails; - Ssl::ErrorDetail *errFromFailure = (Ssl::ErrorDetail *)SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail); - if (errFromFailure != NULL) { - // The errFromFailure is attached to the ssl object - // and will be released when ssl object destroyed. - // Copy errFromFailure to a new Ssl::ErrorDetail object. - errDetails = new Ssl::ErrorDetail(*errFromFailure); - } else { - // server_cert can be NULL here - X509 *server_cert = SSL_get_peer_certificate(ssl); - errDetails = new Ssl::ErrorDetail(SQUID_ERR_SSL_HANDSHAKE, server_cert, NULL); - X509_free(server_cert); - } - - if (ssl_lib_error != SSL_ERROR_NONE) - errDetails->setLibError(ssl_lib_error); - - if (request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - serverBump->serverCert.resetAndLock(errDetails->peerCert()); - - // remember validation errors, if any - if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) - serverBump->sslErrors = cbdataReference(errs); - } - - // For intercepted connections, set the host name to the server - // certificate CN. Otherwise, we just hope that CONNECT is using - // a user-entered address (a host name or a user-entered IP). - const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted(); - if (request->flags.sslPeek && !isConnectRequest) { - if (X509 *srvX509 = errDetails->peerCert()) { - if (const char *name = Ssl::CommonHostName(srvX509)) { - request->SetHost(name); - debugs(83, 3, HERE << "reset request host: " << name); - } - } - } - } - - ErrorState *const anErr = makeConnectingError(ERR_SECURE_CONNECT_FAIL); - anErr->xerrno = sysErrNo; - anErr->detail = errDetails; - fail(anErr); - - if (serverConnection()->getPeer()) { - peerConnectFailed(serverConnection()->getPeer()); - } - - serverConn->close(); - return; - } - } - - if (request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - serverBump->serverCert.reset(SSL_get_peer_certificate(ssl)); - - // remember validation errors, if any - if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) - serverBump->sslErrors = cbdataReference(errs); - } - } - - if (serverConnection()->getPeer() && !SSL_session_reused(ssl)) { - if (serverConnection()->getPeer()->sslSession) - SSL_SESSION_free(serverConnection()->getPeer()->sslSession); - - serverConnection()->getPeer()->sslSession = SSL_get1_session(ssl); - } - - if (Ssl::TheConfig.ssl_crt_validator) { - Ssl::CertValidationRequest validationRequest; - // WARNING: Currently we do not use any locking for any of the - // members of the Ssl::CertValidationRequest class. In this code the - // Ssl::CertValidationRequest object used only to pass data to - // Ssl::CertValidationHelper::submit method. - validationRequest.ssl = ssl; - validationRequest.domainName = request->GetHost(); - if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) - // validationRequest disappears on return so no need to cbdataReference - validationRequest.errors = errs; - else - validationRequest.errors = NULL; - try { - debugs(83, 5, "Sending SSL certificate for validation to ssl_crtvd."); - Ssl::CertValidationHelper::GetInstance()->sslSubmit(validationRequest, sslCrtvdHandleReplyWrapper, this); - return; - } catch (const std::exception &e) { - debugs(33, DBG_IMPORTANT, "ERROR: Failed to compose ssl_crtvd " << - "request for " << validationRequest.domainName << - " certificate: " << e.what() << "; will now block to " << - "validate that certificate."); - // fall through to do blocking in-process generation. - ErrorState *anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request); - fail(anErr); - if (serverConnection()->getPeer()) { - peerConnectFailed(serverConnection()->getPeer()); - } - serverConn->close(); - self = NULL; - return; - } - } - - dispatch(); -} - -void -FwdState::sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &validationResponse) -{ - FwdState * fwd = (FwdState *)(data); - fwd->sslCrtvdHandleReply(validationResponse); -} - -void -FwdState::sslCrtvdHandleReply(Ssl::CertValidationResponse const &validationResponse) -{ - Ssl::CertErrors *errs = NULL; - Ssl::ErrorDetail *errDetails = NULL; - bool validatorFailed = false; - if (!Comm::IsConnOpen(serverConnection())) { - return; - } - - debugs(83,5, request->GetHost() << " cert validation result: " << validationResponse.resultCode); - - if (validationResponse.resultCode == HelperReply::Error) - errs = sslCrtvdCheckForErrors(validationResponse, errDetails); - else if (validationResponse.resultCode != HelperReply::Okay) - validatorFailed = true; - - if (!errDetails && !validatorFailed) { - dispatch(); - return; - } - - ErrorState *anErr = NULL; - if (validatorFailed) { - anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request); - } else { - - // Check the list error with - if (errDetails && request->clientConnectionManager.valid()) { - // remember the server certificate from the ErrorDetail object - if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { - // remember validation errors, if any - if (errs) { - if (serverBump->sslErrors) - cbdataReferenceDone(serverBump->sslErrors); - serverBump->sslErrors = cbdataReference(errs); - } - } - } - - anErr = makeConnectingError(ERR_SECURE_CONNECT_FAIL); - anErr->detail = errDetails; - /*anErr->xerrno= Should preserved*/ - } - - fail(anErr); - if (serverConnection()->getPeer()) { - peerConnectFailed(serverConnection()->getPeer()); - } - serverConn->close(); - self = NULL; - return; -} - -/// Checks errors in the cert. validator response against sslproxy_cert_error. -/// The first honored error, if any, is returned via errDetails parameter. -/// The method returns all seen errors except SSL_ERROR_NONE as Ssl::CertErrors. -Ssl::CertErrors * -FwdState::sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &resp, Ssl::ErrorDetail *& errDetails) -{ - Ssl::CertErrors *errs = NULL; - - ACLFilledChecklist *check = NULL; - if (acl_access *acl = Config.ssl_client.cert_error) - check = new ACLFilledChecklist(acl, request, dash_str); - - SSL *ssl = fd_table[serverConnection()->fd].ssl; - typedef Ssl::CertValidationResponse::RecvdErrors::const_iterator SVCRECI; - for (SVCRECI i = resp.errors.begin(); i != resp.errors.end(); ++i) { - debugs(83, 7, "Error item: " << i->error_no << " " << i->error_reason); - - assert(i->error_no != SSL_ERROR_NONE); - - if (!errDetails) { - bool allowed = false; - if (check) { - check->sslErrors = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get())); - if (check->fastCheck() == ACCESS_ALLOWED) - allowed = true; - } - // else the Config.ssl_client.cert_error access list is not defined - // and the first error will cause the error page - - if (allowed) { - debugs(83, 3, "bypassing SSL error " << i->error_no << " in " << "buffer"); - } else { - debugs(83, 5, "confirming SSL error " << i->error_no); - X509 *brokenCert = i->cert.get(); - Ssl::X509_Pointer peerCert(SSL_get_peer_certificate(ssl)); - const char *aReason = i->error_reason.empty() ? NULL : i->error_reason.c_str(); - errDetails = new Ssl::ErrorDetail(i->error_no, peerCert.get(), brokenCert, aReason); - } - if (check) { - delete check->sslErrors; - check->sslErrors = NULL; - } - } - - if (!errs) - errs = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get())); - else - errs->push_back_unique(Ssl::CertError(i->error_no, i->cert.get())); - } - if (check) - delete check; - - return errs; -} - -void -FwdState::initiateSSL() -{ - SSL *ssl; - SSL_CTX *sslContext = NULL; - const CachePeer *peer = serverConnection()->getPeer(); - int fd = serverConnection()->fd; - - if (peer) { - assert(peer->use_ssl); - sslContext = peer->sslContext; - } else { - sslContext = Config.ssl_client.sslContext; - } - - assert(sslContext); - - if ((ssl = SSL_new(sslContext)) == NULL) { - debugs(83, DBG_IMPORTANT, "fwdInitiateSSL: Error allocating handle: " << ERR_error_string(ERR_get_error(), NULL) ); - ErrorState *anErr = new ErrorState(ERR_SOCKET_FAILURE, Http::scInternalServerError, request); - // TODO: create Ssl::ErrorDetail with OpenSSL-supplied error code - fail(anErr); - self = NULL; // refcounted - return; - } - - SSL_set_fd(ssl, fd); - - if (peer) { - if (peer->ssldomain) - SSL_set_ex_data(ssl, ssl_ex_index_server, peer->ssldomain); - -#if NOT_YET - - else if (peer->name) - SSL_set_ex_data(ssl, ssl_ex_index_server, peer->name); - -#endif - - else - SSL_set_ex_data(ssl, ssl_ex_index_server, peer->host); - - if (peer->sslSession) - SSL_set_session(ssl, peer->sslSession); - - } else { - // While we are peeking at the certificate, we may not know the server - // name that the client will request (after interception or CONNECT) - // unless it was the CONNECT request with a user-typed address. - const char *hostname = request->GetHost(); - const bool hostnameIsIp = request->GetHostIsNumeric(); - const bool isConnectRequest = request->clientConnectionManager.valid() && - !request->clientConnectionManager->port->flags.isIntercepted(); - if (!request->flags.sslPeek || isConnectRequest) - SSL_set_ex_data(ssl, ssl_ex_index_server, (void*)hostname); - - // Use SNI TLS extension only when we connect directly - // to the origin server and we know the server host name. - if (!hostnameIsIp) - Ssl::setClientSNI(ssl, hostname); - } - - // If CertValidation Helper used do not lookup checklist for errors, - // but keep a list of errors to send it to CertValidator - if (!Ssl::TheConfig.ssl_crt_validator) { - // Create the ACL check list now, while we have access to more info. - // The list is used in ssl_verify_cb() and is freed in ssl_free(). - if (acl_access *acl = Config.ssl_client.cert_error) { - ACLFilledChecklist *check = new ACLFilledChecklist(acl, request, dash_str); - SSL_set_ex_data(ssl, ssl_ex_index_cert_error_check, check); - } - } - - // store peeked cert to check SQUID_X509_V_ERR_CERT_CHANGE - X509 *peeked_cert; - if (request->clientConnectionManager.valid() && - request->clientConnectionManager->serverBump() && - (peeked_cert = request->clientConnectionManager->serverBump()->serverCert.get())) { - CRYPTO_add(&(peeked_cert->references),1,CRYPTO_LOCK_X509); - SSL_set_ex_data(ssl, ssl_ex_index_ssl_peeked_cert, peeked_cert); - } - - fd_table[fd].ssl = ssl; - fd_table[fd].read_method = &ssl_read_method; - fd_table[fd].write_method = &ssl_write_method; - negotiateSSL(fd); -} - -#endif - void FwdState::connectDone(const Comm::ConnectionPointer &conn, comm_err_t status, int xerrno) { if (status != COMM_OK) { ErrorState *const anErr = makeConnectingError(ERR_CONNECT_FAIL); anErr->xerrno = xerrno; fail(anErr); /* it might have been a timeout with a partially open link */ if (conn != NULL) { if (conn->getPeer()) peerConnectFailed(conn->getPeer()); conn->close(); } retryOrBail(); return; } serverConn = conn; flags.connected_okay = true; debugs(17, 3, HERE << serverConnection() << ": '" << entry->url() << "'" ); comm_add_close_handler(serverConnection()->fd, fwdServerClosedWrapper, this); if (serverConnection()->getPeer()) peerConnectSucceded(serverConnection()->getPeer()); #if USE_OPENSSL if (!request->flags.pinned) { if ((serverConnection()->getPeer() && serverConnection()->getPeer()->use_ssl) || (!serverConnection()->getPeer() && request->protocol == AnyP::PROTO_HTTPS) || request->flags.sslPeek) { - initiateSSL(); + + HttpRequest::Pointer requestPointer = request; + AsyncCall::Pointer callback = asyncCall(17,4, + "FwdState::ConnectedToPeer", + FwdStatePeerAnswerDialer(&FwdState::connectedToPeer, this)); + Ssl::PeerConnector *connector = + new Ssl::PeerConnector(requestPointer, serverConnection(), callback); + AsyncJob::Start(connector); // will call our callback return; } } #endif dispatch(); } +#if USE_OPENSSL +void +FwdState::connectedToPeer(Ssl::PeerConnectorAnswer &answer) +{ + if (ErrorState *error = answer.error.get()) { + fail(error); + answer.error.clear(); // preserve error for errorSendComplete() + self = NULL; + return; + } + + dispatch(); +} +#endif + void FwdState::connectTimeout(int fd) { debugs(17, 2, "fwdConnectTimeout: FD " << fd << ": '" << entry->url() << "'" ); assert(serverDestinations[0] != NULL); assert(fd == serverDestinations[0]->fd); if (entry->isEmpty()) { ErrorState *anErr = new ErrorState(ERR_CONNECT_FAIL, Http::scGatewayTimeout, request); anErr->xerrno = ETIMEDOUT; fail(anErr); /* This marks the peer DOWN ... */ if (serverDestinations[0]->getPeer()) peerConnectFailed(serverDestinations[0]->getPeer()); } if (Comm::IsConnOpen(serverDestinations[0])) { serverDestinations[0]->close(); } === modified file 'src/FwdState.h' --- src/FwdState.h 2014-03-30 12:00:34 +0000 +++ src/FwdState.h 2014-04-09 16:40:53 +0000 @@ -7,40 +7,41 @@ #include "err_type.h" #include "fde.h" #include "http/StatusCode.h" #include "ip/Address.h" #if USE_OPENSSL #include "ssl/support.h" #endif /* forward decls */ class AccessLogEntry; typedef RefCount AccessLogEntryPointer; class ErrorState; class HttpRequest; #if USE_OPENSSL namespace Ssl { class ErrorDetail; class CertValidationResponse; +class PeerConnectorAnswer; }; #endif /** * Returns the TOS value that we should be setting on the connection * to the server, based on the ACL. */ tos_t GetTosToServer(HttpRequest * request); /** * Returns the Netfilter mark value that we should be setting on the * connection to the server, based on the ACL. */ nfmark_t GetNfmarkToServer(HttpRequest * request); class HelperReply; class FwdState : public RefCountable { public: @@ -52,75 +53,68 @@ static void Start(const Comm::ConnectionPointer &client, StoreEntry *, HttpRequest *, const AccessLogEntryPointer &alp); /// Same as Start() but no master xaction info (AccessLogEntry) available. static void fwdStart(const Comm::ConnectionPointer &client, StoreEntry *, HttpRequest *); /// This is the real beginning of server connection. Call it whenever /// the forwarding server destination has changed and a new one needs to be opened. /// Produces the cannot-forward error on fail if no better error exists. void startConnectionOrFail(); void fail(ErrorState *err); void unregister(Comm::ConnectionPointer &conn); void unregister(int fd); void complete(); void handleUnregisteredServerEnd(); int reforward(); bool reforwardableStatus(const Http::StatusCode s) const; void serverClosed(int fd); void connectStart(); void connectDone(const Comm::ConnectionPointer & conn, comm_err_t status, int xerrno); void connectTimeout(int fd); - void initiateSSL(); - void negotiateSSL(int fd); bool checkRetry(); bool checkRetriable(); void dispatch(); void pconnPush(Comm::ConnectionPointer & conn, const char *domain); bool dontRetry() { return flags.dont_retry; } void dontRetry(bool val) { flags.dont_retry = val; } /** return a ConnectionPointer to the current server connection (may or may not be open) */ Comm::ConnectionPointer const & serverConnection() const { return serverConn; }; -#if USE_OPENSSL - /// Callback function called when squid receive message from cert validator helper - static void sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &); - /// Process response from cert validator helper - void sslCrtvdHandleReply(Ssl::CertValidationResponse const &); - /// Check SSL errors returned from cert validator against sslproxy_cert_error access list - Ssl::CertErrors *sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &, Ssl::ErrorDetail *&); -#endif private: // hidden for safer management of self; use static fwdStart FwdState(const Comm::ConnectionPointer &client, StoreEntry *, HttpRequest *, const AccessLogEntryPointer &alp); void start(Pointer aSelf); #if STRICT_ORIGINAL_DST void selectPeerForIntercepted(); #endif static void logReplyStatus(int tries, const Http::StatusCode status); void doneWithRetries(); void completed(); void retryOrBail(); ErrorState *makeConnectingError(const err_type type) const; +#if USE_OPENSSL + void connectedToPeer(Ssl::PeerConnectorAnswer &answer); +#endif static void RegisterWithCacheManager(void); public: StoreEntry *entry; HttpRequest *request; AccessLogEntryPointer al; ///< info for the future access.log entry static void abort(void*); private: Pointer self; ErrorState *err; Comm::ConnectionPointer clientConn; ///< a possibly open connection to the client. time_t start_t; int n_tries; // AsyncCalls which we set and may need cancelling. struct { AsyncCall::Pointer connector; ///< a call linking us to the ConnOpener producing serverConn. } calls; === modified file 'src/errorpage.cc' --- src/errorpage.cc 2014-03-31 06:57:27 +0000 +++ src/errorpage.cc 2014-04-03 15:27:57 +0000 @@ -551,40 +551,49 @@ ErrorDynamicPages.push_back(info); id = info->id; } return (err_type)id; } /// \ingroup ErrorPageInternal const char * errorPageName(int pageId) { if (pageId >= ERR_NONE && pageId < ERR_MAX) /* common case */ return err_type_str[pageId]; if (pageId >= ERR_MAX && pageId - ERR_MAX < (ssize_t)ErrorDynamicPages.size()) return ErrorDynamicPages[pageId - ERR_MAX]->page_name; return "ERR_UNKNOWN"; /* should not happen */ } +ErrorState * +ErrorState::NewForwarding(err_type type, HttpRequest *request) +{ + assert(request); + const Http::StatusCode status = request->flags.needValidation ? + Http::scGatewayTimeout : Http::scServiceUnavailable; + return new ErrorState(type, status, request); +} + ErrorState::ErrorState(err_type t, Http::StatusCode status, HttpRequest * req) : type(t), page_id(t), err_language(NULL), httpStatus(status), #if USE_AUTH auth_user_request (NULL), #endif request(NULL), url(NULL), xerrno(0), port(0), dnsError(), ttl(0), src_addr(), redirect_url(NULL), callback(NULL), callback_data(NULL), request_hdrs(NULL), err_msg(NULL), === modified file 'src/errorpage.h' --- src/errorpage.h 2014-03-30 12:00:34 +0000 +++ src/errorpage.h 2014-04-03 14:55:21 +0000 @@ -84,40 +84,43 @@ w - cachemgr email address x W - error data (to be included in the mailto links) x - error name x z - dns server error message x Z - Preformatted error message x \endverbatim */ class HttpReply; class HttpRequest; class MemBuf; /// \ingroup ErrorPageAPI class ErrorState { public: ErrorState(err_type type, Http::StatusCode, HttpRequest * request); ErrorState(); // not implemented. ~ErrorState(); + /// Creates a general request forwarding error with the right http_status. + static ErrorState *NewForwarding(err_type type, HttpRequest *request); + /** * Allocates and initializes an error response */ HttpReply *BuildHttpReply(void); /// set error type-specific detail code void detailError(int dCode) {detailCode = dCode;} private: /** * Locates error page template to be used for this error * and constructs the HTML page content from it. */ MemBuf *BuildContent(void); /** * Convert the given template string into textual output * * \param text The string to be converted * \param allowRecursion Whether to convert codes which output may contain codes === modified file 'src/ssl/Makefile.am' --- src/ssl/Makefile.am 2012-11-24 14:12:30 +0000 +++ src/ssl/Makefile.am 2014-04-03 10:56:20 +0000 @@ -10,40 +10,42 @@ ssl_crtd.8 if USE_SSL_CRTD SSL_CRTD = ssl_crtd else SSL_CRTD = endif ## SSL stuff used by main Squid but not by ssl_crtd libsslsquid_la_SOURCES = \ cert_validate_message.cc \ cert_validate_message.h \ context_storage.cc \ context_storage.h \ Config.cc \ Config.h \ ErrorDetail.cc \ ErrorDetail.h \ ErrorDetailManager.cc \ ErrorDetailManager.h \ + PeerConnector.cc \ + PeerConnector.h \ ProxyCerts.h \ ServerBump.cc \ ServerBump.h \ support.cc \ support.h \ helper.cc \ helper.h ## SSL stuff used by main Squid and ssl_crtd libsslutil_la_SOURCES = \ gadgets.cc \ gadgets.h \ crtd_message.cc \ crtd_message.h libexec_PROGRAMS = \ $(SSL_CRTD) if USE_SSL_CRTD ssl_crtd_SOURCES = ssl_crtd.cc certificate_db.cc certificate_db.h === added file 'src/ssl/PeerConnector.cc' --- src/ssl/PeerConnector.cc 1970-01-01 00:00:00 +0000 +++ src/ssl/PeerConnector.cc 2014-04-12 17:41:10 +0000 @@ -0,0 +1,545 @@ +/* + * DEBUG: section 17 Request Forwarding + * + */ + +#include "squid.h" +#include "acl/FilledChecklist.h" +#include "base/AsyncCbdataCalls.h" +#include "CachePeer.h" +#include "client_side.h" +#include "comm/Loops.h" +#include "errorpage.h" +#include "fde.h" +#include "globals.h" +#include "HttpRequest.h" +#include "neighbors.h" +#include "ssl/cert_validate_message.h" +#include "ssl/Config.h" +#include "ssl/ErrorDetail.h" +#include "ssl/helper.h" +#include "ssl/PeerConnector.h" +#include "ssl/ServerBump.h" +#include "ssl/support.h" +#include "SquidConfig.h" + +CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeerConnector); + +Ssl::PeerConnector::PeerConnector( + HttpRequestPointer &aRequest, + const Comm::ConnectionPointer &aServerConn, + AsyncCall::Pointer &aCallback): + AsyncJob("Ssl::PeerConnector"), + request(aRequest), + serverConn(aServerConn), + callback(aCallback) +{ + // if this throws, the caller's cb dialer is not our CbDialer + Must(dynamic_cast(callback->getDialer())); +} + +Ssl::PeerConnector::~PeerConnector() +{ + debugs(83, 5, "Peer connector " << this << " gone"); +} + +bool Ssl::PeerConnector::doneAll() const +{ + return (!callback || callback->canceled()) && AsyncJob::doneAll(); +} + +/// Preps connection and SSL state. Calls negotiate(). +void +Ssl::PeerConnector::start() +{ + AsyncJob::start(); + + if (prepareSocket()) { + initializeSsl(); + negotiateSsl(); + } +} + +void +Ssl::PeerConnector::commCloseHandler(const CommCloseCbParams ¶ms) +{ + debugs(83, 5, "FD " << params.fd << ", Ssl::PeerConnector=" << params.data); + connectionClosed("Ssl::PeerConnector::commCloseHandler"); +} + +void +Ssl::PeerConnector::connectionClosed(const char *reason) +{ + mustStop(reason); + callback = NULL; +} + +bool +Ssl::PeerConnector::prepareSocket() +{ + const int fd = serverConnection()->fd; + if (!Comm::IsConnOpen(serverConn) || fd_table[serverConn->fd].closing()) { + connectionClosed("Ssl::PeerConnector::prepareSocket"); + return false; + } + + // watch for external connection closures + typedef CommCbMemFunT Dialer; + closeHandler = JobCallback(9, 5, Dialer, this, Ssl::PeerConnector::commCloseHandler); + comm_add_close_handler(fd, closeHandler); + return true; +} + +void +Ssl::PeerConnector::initializeSsl() +{ + SSL *ssl; + SSL_CTX *sslContext = NULL; + const CachePeer *peer = serverConnection()->getPeer(); + const int fd = serverConnection()->fd; + + if (peer) { + assert(peer->use_ssl); + sslContext = peer->sslContext; + } else { + sslContext = ::Config.ssl_client.sslContext; + } + + assert(sslContext); + + if ((ssl = SSL_new(sslContext)) == NULL) { + ErrorState *anErr = new ErrorState(ERR_SOCKET_FAILURE, Http::scInternalServerError, request.getRaw()); + anErr->xerrno = errno; + debugs(83, DBG_IMPORTANT, "Error allocating SSL handle: " << ERR_error_string(ERR_get_error(), NULL)); + bail(anErr); + return; + } + + SSL_set_fd(ssl, fd); + + if (peer) { + if (peer->ssldomain) + SSL_set_ex_data(ssl, ssl_ex_index_server, peer->ssldomain); + +#if NOT_YET + + else if (peer->name) + SSL_set_ex_data(ssl, ssl_ex_index_server, peer->name); + +#endif + + else + SSL_set_ex_data(ssl, ssl_ex_index_server, peer->host); + + if (peer->sslSession) + SSL_set_session(ssl, peer->sslSession); + + } else { + // While we are peeking at the certificate, we may not know the server + // name that the client will request (after interception or CONNECT) + // unless it was the CONNECT request with a user-typed address. + const char *hostname = request->GetHost(); + const bool hostnameIsIp = request->GetHostIsNumeric(); + const bool isConnectRequest = request->clientConnectionManager.valid() && + !request->clientConnectionManager->port->flags.isIntercepted(); + if (!request->flags.sslPeek || isConnectRequest) + SSL_set_ex_data(ssl, ssl_ex_index_server, (void*)hostname); + + // Use SNI TLS extension only when we connect directly + // to the origin server and we know the server host name. + if (!hostnameIsIp) + Ssl::setClientSNI(ssl, hostname); + } + + // If CertValidation Helper used do not lookup checklist for errors, + // but keep a list of errors to send it to CertValidator + if (!Ssl::TheConfig.ssl_crt_validator) { + // Create the ACL check list now, while we have access to more info. + // The list is used in ssl_verify_cb() and is freed in ssl_free(). + if (acl_access *acl = ::Config.ssl_client.cert_error) { + ACLFilledChecklist *check = new ACLFilledChecklist(acl, request.getRaw(), dash_str); + // check->fd(fd); XXX: need client FD here + SSL_set_ex_data(ssl, ssl_ex_index_cert_error_check, check); + } + } + + // store peeked cert to check SQUID_X509_V_ERR_CERT_CHANGE + X509 *peeked_cert; + if (request->clientConnectionManager.valid() && + request->clientConnectionManager->serverBump() && + (peeked_cert = request->clientConnectionManager->serverBump()->serverCert.get())) { + CRYPTO_add(&(peeked_cert->references),1,CRYPTO_LOCK_X509); + SSL_set_ex_data(ssl, ssl_ex_index_ssl_peeked_cert, peeked_cert); + } + + fd_table[fd].ssl = ssl; + fd_table[fd].read_method = &ssl_read_method; + fd_table[fd].write_method = &ssl_write_method; +} + +void +Ssl::PeerConnector::negotiateSsl() +{ + if (!Comm::IsConnOpen(serverConnection()) || fd_table[serverConnection()->fd].closing()) + return; + + const int fd = serverConnection()->fd; + SSL *ssl = fd_table[fd].ssl; + const int result = SSL_connect(ssl); + if (result <= 0) { + handleNegotiateError(result); + return; // we might be gone by now + } + + if (request->clientConnectionManager.valid()) { + // remember the server certificate from the ErrorDetail object + if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { + serverBump->serverCert.reset(SSL_get_peer_certificate(ssl)); + + // remember validation errors, if any + if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) + serverBump->sslErrors = cbdataReference(errs); + } + } + + if (serverConnection()->getPeer() && !SSL_session_reused(ssl)) { + if (serverConnection()->getPeer()->sslSession) + SSL_SESSION_free(serverConnection()->getPeer()->sslSession); + + serverConnection()->getPeer()->sslSession = SSL_get1_session(ssl); + } + + if (Ssl::TheConfig.ssl_crt_validator) { + Ssl::CertValidationRequest validationRequest; + // WARNING: Currently we do not use any locking for any of the + // members of the Ssl::CertValidationRequest class. In this code the + // Ssl::CertValidationRequest object used only to pass data to + // Ssl::CertValidationHelper::submit method. + validationRequest.ssl = ssl; + validationRequest.domainName = request->GetHost(); + if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) + // validationRequest disappears on return so no need to cbdataReference + validationRequest.errors = errs; + else + validationRequest.errors = NULL; + try { + debugs(83, 5, "Sending SSL certificate for validation to ssl_crtvd."); + Ssl::CertValidationHelper::GetInstance()->sslSubmit(validationRequest, sslCrtvdHandleReplyWrapper, this); + return; + } catch (const std::exception &e) { + debugs(83, DBG_IMPORTANT, "ERROR: Failed to compose ssl_crtvd " << + "request for " << validationRequest.domainName << + " certificate: " << e.what() << "; will now block to " << + "validate that certificate."); + // fall through to do blocking in-process generation. + ErrorState *anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw()); + bail(anErr); + if (serverConnection()->getPeer()) { + peerConnectFailed(serverConnection()->getPeer()); + } + serverConn->close(); + return; + } + } + + callBack(); +} + +void +Ssl::PeerConnector::sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &validationResponse) +{ + Ssl::PeerConnector *connector = (Ssl::PeerConnector *)(data); + connector->sslCrtvdHandleReply(validationResponse); +} + +void +Ssl::PeerConnector::sslCrtvdHandleReply(Ssl::CertValidationResponse const &validationResponse) +{ + Ssl::CertErrors *errs = NULL; + Ssl::ErrorDetail *errDetails = NULL; + bool validatorFailed = false; + if (!Comm::IsConnOpen(serverConnection())) { + return; + } + + debugs(83,5, request->GetHost() << " cert validation result: " << validationResponse.resultCode); + + if (validationResponse.resultCode == HelperReply::Error) + errs = sslCrtvdCheckForErrors(validationResponse, errDetails); + else if (validationResponse.resultCode != HelperReply::Okay) + validatorFailed = true; + + if (!errDetails && !validatorFailed) { + callBack(); + return; + } + + ErrorState *anErr = NULL; + if (validatorFailed) { + anErr = new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw()); + } else { + + // Check the list error with + if (errDetails && request->clientConnectionManager.valid()) { + // remember the server certificate from the ErrorDetail object + if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { + // remember validation errors, if any + if (errs) { + if (serverBump->sslErrors) + cbdataReferenceDone(serverBump->sslErrors); + serverBump->sslErrors = cbdataReference(errs); + } + } + } + + anErr = new ErrorState(ERR_SECURE_CONNECT_FAIL, Http::scServiceUnavailable, request.getRaw()); + anErr->detail = errDetails; + /*anErr->xerrno= Should preserved*/ + } + + bail(anErr); + if (serverConnection()->getPeer()) { + peerConnectFailed(serverConnection()->getPeer()); + } + serverConn->close(); + return; +} + +/// Checks errors in the cert. validator response against sslproxy_cert_error. +/// The first honored error, if any, is returned via errDetails parameter. +/// The method returns all seen errors except SSL_ERROR_NONE as Ssl::CertErrors. +Ssl::CertErrors * +Ssl::PeerConnector::sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &resp, Ssl::ErrorDetail *& errDetails) +{ + Ssl::CertErrors *errs = NULL; + + ACLFilledChecklist *check = NULL; + if (acl_access *acl = ::Config.ssl_client.cert_error) + check = new ACLFilledChecklist(acl, request.getRaw(), dash_str); + + SSL *ssl = fd_table[serverConnection()->fd].ssl; + typedef Ssl::CertValidationResponse::RecvdErrors::const_iterator SVCRECI; + for (SVCRECI i = resp.errors.begin(); i != resp.errors.end(); ++i) { + debugs(83, 7, "Error item: " << i->error_no << " " << i->error_reason); + + assert(i->error_no != SSL_ERROR_NONE); + + if (!errDetails) { + bool allowed = false; + if (check) { + check->sslErrors = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get())); + if (check->fastCheck() == ACCESS_ALLOWED) + allowed = true; + } + // else the Config.ssl_client.cert_error access list is not defined + // and the first error will cause the error page + + if (allowed) { + debugs(83, 3, "bypassing SSL error " << i->error_no << " in " << "buffer"); + } else { + debugs(83, 5, "confirming SSL error " << i->error_no); + X509 *brokenCert = i->cert.get(); + Ssl::X509_Pointer peerCert(SSL_get_peer_certificate(ssl)); + const char *aReason = i->error_reason.empty() ? NULL : i->error_reason.c_str(); + errDetails = new Ssl::ErrorDetail(i->error_no, peerCert.get(), brokenCert, aReason); + } + if (check) { + delete check->sslErrors; + check->sslErrors = NULL; + } + } + + if (!errs) + errs = new Ssl::CertErrors(Ssl::CertError(i->error_no, i->cert.get())); + else + errs->push_back_unique(Ssl::CertError(i->error_no, i->cert.get())); + } + if (check) + delete check; + + return errs; +} + +/// A wrapper for Comm::SetSelect() notifications. +void +Ssl::PeerConnector::NegotiateSsl(int, void *data) +{ + PeerConnector *pc = static_cast(data); + // Use job calls to add done() checks and other job logic/protections. + CallJobHere(83, 7, pc, Ssl::PeerConnector, negotiateSsl); +} + +void +Ssl::PeerConnector::handleNegotiateError(const int ret) +{ + const int fd = serverConnection()->fd; + unsigned long ssl_lib_error = SSL_ERROR_NONE; + SSL *ssl = fd_table[fd].ssl; + int ssl_error = SSL_get_error(ssl, ret); + +#ifdef EPROTO + int sysErrNo = EPROTO; +#else + int sysErrNo = EACCES; +#endif + + switch (ssl_error) { + + case SSL_ERROR_WANT_READ: + Comm::SetSelect(fd, COMM_SELECT_READ, &NegotiateSsl, this, 0); + return; + + case SSL_ERROR_WANT_WRITE: + Comm::SetSelect(fd, COMM_SELECT_WRITE, &NegotiateSsl, this, 0); + return; + + case SSL_ERROR_SSL: + case SSL_ERROR_SYSCALL: + ssl_lib_error = ERR_get_error(); + + // store/report errno when ssl_error is SSL_ERROR_SYSCALL, ssl_lib_error is 0, and ret is -1 + if (ssl_error == SSL_ERROR_SYSCALL && ret == -1 && ssl_lib_error == 0) + sysErrNo = errno; + + debugs(83, DBG_IMPORTANT, "Error negotiating SSL on FD " << fd << + ": " << ERR_error_string(ssl_lib_error, NULL) << " (" << + ssl_error << "/" << ret << "/" << errno << ")"); + + break; // proceed to the general error handling code + + default: + break; // no special error handling for all other errors + } + + ErrorState *const anErr = ErrorState::NewForwarding(ERR_SECURE_CONNECT_FAIL, request.getRaw()); + anErr->xerrno = sysErrNo; + + Ssl::ErrorDetail *errFromFailure = (Ssl::ErrorDetail *)SSL_get_ex_data(ssl, ssl_ex_index_ssl_error_detail); + if (errFromFailure != NULL) { + // The errFromFailure is attached to the ssl object + // and will be released when ssl object destroyed. + // Copy errFromFailure to a new Ssl::ErrorDetail object + anErr->detail = new Ssl::ErrorDetail(*errFromFailure); + } else { + // server_cert can be NULL here + X509 *server_cert = SSL_get_peer_certificate(ssl); + anErr->detail = new Ssl::ErrorDetail(SQUID_ERR_SSL_HANDSHAKE, server_cert, NULL); + X509_free(server_cert); + } + + if (ssl_lib_error != SSL_ERROR_NONE) + anErr->detail->setLibError(ssl_lib_error); + + if (request->clientConnectionManager.valid()) { + // remember the server certificate from the ErrorDetail object + if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) { + serverBump->serverCert.resetAndLock(anErr->detail->peerCert()); + + // remember validation errors, if any + if (Ssl::CertErrors *errs = static_cast(SSL_get_ex_data(ssl, ssl_ex_index_ssl_errors))) + serverBump->sslErrors = cbdataReference(errs); + } + + // For intercepted connections, set the host name to the server + // certificate CN. Otherwise, we just hope that CONNECT is using + // a user-entered address (a host name or a user-entered IP). + const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted(); + if (request->flags.sslPeek && !isConnectRequest) { + if (X509 *srvX509 = anErr->detail->peerCert()) { + if (const char *name = Ssl::CommonHostName(srvX509)) { + request->SetHost(name); + debugs(83, 3, HERE << "reset request host: " << name); + } + } + } + } + + bail(anErr); +} + +void +Ssl::PeerConnector::bail(ErrorState *error) +{ + Must(error); // or the recepient will not know there was a problem + + // XXX: forward.cc calls peerConnectSucceeded() after an OK TCP connect but + // we call peerConnectFailed() if SSL failed afterwards. Is that OK? + // 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); + + Must(callback != NULL); + CbDialer *dialer = dynamic_cast(callback->getDialer()); + Must(dialer); + dialer->answer().error = error; + + callBack(); + // Our job is done. The callabck recepient 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 recepient before the fd-closure notification), but we would rather + // minimize the number of fd-closure notifications and let the recepient + // manage the TCP state of the connection. +} + +void +Ssl::PeerConnector::callBack() +{ + AsyncCall::Pointer cb = callback; + // 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(cb->getDialer()); + Must(dialer); + dialer->answer().conn = serverConnection(); + ScheduleCallHere(cb); +} + + +void +Ssl::PeerConnector::swanSong() +{ + // XXX: unregister fd-closure monitoring and CommSetSelect interest, if any + AsyncJob::swanSong(); + assert(!callback); // paranoid: we have not left the caller waiting +} + +const char * +Ssl::PeerConnector::status() const +{ + static MemBuf buf; + buf.reset(); + + // TODO: redesign AsyncJob::status() API to avoid this + // id and stop reason reporting duplication. + buf.append(" [", 2); + if (stopReason != NULL) { + buf.Printf("Stopped, reason:"); + buf.Printf("%s",stopReason); + } + if (serverConn != NULL) + buf.Printf(" FD %d", serverConn->fd); + buf.Printf(" %s%u]", id.Prefix, id.value); + buf.terminate(); + + return buf.content(); +} + +/* PeerConnectorAnswer */ + +Ssl::PeerConnectorAnswer::~PeerConnectorAnswer() +{ + delete error.get(); +} + +std::ostream & +operator <<(std::ostream &os, const Ssl::PeerConnectorAnswer &answer) +{ + return os << answer.conn << ", " << answer.error; +} === added file 'src/ssl/PeerConnector.h' --- src/ssl/PeerConnector.h 1970-01-01 00:00:00 +0000 +++ src/ssl/PeerConnector.h 2014-04-12 18:01:44 +0000 @@ -0,0 +1,170 @@ +/* + * SQUID Web Proxy Cache http://www.squid-cache.org/ + * ---------------------------------------------------------- + * + * Squid is the result of efforts by numerous individuals from + * the Internet community; see the CONTRIBUTORS file for full + * details. Many organizations have provided support for Squid's + * development; see the SPONSORS file for full details. Squid is + * Copyrighted (C) 2001 by the Regents of the University of + * California; see the COPYRIGHT file for full details. Squid + * incorporates software developed and/or copyrighted by other + * sources; see the CREDITS file for full details. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. + * + */ +#ifndef SQUID_SSL_PEER_CONNECTOR_H +#define SQUID_SSL_PEER_CONNECTOR_H + +#include "base/AsyncJob.h" +#include "base/AsyncCbdataCalls.h" +#include "ssl/support.h" +#include + +class HttpRequest; +class ErrorState; + +namespace Ssl { + +class ErrorDetail; +class CertValidationResponse; + +/// PeerConnector results (supplied via a callback). +/// The connection to peer was secured if and only if the error member is nil. +class PeerConnectorAnswer { +public: + ~PeerConnectorAnswer(); ///< deletes error if it is still set + Comm::ConnectionPointer conn; ///< peer connection (secured on success) + + /// answer recepients must clear the error member in order to keep its info + /// XXX: We should refcount ErrorState instead of cbdata-protecting it. + CbcPointer error; ///< problem details (nil on success) +}; + +/** + \par + * Connects Squid client-side to an SSL peer (cache_peer ... ssl). + * Handles peer certificate validation. + * Used by TunnelStateData, FwdState, and PeerPoolMgr to start talking to an + * SSL peer. + \par + * The caller receives a call back with PeerConnectorAnswer. If answer.error + * is not nil, then there was an error and the SSL connection to the SSL peer + * 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 + * 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: Neither tunnel.cc + * nor forward.cc have a "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 +{ +public: + /// Callback dialier API to allow PeerConnector to set the answer. + class CbDialer { + public: + virtual ~CbDialer() {} + /// gives PeerConnector access to the in-dialer answer + virtual PeerConnectorAnswer &answer() = 0; + }; + + typedef RefCount HttpRequestPointer; + +public: + PeerConnector(HttpRequestPointer &aRequest, + const Comm::ConnectionPointer &aServerConn, + AsyncCall::Pointer &aCallback); + virtual ~PeerConnector(); + +protected: + // AsyncJob API + virtual void start(); + virtual bool doneAll() const; + virtual void swanSong(); + virtual const char *status() const; + + /// 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(); + + void initializeSsl(); ///< Initializes SSL state + + /// Performs a single secure connection negotiation step. + /// It is called multiple times untill the negotiation finish or aborted. + void negotiateSsl(); + + /// Called when the SSL negotiation step aborted because data needs to + /// be transferred to/from SSL server or on error. In the first case + /// setups the appropriate Comm::SetSelect handler. In second case + /// fill an error and report to the PeerConnector caller. + void handleNegotiateError(const int result); + +private: + PeerConnector(const PeerConnector &); // not implemented + PeerConnector &operator =(const PeerConnector &); // not implemented + + /// 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 + + /// Callback the caller class, and pass the ready to communicate secure + /// connection or an error if PeerConnector failed. + void callBack(); + + /// Process response from cert validator helper + void sslCrtvdHandleReply(Ssl::CertValidationResponse const &); + + /// Check SSL errors returned from cert validator against sslproxy_cert_error access list + Ssl::CertErrors *sslCrtvdCheckForErrors(Ssl::CertValidationResponse const &, Ssl::ErrorDetail *&); + + /// Callback function called when squid receive message from cert validator helper + static void sslCrtvdHandleReplyWrapper(void *data, Ssl::CertValidationResponse const &); + + /// A wrapper function for negotiateSsl for use with Comm::SetSelect + static void NegotiateSsl(int fd, void *data); + + HttpRequestPointer request; ///< peer connection trigger or cause + Comm::ConnectionPointer serverConn; ///< TCP connection to the peer + AsyncCall::Pointer callback; ///< we call this with the results + AsyncCall::Pointer closeHandler; ///< we call this when the connection closed + + CBDATA_CLASS2(PeerConnector); +}; + +} // namespace Ssl + +std::ostream &operator <<(std::ostream &os, const Ssl::PeerConnectorAnswer &a); + +#endif /* SQUID_PEER_CONNECTOR_H */ === modified file 'src/tunnel.cc' --- src/tunnel.cc 2014-02-21 10:46:19 +0000 +++ src/tunnel.cc 2014-04-12 17:42:37 +0000 @@ -33,40 +33,43 @@ #include "squid.h" #include "acl/FilledChecklist.h" #include "CachePeer.h" #include "client_side.h" #include "client_side_request.h" #include "comm.h" #include "comm/Connection.h" #include "comm/ConnOpener.h" #include "comm/Write.h" #include "errorpage.h" #include "fde.h" #include "http.h" #include "HttpRequest.h" #include "HttpStateFlags.h" #include "ip/QosConfig.h" #include "MemBuf.h" #include "PeerSelectState.h" #include "SquidConfig.h" #include "StatCounters.h" +#if USE_OPENSSL +#include "ssl/PeerConnector.h" +#endif #include "tools.h" #if USE_DELAY_POOLS #include "DelayId.h" #endif #include #include /** * TunnelStateData is the state engine performing the tasks for * setup of a TCP tunnel from an existing open client FD to a server * then shuffling binary data between the resulting FD pair. */ /* * TODO 1: implement a read/write API on ConnStateData to send/receive blocks * of pre-formatted data. Then we can use that as the client side of the tunnel * instead of re-implementing it here and occasionally getting the ConnStateData * read/write state wrong. * * TODO 2: then convert this into a AsyncJob, possibly a child of 'Server' @@ -140,41 +143,72 @@ char *buf; int64_t *size_ptr; /* pointer to size in an ConnStateData for logging */ Comm::ConnectionPointer conn; ///< The currently connected connection. private: #if USE_DELAY_POOLS DelayId delayId; #endif }; Connection client, server; int *status_ptr; /* pointer to status for logging */ MemBuf *connectRespBuf; ///< accumulates peer CONNECT response when we need it bool connectReqWriting; ///< whether we are writing a CONNECT request to a peer void copyRead(Connection &from, IOCB *completion); + /// continue to set up connection to a peer, going async for SSL peers + void connectToPeer(); + private: +#if USE_OPENSSL + /// Gives PeerConnector access to Answer in the TunnelStateData callback dialer. + class MyAnswerDialer: public CallDialer, public Ssl::PeerConnector::CbDialer + { + public: + typedef void (TunnelStateData::*Method)(Ssl::PeerConnectorAnswer &); + + MyAnswerDialer(Method method, TunnelStateData *tunnel): + method_(method), tunnel_(tunnel), answer_() {} + + /* CallDialer API */ + virtual bool canDial(AsyncCall &call) { return tunnel_.valid(); } + void dial(AsyncCall &call) { ((&(*tunnel_))->*method_)(answer_); } + virtual void print(std::ostream &os) const { + os << '(' << tunnel_.get() << ", " << answer_ << ')'; } + + /* Ssl::PeerConnector::CbDialer API */ + virtual Ssl::PeerConnectorAnswer &answer() { return answer_; } + + private: + Method method_; + CbcPointer tunnel_; + Ssl::PeerConnectorAnswer answer_; + }; + + void connectedToPeer(Ssl::PeerConnectorAnswer &answer); +#endif + CBDATA_CLASS2(TunnelStateData); bool keepGoingAfterRead(size_t len, comm_err_t 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_err_t errcode, int xerrno); void readClient(char *buf, size_t len, comm_err_t errcode, int xerrno); void writeClientDone(char *buf, size_t len, comm_err_t flag, int xerrno); void writeServerDone(char *buf, size_t len, comm_err_t flag, int xerrno); static void ReadConnectResponseDone(const Comm::ConnectionPointer &, char *buf, size_t len, comm_err_t errcode, int xerrno, void *data); void readConnectResponseDone(char *buf, size_t len, comm_err_t errcode, int xerrno); }; static const char *const conn_established = "HTTP/1.1 200 Connection established\r\n\r\n"; static CNCB tunnelConnectDone; static ERCB tunnelErrorComplete; static CLCB tunnelServerClosed; static CLCB tunnelClientClosed; static CTCB tunnelTimeout; @@ -810,41 +844,41 @@ if (conn->getPeer() && conn->getPeer()->options.no_delay) tunnelState->server.setDelayId(DelayId()); #endif tunnelState->request->hier.note(conn, tunnelState->getHost()); tunnelState->server.conn = conn; 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->flags.proxying = !(conn->getPeer()->options.originserver); } else { tunnelState->request->peer_login = NULL; tunnelState->request->flags.proxying = false; } if (tunnelState->request->flags.proxying) - tunnelRelayConnectRequest(conn, tunnelState); + tunnelState->connectToPeer(); else { tunnelConnected(conn, tunnelState); } AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout", CommTimeoutCbPtrFun(tunnelTimeout, tunnelState)); commSetConnTimeout(conn, Config.Timeout.read, timeoutCall); } tos_t GetTosToServer(HttpRequest * request); nfmark_t GetNfmarkToServer(HttpRequest * request); void tunnelStart(ClientHttpRequest * http, int64_t * size_ptr, int *status_ptr, const AccessLogEntryPointer &al) { debugs(26, 3, HERE); /* Create state structure. */ TunnelStateData *tunnelState = NULL; ErrorState *err = NULL; HttpRequest *request = http->request; @@ -885,40 +919,79 @@ tunnelState->request = request; tunnelState->server.size_ptr = size_ptr; tunnelState->status_ptr = status_ptr; tunnelState->client.conn = http->getConn()->clientConnection; tunnelState->al = al; comm_add_close_handler(tunnelState->client.conn->fd, tunnelClientClosed, tunnelState); AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout", CommTimeoutCbPtrFun(tunnelTimeout, tunnelState)); commSetConnTimeout(tunnelState->client.conn, Config.Timeout.lifetime, timeoutCall); peerSelect(&(tunnelState->serverDestinations), request, al, NULL, tunnelPeerSelectComplete, tunnelState); } +void +TunnelStateData::connectToPeer() { + const Comm::ConnectionPointer &srv = server.conn; + +#if USE_OPENSSL + if (CachePeer *p = srv->getPeer()) { + if (p->use_ssl) { + AsyncCall::Pointer callback = asyncCall(5,4, + "TunnelStateData::ConnectedToPeer", + MyAnswerDialer(&TunnelStateData::connectedToPeer, this)); + Ssl::PeerConnector *connector = + new Ssl::PeerConnector(request, srv, callback); + AsyncJob::Start(connector); // will call our callback + return; + } + } +#endif + + tunnelRelayConnectRequest(srv, this); +} + +#if USE_OPENSSL +/// Ssl::PeerConnector callback +void +TunnelStateData::connectedToPeer(Ssl::PeerConnectorAnswer &answer) +{ + if (ErrorState *error = answer.error.get()) { + *status_ptr = error->httpStatus; + error->callback = tunnelErrorComplete; + error->callback_data = this; + errorSend(client.conn, error); + answer.error.clear(); // preserve error for errorSendComplete() + return; + } + + tunnelRelayConnectRequest(server.conn, this); +} +#endif + static void tunnelRelayConnectRequest(const Comm::ConnectionPointer &srv, void *data) { TunnelStateData *tunnelState = (TunnelStateData *)data; assert(!tunnelState->waitingForConnectExchange()); HttpHeader hdr_out(hoRequest); Packer p; HttpStateFlags flags; debugs(26, 3, HERE << srv << ", tunnelState=" << tunnelState); memset(&flags, '\0', sizeof(flags)); flags.proxying = tunnelState->request->flags.proxying; MemBuf mb; mb.init(); mb.Printf("CONNECT %s HTTP/1.1\r\n", tunnelState->url); HttpStateData::httpBuildRequestHeader(tunnelState->request.getRaw(), NULL, /* StoreEntry */ tunnelState->al, /* AccessLogEntry */ &hdr_out, flags); /* flags */ packerToMemInit(&p, &mb);