]>
git.ipfire.org Git - thirdparty/squid.git/blob - src/adaptation/icap/Xaction.cc
2 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9 /* DEBUG: section 93 ICAP (RFC 3507) Client */
12 #include "acl/FilledChecklist.h"
13 #include "adaptation/icap/Config.h"
14 #include "adaptation/icap/Launcher.h"
15 #include "adaptation/icap/Xaction.h"
16 #include "base/AsyncCallbacks.h"
17 #include "base/IoManip.h"
18 #include "base/JobWait.h"
19 #include "base/TextException.h"
21 #include "comm/Connection.h"
22 #include "comm/ConnOpener.h"
23 #include "comm/Read.h"
24 #include "comm/Write.h"
25 #include "CommCalls.h"
26 #include "error/Detail.h"
30 #include "HttpReply.h"
34 #include "security/PeerConnector.h"
35 #include "SquidConfig.h"
41 /// A simple PeerConnector for Secure ICAP services. No SslBump capabilities.
42 class IcapPeerConnector
: public Security::PeerConnector
{
43 CBDATA_CHILD(IcapPeerConnector
);
46 Adaptation::Icap::ServiceRep::Pointer
&service
,
47 const Comm::ConnectionPointer
&aServerConn
,
48 const AsyncCallback
<Security::EncryptorAnswer
> &aCallback
,
49 AccessLogEntry::Pointer
const &alp
,
50 const time_t timeout
= 0):
51 AsyncJob("Ssl::IcapPeerConnector"),
52 Security::PeerConnector(aServerConn
, aCallback
, alp
, timeout
), icapService(service
) {}
54 /* Security::PeerConnector API */
55 bool initialize(Security::SessionPointer
&) override
;
56 void noteNegotiationDone(ErrorState
*error
) override
;
57 Security::ContextPointer
getTlsContext() override
{
58 return icapService
->sslContext
;
62 /* Acl::ChecklistFiller API */
63 void fillChecklist(ACLFilledChecklist
&) const override
;
65 Adaptation::Icap::ServiceRep::Pointer icapService
;
69 CBDATA_NAMESPACED_CLASS_INIT(Ssl
, IcapPeerConnector
);
71 Adaptation::Icap::Xaction::Xaction(const char *aTypeName
, Adaptation::Icap::ServiceRep::Pointer
&aService
):
73 Adaptation::Initiate(aTypeName
),
79 reuseConnection(true),
82 ignoreLastWrite(false),
84 alep(new AccessLogEntry
),
87 debugs(93,3, typeName
<< " constructed, this=" << this <<
88 " [icapx" << id
<< ']'); // we should not call virtual status() here
89 const auto mx
= MasterXaction::MakePortless
<XactionInitiator::initAdaptation
>();
90 icapRequest
= new HttpRequest(mx
);
91 HTTPMSGLOCK(icapRequest
);
92 icap_tr_start
= current_time
;
93 memset(&icap_tio_start
, 0, sizeof(icap_tio_start
));
94 memset(&icap_tio_finish
, 0, sizeof(icap_tio_finish
));
97 Adaptation::Icap::Xaction::~Xaction()
99 debugs(93,3, typeName
<< " destructed, this=" << this <<
100 " [icapx" << id
<< ']'); // we should not call virtual status() here
101 HTTPMSGUNLOCK(icapRequest
);
104 AccessLogEntry::Pointer
105 Adaptation::Icap::Xaction::masterLogEntry()
107 AccessLogEntry::Pointer nil
;
111 Adaptation::Icap::ServiceRep
&
112 Adaptation::Icap::Xaction::service()
114 Must(theService
!= nullptr);
118 void Adaptation::Icap::Xaction::disableRetries()
120 debugs(93,5, typeName
<< (isRetriable
? " from now on" : " still") <<
121 " cannot be retried " << status());
125 void Adaptation::Icap::Xaction::disableRepeats(const char *reason
)
127 debugs(93,5, typeName
<< (isRepeatable
? " from now on" : " still") <<
128 " cannot be repeated because " << reason
<< status());
129 isRepeatable
= false;
132 void Adaptation::Icap::Xaction::start()
134 Adaptation::Initiate::start();
137 // TODO: Make reusable by moving this (and the printing operator from
138 // ip/Address.h that this code is calling) into ip/print.h or similar.
141 inline std::ostream
&
142 operator <<(std::ostream
&os
, const std::optional
<Address
> &optional
)
144 if (optional
.has_value())
145 os
<< optional
.value();
154 icapLookupDnsResults(const ipcache_addrs
*ia
, const Dns::LookupDetails
&, void *data
)
156 Adaptation::Icap::Xaction
*xa
= static_cast<Adaptation::Icap::Xaction
*>(data
);
157 const auto &addr
= ia
? std::optional
<Ip::Address
>(ia
->current()) : std::optional
<Ip::Address
>();
158 CallJobHere1(93, 5, CbcPointer
<Adaptation::Icap::Xaction
>(xa
), Adaptation::Icap::Xaction
, dnsLookupDone
, addr
);
161 // TODO: obey service-specific, OPTIONS-reported connection limit
163 Adaptation::Icap::Xaction::openConnection()
165 Must(!haveConnection());
167 Adaptation::Icap::ServiceRep
&s
= service();
169 if (!TheConfig
.reuse_connections
)
170 disableRetries(); // this will also safely drain pconn pool
172 if (const auto pconn
= s
.getIdleConnection(isRetriable
)) {
173 useTransportConnection(pconn
);
177 disableRetries(); // we only retry pconn failures
179 // Attempt to open a new connection...
180 debugs(93,3, typeName
<< " opens connection to " << s
.cfg().host
.termedBuf() << ":" << s
.cfg().port
);
182 // Locate the Service IP(s) to open
183 assert(!waitingForDns
);
184 waitingForDns
= true; // before the possibly-synchronous ipcache_nbgethostbyname()
185 ipcache_nbgethostbyname(s
.cfg().host
.termedBuf(), icapLookupDnsResults
, this);
189 Adaptation::Icap::Xaction::dnsLookupDone(std::optional
<Ip::Address
> addr
)
191 assert(waitingForDns
);
192 waitingForDns
= false;
194 Adaptation::Icap::ServiceRep
&s
= service();
196 if (!addr
.has_value()) {
197 debugs(44, DBG_IMPORTANT
, "ERROR: ICAP: Unknown service host: " << s
.cfg().host
);
199 #if WHEN_IPCACHE_NBGETHOSTBYNAME_USES_ASYNC_CALLS
200 dieOnConnectionFailure(); // throws
201 #else // take a step back into protected Async call dialing.
202 CallJobHere(93, 3, this, Xaction
, Xaction::dieOnConnectionFailure
);
207 const Comm::ConnectionPointer conn
= new Comm::Connection();
208 conn
->remote
= addr
.value();
209 conn
->remote
.port(s
.cfg().port
);
210 getOutgoingAddress(nullptr, conn
);
212 // TODO: service bypass status may differ from that of a transaction
213 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommConnectCbParams
> ConnectDialer
;
214 AsyncCall::Pointer callback
= JobCallback(93, 3, ConnectDialer
, this, Adaptation::Icap::Xaction::noteCommConnected
);
215 const auto cs
= new Comm::ConnOpener(conn
, callback
, TheConfig
.connect_timeout(service().cfg().bypass
));
216 cs
->setHost(s
.cfg().host
.termedBuf());
217 transportWait
.start(cs
, callback
);
220 void Adaptation::Icap::Xaction::closeConnection()
222 if (haveConnection()) {
224 if (closer
!= nullptr) {
225 comm_remove_close_handler(connection
->fd
, closer
);
229 commUnsetConnTimeout(connection
);
231 cancelRead(); // may not work
233 if (reuseConnection
&& !doneWithIo()) {
234 //status() adds leading spaces.
235 debugs(93,5, "not reusing pconn due to pending I/O" << status());
236 reuseConnection
= false;
242 const bool reset
= !reuseConnection
&&
243 (al
.icap
.outcome
== xoGone
|| al
.icap
.outcome
== xoError
);
245 Adaptation::Icap::ServiceRep
&s
= service();
246 s
.putConnection(connection
, reuseConnection
, reset
, status());
250 connection
= nullptr;
254 /// called when the connection attempt to an ICAP service completes (successfully or not)
255 void Adaptation::Icap::Xaction::noteCommConnected(const CommConnectCbParams
&io
)
257 transportWait
.finish();
259 if (io
.flag
!= Comm::OK
) {
260 dieOnConnectionFailure(); // throws
264 useTransportConnection(io
.conn
);
267 /// React to the availability of a transport connection to the ICAP service.
268 /// The given connection may (or may not) be secured already.
270 Adaptation::Icap::Xaction::useTransportConnection(const Comm::ConnectionPointer
&conn
)
272 assert(Comm::IsConnOpen(conn
));
275 // If it is a reused connection and the TLS object is built
276 // we should not negotiate new TLS session
277 const auto &ssl
= fd_table
[conn
->fd
].ssl
;
278 if (!ssl
&& service().cfg().secure
.encryptTransport
) {
279 // XXX: Exceptions orphan conn.
280 const auto callback
= asyncCallback(93, 4, Adaptation::Icap::Xaction::handleSecuredPeer
, this);
281 const auto sslConnector
= new Ssl::IcapPeerConnector(theService
, conn
, callback
, masterLogEntry(), TheConfig
.connect_timeout(service().cfg().bypass
));
283 encryptionWait
.start(sslConnector
, callback
);
287 useIcapConnection(conn
);
290 /// react to the availability of a fully-ready ICAP connection
292 Adaptation::Icap::Xaction::useIcapConnection(const Comm::ConnectionPointer
&conn
)
296 assert(Comm::IsConnOpen(conn
));
298 service().noteConnectionUse(connection
);
300 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommCloseCbParams
> CloseDialer
;
301 closer
= asyncCall(93, 5, "Adaptation::Icap::Xaction::noteCommClosed",
302 CloseDialer(this,&Adaptation::Icap::Xaction::noteCommClosed
));
303 comm_add_close_handler(connection
->fd
, closer
);
308 void Adaptation::Icap::Xaction::dieOnConnectionFailure()
310 debugs(93, 2, typeName
<<
311 " failed to connect to " << service().cfg().uri
);
312 service().noteConnectionFailed("failure");
313 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_START");
315 throw TexcHere("cannot connect to the ICAP service");
318 void Adaptation::Icap::Xaction::scheduleWrite(MemBuf
&buf
)
320 Must(haveConnection());
322 // comm module will free the buffer
323 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommIoCbParams
> Dialer
;
324 writer
= JobCallback(93, 3,
325 Dialer
, this, Adaptation::Icap::Xaction::noteCommWrote
);
327 Comm::Write(connection
, &buf
, writer
);
331 void Adaptation::Icap::Xaction::noteCommWrote(const CommIoCbParams
&io
)
333 Must(writer
!= nullptr);
336 if (ignoreLastWrite
) {
337 // a hack due to comm inability to cancel a pending write
338 ignoreLastWrite
= false;
339 debugs(93, 7, "ignoring last write; status: " << io
.flag
);
341 Must(io
.flag
== Comm::OK
);
342 al
.icap
.bytesSent
+= io
.size
;
344 handleCommWrote(io
.size
);
348 // communication timeout with the ICAP service
349 void Adaptation::Icap::Xaction::noteCommTimedout(const CommTimeoutCbParams
&)
351 debugs(93, 2, typeName
<< " failed: timeout with " <<
352 theService
->cfg().methodStr() << " " <<
353 theService
->cfg().uri
<< status());
354 reuseConnection
= false;
355 assert(haveConnection());
357 throw TextException("timed out while talking to the ICAP service", Here());
360 // unexpected connection close while talking to the ICAP service
361 void Adaptation::Icap::Xaction::noteCommClosed(const CommCloseCbParams
&)
364 connection
->noteClosure();
365 connection
= nullptr;
369 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_CLOSE");
371 mustStop("ICAP service connection externally closed");
374 void Adaptation::Icap::Xaction::callException(const std::exception
&e
)
377 service().noteFailure();
378 Adaptation::Initiate::callException(e
);
381 void Adaptation::Icap::Xaction::callEnd()
384 debugs(93, 5, typeName
<< " done with I/O" << status());
387 Adaptation::Initiate::callEnd(); // may destroy us
390 bool Adaptation::Icap::Xaction::doneAll() const
392 return !waitingForDns
&& !transportWait
&& !encryptionWait
&&
393 !reader
&& !writer
&&
394 Adaptation::Initiate::doneAll();
397 void Adaptation::Icap::Xaction::updateTimeout()
399 Must(haveConnection());
401 if (reader
!= nullptr || writer
!= nullptr) {
402 // restart the timeout before each I/O
403 // XXX: why does Config.Timeout lacks a write timeout?
404 // TODO: service bypass status may differ from that of a transaction
405 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommTimeoutCbParams
> TimeoutDialer
;
406 AsyncCall::Pointer call
= JobCallback(93, 5, TimeoutDialer
, this, Adaptation::Icap::Xaction::noteCommTimedout
);
407 commSetConnTimeout(connection
, TheConfig
.io_timeout(service().cfg().bypass
), call
);
409 // clear timeout when there is no I/O
410 // Do we need a lifetime timeout?
411 commUnsetConnTimeout(connection
);
415 void Adaptation::Icap::Xaction::scheduleRead()
417 Must(haveConnection());
419 Must(readBuf
.length() < SQUID_TCP_SO_RCVBUF
); // will expand later if needed
421 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommIoCbParams
> Dialer
;
422 reader
= JobCallback(93, 3, Dialer
, this, Adaptation::Icap::Xaction::noteCommRead
);
423 Comm::Read(connection
, reader
);
427 // comm module read a portion of the ICAP response for us
428 void Adaptation::Icap::Xaction::noteCommRead(const CommIoCbParams
&io
)
430 Must(reader
!= nullptr);
433 Must(io
.flag
== Comm::OK
);
435 // TODO: tune this better to expected message sizes
436 readBuf
.reserveCapacity(SQUID_TCP_SO_RCVBUF
);
437 // we are not asked to grow beyond the allowed maximum
438 Must(readBuf
.length() < SQUID_TCP_SO_RCVBUF
);
439 // now we can ensure that there is space to read new data,
440 // even if readBuf.spaceSize() currently returns zero.
441 readBuf
.rawAppendStart(1);
443 CommIoCbParams
rd(this); // will be expanded with ReadNow results
446 switch (Comm::ReadNow(rd
, readBuf
)) {
447 case Comm::INPROGRESS
:
448 if (readBuf
.isEmpty())
449 debugs(33, 2, io
.conn
<< ": no data to process, " << xstrerr(rd
.xerrno
));
454 al
.icap
.bytesRead
+= rd
.size
;
458 debugs(93, 3, "read " << rd
.size
<< " bytes");
460 disableRetries(); // because pconn did not fail
462 /* Continue to process previously read data */
465 case Comm::ENDFILE
: // close detected by 0-byte read
467 reuseConnection
= false;
469 // detect a pconn race condition: eof on the first pconn read
470 if (!al
.icap
.bytesRead
&& retriable()) {
472 mustStop("pconn race");
478 // case Comm::COMM_ERROR:
479 default: // no other flags should ever occur
480 debugs(11, 2, io
.conn
<< ": read failure: " << xstrerr(rd
.xerrno
));
481 mustStop("unknown ICAP I/O read error");
485 handleCommRead(io
.size
);
488 void Adaptation::Icap::Xaction::cancelRead()
490 if (reader
!= nullptr) {
491 Must(haveConnection());
492 Comm::ReadCancel(connection
->fd
, reader
);
498 Adaptation::Icap::Xaction::parseHttpMsg(Http::Message
*msg
)
500 debugs(93, 5, "have " << readBuf
.length() << " head bytes to parse");
502 Http::StatusCode error
= Http::scNone
;
503 // XXX: performance regression c_str() data copies
504 const char *buf
= readBuf
.c_str();
505 const bool parsed
= msg
->parse(buf
, readBuf
.length(), commEof
, &error
);
506 Must(parsed
|| !error
); // success or need more data
508 if (!parsed
) { // need more data
514 readBuf
.consume(msg
->hdr_sz
);
518 bool Adaptation::Icap::Xaction::mayReadMore() const
520 return !doneReading() && // will read more data
521 readBuf
.length() < SQUID_TCP_SO_RCVBUF
; // have space for more data
524 bool Adaptation::Icap::Xaction::doneReading() const
529 bool Adaptation::Icap::Xaction::doneWriting() const
534 bool Adaptation::Icap::Xaction::doneWithIo() const
536 return haveConnection() &&
537 !transportWait
&& !reader
&& !writer
&& // fast checks, some redundant
538 doneReading() && doneWriting();
541 bool Adaptation::Icap::Xaction::haveConnection() const
543 return connection
!= nullptr && connection
->isOpen();
547 void Adaptation::Icap::Xaction::noteInitiatorAborted()
550 if (theInitiator
.set()) {
551 debugs(93,4, "Initiator gone before ICAP transaction ended");
553 static const auto d
= MakeNamedErrorDetail("ICAP_INIT_GONE");
556 mustStop("initiator aborted");
561 void Adaptation::Icap::Xaction::setOutcome(const Adaptation::Icap::XactOutcome
&xo
)
563 if (al
.icap
.outcome
!= xoUnknown
) {
564 debugs(93, 3, "WARNING: resetting outcome: from " << al
.icap
.outcome
<< " to " << xo
);
568 al
.icap
.outcome
= xo
;
571 // This 'last chance' method is called before a 'done' transaction is deleted.
572 // It is wrong to call virtual methods from a destructor. Besides, this call
573 // indicates that the transaction will terminate as planned.
574 void Adaptation::Icap::Xaction::swanSong()
576 // kids should sing first and then call the parent method.
577 if (transportWait
|| encryptionWait
) {
578 service().noteConnectionFailed("abort");
581 closeConnection(); // TODO: rename because we do not always close
589 Adaptation::Initiate::swanSong();
592 void Adaptation::Icap::Xaction::tellQueryAborted()
594 if (theInitiator
.set()) {
595 Adaptation::Icap::XactAbortInfo
abortInfo(icapRequest
, icapReply
.getRaw(),
596 retriable(), repeatable());
597 Launcher
*launcher
= dynamic_cast<Launcher
*>(theInitiator
.get());
598 // launcher may be nil if initiator is invalid
599 CallJobHere1(91,5, CbcPointer
<Launcher
>(launcher
),
600 Launcher
, noteXactAbort
, abortInfo
);
605 void Adaptation::Icap::Xaction::maybeLog()
607 if (IcapLogfileStatus
== LOG_ENABLE
) {
613 void Adaptation::Icap::Xaction::finalizeLogInfo()
616 al
.icp
.opcode
= ICP_INVALID
;
618 const Adaptation::Icap::ServiceRep
&s
= service();
619 al
.icap
.hostAddr
= s
.cfg().host
.termedBuf();
620 al
.icap
.serviceName
= s
.cfg().key
;
621 al
.icap
.reqUri
= s
.cfg().uri
;
623 tvSub(al
.icap
.ioTime
, icap_tio_start
, icap_tio_finish
);
624 tvSub(al
.icap
.trTime
, icap_tr_start
, current_time
);
626 al
.icap
.request
= icapRequest
;
627 HTTPMSGLOCK(al
.icap
.request
);
628 if (icapReply
!= nullptr) {
629 al
.icap
.reply
= icapReply
.getRaw();
630 HTTPMSGLOCK(al
.icap
.reply
);
631 al
.icap
.resStatus
= icapReply
->sline
.status();
635 // returns a temporary string depicting transaction status, for debugging
636 const char *Adaptation::Icap::Xaction::status() const
641 fillPendingStatus(buf
);
644 buf
.appendf(" %s%u]", id
.prefix(), id
.value
);
647 return buf
.content();
650 void Adaptation::Icap::Xaction::fillPendingStatus(MemBuf
&buf
) const
652 if (haveConnection()) {
653 buf
.appendf("FD %d", connection
->fd
);
655 if (writer
!= nullptr)
658 if (reader
!= nullptr)
668 void Adaptation::Icap::Xaction::fillDoneStatus(MemBuf
&buf
) const
670 if (haveConnection() && commEof
)
671 buf
.appendf("Comm(%d)", connection
->fd
);
673 if (stopReason
!= nullptr)
674 buf
.append("Stopped", 7);
677 bool Adaptation::Icap::Xaction::fillVirginHttpHeader(MemBuf
&) const
683 Ssl::IcapPeerConnector::initialize(Security::SessionPointer
&serverSession
)
685 if (!Security::PeerConnector::initialize(serverSession
))
688 assert(!icapService
->cfg().secure
.sslDomain
.isEmpty());
690 SBuf
*host
= new SBuf(icapService
->cfg().secure
.sslDomain
);
691 SSL_set_ex_data(serverSession
.get(), ssl_ex_index_server
, host
);
692 setClientSNI(serverSession
.get(), host
->c_str());
695 Security::SetSessionResumeData(serverSession
, icapService
->sslSession
);
700 Ssl::IcapPeerConnector::fillChecklist(ACLFilledChecklist
&checklist
) const
702 Security::PeerConnector::fillChecklist(checklist
);
703 if (checklist
.dst_peer_name
.isEmpty())
704 checklist
.dst_peer_name
= icapService
->cfg().secure
.sslDomain
;
708 Ssl::IcapPeerConnector::noteNegotiationDone(ErrorState
*error
)
713 const int fd
= serverConnection()->fd
;
714 Security::MaybeGetSessionResumeData(fd_table
[fd
].ssl
, icapService
->sslSession
);
718 Adaptation::Icap::Xaction::handleSecuredPeer(Security::EncryptorAnswer
&answer
)
720 encryptionWait
.finish();
722 assert(!answer
.tunneled
);
723 if (answer
.error
.get()) {
724 assert(!answer
.conn
);
725 // TODO: Refactor dieOnConnectionFailure() to be usable here as well.
726 debugs(93, 2, typeName
<<
727 " TLS negotiation to " << service().cfg().uri
<< " failed");
728 service().noteConnectionFailed("failure");
729 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_SSL_START");
731 throw TexcHere("cannot connect to the TLS ICAP service");
734 debugs(93, 5, "TLS negotiation to " << service().cfg().uri
<< " complete");
738 // The socket could get closed while our callback was queued. Sync
739 // Connection. XXX: Connection::fd may already be stale/invalid here.
740 if (answer
.conn
->isOpen() && fd_table
[answer
.conn
->fd
].closing()) {
741 answer
.conn
->noteClosure();
742 service().noteConnectionFailed("external TLS connection closure");
743 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_SSL_CLOSE");
745 throw TexcHere("external closure of the TLS ICAP service connection");
748 useIcapConnection(answer
.conn
);