]>
git.ipfire.org Git - thirdparty/squid.git/blob - src/adaptation/icap/Xaction.cc
2 * Copyright (C) 1996-2022 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/JobWait.h"
17 #include "base/TextException.h"
19 #include "comm/Connection.h"
20 #include "comm/ConnOpener.h"
21 #include "comm/Read.h"
22 #include "comm/Write.h"
23 #include "CommCalls.h"
24 #include "error/Detail.h"
28 #include "HttpReply.h"
32 #include "security/PeerConnector.h"
33 #include "SquidConfig.h"
34 #include "SquidTime.h"
36 /// Gives Security::PeerConnector access to Answer in the PeerPoolMgr callback dialer.
37 class MyIcapAnswerDialer
: public UnaryMemFunT
<Adaptation::Icap::Xaction
, Security::EncryptorAnswer
, Security::EncryptorAnswer
&>,
38 public Security::PeerConnector::CbDialer
41 MyIcapAnswerDialer(const JobPointer
&aJob
, Method aMethod
):
42 UnaryMemFunT
<Adaptation::Icap::Xaction
, Security::EncryptorAnswer
, Security::EncryptorAnswer
&>(aJob
, aMethod
, Security::EncryptorAnswer()) {}
44 /* Security::PeerConnector::CbDialer API */
45 virtual Security::EncryptorAnswer
&answer() { return arg1
; }
50 /// A simple PeerConnector for Secure ICAP services. No SslBump capabilities.
51 class IcapPeerConnector
: public Security::PeerConnector
{
52 CBDATA_CLASS(IcapPeerConnector
);
55 Adaptation::Icap::ServiceRep::Pointer
&service
,
56 const Comm::ConnectionPointer
&aServerConn
,
57 AsyncCall::Pointer
&aCallback
,
58 AccessLogEntry::Pointer
const &alp
,
59 const time_t timeout
= 0):
60 AsyncJob("Ssl::IcapPeerConnector"),
61 Security::PeerConnector(aServerConn
, aCallback
, alp
, timeout
), icapService(service
) {}
63 /* Security::PeerConnector API */
64 virtual bool initialize(Security::SessionPointer
&);
65 virtual void noteNegotiationDone(ErrorState
*error
);
66 virtual Security::ContextPointer
getTlsContext() {
67 return icapService
->sslContext
;
71 /* Acl::ChecklistFiller API */
72 virtual void fillChecklist(ACLFilledChecklist
&) const;
74 Adaptation::Icap::ServiceRep::Pointer icapService
;
78 CBDATA_NAMESPACED_CLASS_INIT(Ssl
, IcapPeerConnector
);
80 Adaptation::Icap::Xaction::Xaction(const char *aTypeName
, Adaptation::Icap::ServiceRep::Pointer
&aService
):
82 Adaptation::Initiate(aTypeName
),
88 reuseConnection(true),
91 ignoreLastWrite(false),
93 alep(new AccessLogEntry
),
96 debugs(93,3, typeName
<< " constructed, this=" << this <<
97 " [icapx" << id
<< ']'); // we should not call virtual status() here
98 const MasterXaction::Pointer mx
= new MasterXaction(XactionInitiator::initAdaptation
);
99 icapRequest
= new HttpRequest(mx
);
100 HTTPMSGLOCK(icapRequest
);
101 icap_tr_start
= current_time
;
102 memset(&icap_tio_start
, 0, sizeof(icap_tio_start
));
103 memset(&icap_tio_finish
, 0, sizeof(icap_tio_finish
));
106 Adaptation::Icap::Xaction::~Xaction()
108 debugs(93,3, typeName
<< " destructed, this=" << this <<
109 " [icapx" << id
<< ']'); // we should not call virtual status() here
110 HTTPMSGUNLOCK(icapRequest
);
113 AccessLogEntry::Pointer
114 Adaptation::Icap::Xaction::masterLogEntry()
116 AccessLogEntry::Pointer nil
;
120 Adaptation::Icap::ServiceRep
&
121 Adaptation::Icap::Xaction::service()
123 Must(theService
!= NULL
);
127 void Adaptation::Icap::Xaction::disableRetries()
129 debugs(93,5, typeName
<< (isRetriable
? " from now on" : " still") <<
130 " cannot be retried " << status());
134 void Adaptation::Icap::Xaction::disableRepeats(const char *reason
)
136 debugs(93,5, typeName
<< (isRepeatable
? " from now on" : " still") <<
137 " cannot be repeated because " << reason
<< status());
138 isRepeatable
= false;
141 void Adaptation::Icap::Xaction::start()
143 Adaptation::Initiate::start();
147 icapLookupDnsResults(const ipcache_addrs
*ia
, const Dns::LookupDetails
&, void *data
)
149 Adaptation::Icap::Xaction
*xa
= static_cast<Adaptation::Icap::Xaction
*>(data
);
150 /// TODO: refactor with CallJobHere1, passing either std::optional (after upgrading to C++17)
151 /// or Optional<Ip::Address> (when it can take non-trivial types)
152 xa
->dnsLookupDone(ia
);
155 // TODO: obey service-specific, OPTIONS-reported connection limit
157 Adaptation::Icap::Xaction::openConnection()
159 Must(!haveConnection());
161 Adaptation::Icap::ServiceRep
&s
= service();
163 if (!TheConfig
.reuse_connections
)
164 disableRetries(); // this will also safely drain pconn pool
166 if (const auto pconn
= s
.getIdleConnection(isRetriable
)) {
167 useTransportConnection(pconn
);
171 disableRetries(); // we only retry pconn failures
173 // Attempt to open a new connection...
174 debugs(93,3, typeName
<< " opens connection to " << s
.cfg().host
.termedBuf() << ":" << s
.cfg().port
);
176 // Locate the Service IP(s) to open
177 assert(!waitingForDns
);
178 waitingForDns
= true; // before the possibly-synchronous ipcache_nbgethostbyname()
179 ipcache_nbgethostbyname(s
.cfg().host
.termedBuf(), icapLookupDnsResults
, this);
183 Adaptation::Icap::Xaction::dnsLookupDone(const ipcache_addrs
*ia
)
185 assert(waitingForDns
);
186 waitingForDns
= false;
188 Adaptation::Icap::ServiceRep
&s
= service();
191 debugs(44, DBG_IMPORTANT
, "ERROR: ICAP: Unknown service host: " << s
.cfg().host
);
193 #if WHEN_IPCACHE_NBGETHOSTBYNAME_USES_ASYNC_CALLS
194 dieOnConnectionFailure(); // throws
195 #else // take a step back into protected Async call dialing.
196 CallJobHere(93, 3, this, Xaction
, Xaction::dieOnConnectionFailure
);
201 const Comm::ConnectionPointer conn
= new Comm::Connection();
202 conn
->remote
= ia
->current();
203 conn
->remote
.port(s
.cfg().port
);
204 getOutgoingAddress(nullptr, conn
);
206 // TODO: service bypass status may differ from that of a transaction
207 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommConnectCbParams
> ConnectDialer
;
208 AsyncCall::Pointer callback
= JobCallback(93, 3, ConnectDialer
, this, Adaptation::Icap::Xaction::noteCommConnected
);
209 const auto cs
= new Comm::ConnOpener(conn
, callback
, TheConfig
.connect_timeout(service().cfg().bypass
));
210 cs
->setHost(s
.cfg().host
.termedBuf());
211 transportWait
.start(cs
, callback
);
214 void Adaptation::Icap::Xaction::closeConnection()
216 if (haveConnection()) {
218 if (closer
!= NULL
) {
219 comm_remove_close_handler(connection
->fd
, closer
);
223 commUnsetConnTimeout(connection
);
225 cancelRead(); // may not work
227 if (reuseConnection
&& !doneWithIo()) {
228 //status() adds leading spaces.
229 debugs(93,5, "not reusing pconn due to pending I/O" << status());
230 reuseConnection
= false;
236 const bool reset
= !reuseConnection
&&
237 (al
.icap
.outcome
== xoGone
|| al
.icap
.outcome
== xoError
);
239 Adaptation::Icap::ServiceRep
&s
= service();
240 s
.putConnection(connection
, reuseConnection
, reset
, status());
248 /// called when the connection attempt to an ICAP service completes (successfully or not)
249 void Adaptation::Icap::Xaction::noteCommConnected(const CommConnectCbParams
&io
)
251 transportWait
.finish();
253 if (io
.flag
!= Comm::OK
) {
254 dieOnConnectionFailure(); // throws
258 useTransportConnection(io
.conn
);
261 /// React to the availability of a transport connection to the ICAP service.
262 /// The given connection may (or may not) be secured already.
264 Adaptation::Icap::Xaction::useTransportConnection(const Comm::ConnectionPointer
&conn
)
266 assert(Comm::IsConnOpen(conn
));
269 // If it is a reused connection and the TLS object is built
270 // we should not negotiate new TLS session
271 const auto &ssl
= fd_table
[conn
->fd
].ssl
;
272 if (!ssl
&& service().cfg().secure
.encryptTransport
) {
273 // XXX: Exceptions orphan conn.
274 CbcPointer
<Adaptation::Icap::Xaction
> me(this);
275 AsyncCall::Pointer callback
= asyncCall(93, 4, "Adaptation::Icap::Xaction::handleSecuredPeer",
276 MyIcapAnswerDialer(me
, &Adaptation::Icap::Xaction::handleSecuredPeer
));
278 const auto sslConnector
= new Ssl::IcapPeerConnector(theService
, conn
, callback
, masterLogEntry(), TheConfig
.connect_timeout(service().cfg().bypass
));
280 encryptionWait
.start(sslConnector
, callback
);
284 useIcapConnection(conn
);
287 /// react to the availability of a fully-ready ICAP connection
289 Adaptation::Icap::Xaction::useIcapConnection(const Comm::ConnectionPointer
&conn
)
293 assert(Comm::IsConnOpen(conn
));
295 service().noteConnectionUse(connection
);
297 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommCloseCbParams
> CloseDialer
;
298 closer
= asyncCall(93, 5, "Adaptation::Icap::Xaction::noteCommClosed",
299 CloseDialer(this,&Adaptation::Icap::Xaction::noteCommClosed
));
300 comm_add_close_handler(connection
->fd
, closer
);
305 void Adaptation::Icap::Xaction::dieOnConnectionFailure()
307 debugs(93, 2, typeName
<<
308 " failed to connect to " << service().cfg().uri
);
309 service().noteConnectionFailed("failure");
310 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_START");
312 throw TexcHere("cannot connect to the ICAP service");
315 void Adaptation::Icap::Xaction::scheduleWrite(MemBuf
&buf
)
317 Must(haveConnection());
319 // comm module will free the buffer
320 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommIoCbParams
> Dialer
;
321 writer
= JobCallback(93, 3,
322 Dialer
, this, Adaptation::Icap::Xaction::noteCommWrote
);
324 Comm::Write(connection
, &buf
, writer
);
328 void Adaptation::Icap::Xaction::noteCommWrote(const CommIoCbParams
&io
)
330 Must(writer
!= NULL
);
333 if (ignoreLastWrite
) {
334 // a hack due to comm inability to cancel a pending write
335 ignoreLastWrite
= false;
336 debugs(93, 7, "ignoring last write; status: " << io
.flag
);
338 Must(io
.flag
== Comm::OK
);
339 al
.icap
.bytesSent
+= io
.size
;
341 handleCommWrote(io
.size
);
345 // communication timeout with the ICAP service
346 void Adaptation::Icap::Xaction::noteCommTimedout(const CommTimeoutCbParams
&)
348 debugs(93, 2, typeName
<< " failed: timeout with " <<
349 theService
->cfg().methodStr() << " " <<
350 theService
->cfg().uri
<< status());
351 reuseConnection
= false;
352 assert(haveConnection());
354 throw TextException("timed out while talking to the ICAP service", Here());
357 // unexpected connection close while talking to the ICAP service
358 void Adaptation::Icap::Xaction::noteCommClosed(const CommCloseCbParams
&)
361 connection
->noteClosure();
362 connection
= nullptr;
366 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_CLOSE");
368 mustStop("ICAP service connection externally closed");
371 void Adaptation::Icap::Xaction::callException(const std::exception
&e
)
374 service().noteFailure();
375 Adaptation::Initiate::callException(e
);
378 void Adaptation::Icap::Xaction::callEnd()
381 debugs(93, 5, typeName
<< " done with I/O" << status());
384 Adaptation::Initiate::callEnd(); // may destroy us
387 bool Adaptation::Icap::Xaction::doneAll() const
389 return !waitingForDns
&& !transportWait
&& !encryptionWait
&&
390 !reader
&& !writer
&&
391 Adaptation::Initiate::doneAll();
394 void Adaptation::Icap::Xaction::updateTimeout()
396 Must(haveConnection());
398 if (reader
!= NULL
|| writer
!= NULL
) {
399 // restart the timeout before each I/O
400 // XXX: why does Config.Timeout lacks a write timeout?
401 // TODO: service bypass status may differ from that of a transaction
402 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommTimeoutCbParams
> TimeoutDialer
;
403 AsyncCall::Pointer call
= JobCallback(93, 5, TimeoutDialer
, this, Adaptation::Icap::Xaction::noteCommTimedout
);
404 commSetConnTimeout(connection
, TheConfig
.io_timeout(service().cfg().bypass
), call
);
406 // clear timeout when there is no I/O
407 // Do we need a lifetime timeout?
408 commUnsetConnTimeout(connection
);
412 void Adaptation::Icap::Xaction::scheduleRead()
414 Must(haveConnection());
416 Must(readBuf
.length() < SQUID_TCP_SO_RCVBUF
); // will expand later if needed
418 typedef CommCbMemFunT
<Adaptation::Icap::Xaction
, CommIoCbParams
> Dialer
;
419 reader
= JobCallback(93, 3, Dialer
, this, Adaptation::Icap::Xaction::noteCommRead
);
420 Comm::Read(connection
, reader
);
424 // comm module read a portion of the ICAP response for us
425 void Adaptation::Icap::Xaction::noteCommRead(const CommIoCbParams
&io
)
427 Must(reader
!= NULL
);
430 Must(io
.flag
== Comm::OK
);
432 // TODO: tune this better to expected message sizes
433 readBuf
.reserveCapacity(SQUID_TCP_SO_RCVBUF
);
434 // we are not asked to grow beyond the allowed maximum
435 Must(readBuf
.length() < SQUID_TCP_SO_RCVBUF
);
436 // now we can ensure that there is space to read new data,
437 // even if readBuf.spaceSize() currently returns zero.
438 readBuf
.rawAppendStart(1);
440 CommIoCbParams
rd(this); // will be expanded with ReadNow results
443 switch (Comm::ReadNow(rd
, readBuf
)) {
444 case Comm::INPROGRESS
:
445 if (readBuf
.isEmpty())
446 debugs(33, 2, io
.conn
<< ": no data to process, " << xstrerr(rd
.xerrno
));
451 al
.icap
.bytesRead
+= rd
.size
;
455 debugs(93, 3, "read " << rd
.size
<< " bytes");
457 disableRetries(); // because pconn did not fail
459 /* Continue to process previously read data */
462 case Comm::ENDFILE
: // close detected by 0-byte read
464 reuseConnection
= false;
466 // detect a pconn race condition: eof on the first pconn read
467 if (!al
.icap
.bytesRead
&& retriable()) {
469 mustStop("pconn race");
475 // case Comm::COMM_ERROR:
476 default: // no other flags should ever occur
477 debugs(11, 2, io
.conn
<< ": read failure: " << xstrerr(rd
.xerrno
));
478 mustStop("unknown ICAP I/O read error");
482 handleCommRead(io
.size
);
485 void Adaptation::Icap::Xaction::cancelRead()
487 if (reader
!= NULL
) {
488 Must(haveConnection());
489 Comm::ReadCancel(connection
->fd
, reader
);
495 Adaptation::Icap::Xaction::parseHttpMsg(Http::Message
*msg
)
497 debugs(93, 5, "have " << readBuf
.length() << " head bytes to parse");
499 Http::StatusCode error
= Http::scNone
;
500 // XXX: performance regression c_str() data copies
501 const char *buf
= readBuf
.c_str();
502 const bool parsed
= msg
->parse(buf
, readBuf
.length(), commEof
, &error
);
503 Must(parsed
|| !error
); // success or need more data
505 if (!parsed
) { // need more data
511 readBuf
.consume(msg
->hdr_sz
);
515 bool Adaptation::Icap::Xaction::mayReadMore() const
517 return !doneReading() && // will read more data
518 readBuf
.length() < SQUID_TCP_SO_RCVBUF
; // have space for more data
521 bool Adaptation::Icap::Xaction::doneReading() const
526 bool Adaptation::Icap::Xaction::doneWriting() const
531 bool Adaptation::Icap::Xaction::doneWithIo() const
533 return haveConnection() &&
534 !transportWait
&& !reader
&& !writer
&& // fast checks, some redundant
535 doneReading() && doneWriting();
538 bool Adaptation::Icap::Xaction::haveConnection() const
540 return connection
!= NULL
&& connection
->isOpen();
544 void Adaptation::Icap::Xaction::noteInitiatorAborted()
547 if (theInitiator
.set()) {
548 debugs(93,4, "Initiator gone before ICAP transaction ended");
550 static const auto d
= MakeNamedErrorDetail("ICAP_INIT_GONE");
553 mustStop("initiator aborted");
558 void Adaptation::Icap::Xaction::setOutcome(const Adaptation::Icap::XactOutcome
&xo
)
560 if (al
.icap
.outcome
!= xoUnknown
) {
561 debugs(93, 3, "WARNING: resetting outcome: from " << al
.icap
.outcome
<< " to " << xo
);
565 al
.icap
.outcome
= xo
;
568 // This 'last chance' method is called before a 'done' transaction is deleted.
569 // It is wrong to call virtual methods from a destructor. Besides, this call
570 // indicates that the transaction will terminate as planned.
571 void Adaptation::Icap::Xaction::swanSong()
573 // kids should sing first and then call the parent method.
574 if (transportWait
|| encryptionWait
) {
575 service().noteConnectionFailed("abort");
578 closeConnection(); // TODO: rename because we do not always close
586 Adaptation::Initiate::swanSong();
589 void Adaptation::Icap::Xaction::tellQueryAborted()
591 if (theInitiator
.set()) {
592 Adaptation::Icap::XactAbortInfo
abortInfo(icapRequest
, icapReply
.getRaw(),
593 retriable(), repeatable());
594 Launcher
*launcher
= dynamic_cast<Launcher
*>(theInitiator
.get());
595 // launcher may be nil if initiator is invalid
596 CallJobHere1(91,5, CbcPointer
<Launcher
>(launcher
),
597 Launcher
, noteXactAbort
, abortInfo
);
602 void Adaptation::Icap::Xaction::maybeLog()
604 if (IcapLogfileStatus
== LOG_ENABLE
) {
610 void Adaptation::Icap::Xaction::finalizeLogInfo()
613 al
.icp
.opcode
= ICP_INVALID
;
615 const Adaptation::Icap::ServiceRep
&s
= service();
616 al
.icap
.hostAddr
= s
.cfg().host
.termedBuf();
617 al
.icap
.serviceName
= s
.cfg().key
;
618 al
.icap
.reqUri
= s
.cfg().uri
;
620 tvSub(al
.icap
.ioTime
, icap_tio_start
, icap_tio_finish
);
621 tvSub(al
.icap
.trTime
, icap_tr_start
, current_time
);
623 al
.icap
.request
= icapRequest
;
624 HTTPMSGLOCK(al
.icap
.request
);
625 if (icapReply
!= NULL
) {
626 al
.icap
.reply
= icapReply
.getRaw();
627 HTTPMSGLOCK(al
.icap
.reply
);
628 al
.icap
.resStatus
= icapReply
->sline
.status();
632 // returns a temporary string depicting transaction status, for debugging
633 const char *Adaptation::Icap::Xaction::status() const
638 fillPendingStatus(buf
);
641 buf
.appendf(" %s%u]", id
.prefix(), id
.value
);
644 return buf
.content();
647 void Adaptation::Icap::Xaction::fillPendingStatus(MemBuf
&buf
) const
649 if (haveConnection()) {
650 buf
.appendf("FD %d", connection
->fd
);
665 void Adaptation::Icap::Xaction::fillDoneStatus(MemBuf
&buf
) const
667 if (haveConnection() && commEof
)
668 buf
.appendf("Comm(%d)", connection
->fd
);
670 if (stopReason
!= NULL
)
671 buf
.append("Stopped", 7);
674 bool Adaptation::Icap::Xaction::fillVirginHttpHeader(MemBuf
&) const
680 Ssl::IcapPeerConnector::initialize(Security::SessionPointer
&serverSession
)
682 if (!Security::PeerConnector::initialize(serverSession
))
685 assert(!icapService
->cfg().secure
.sslDomain
.isEmpty());
687 SBuf
*host
= new SBuf(icapService
->cfg().secure
.sslDomain
);
688 SSL_set_ex_data(serverSession
.get(), ssl_ex_index_server
, host
);
689 setClientSNI(serverSession
.get(), host
->c_str());
692 Security::SetSessionResumeData(serverSession
, icapService
->sslSession
);
697 Ssl::IcapPeerConnector::fillChecklist(ACLFilledChecklist
&checklist
) const
699 Security::PeerConnector::fillChecklist(checklist
);
700 if (checklist
.dst_peer_name
.isEmpty())
701 checklist
.dst_peer_name
= icapService
->cfg().secure
.sslDomain
;
705 Ssl::IcapPeerConnector::noteNegotiationDone(ErrorState
*error
)
710 const int fd
= serverConnection()->fd
;
711 Security::MaybeGetSessionResumeData(fd_table
[fd
].ssl
, icapService
->sslSession
);
715 Adaptation::Icap::Xaction::handleSecuredPeer(Security::EncryptorAnswer
&answer
)
717 encryptionWait
.finish();
719 assert(!answer
.tunneled
);
720 if (answer
.error
.get()) {
721 assert(!answer
.conn
);
722 // TODO: Refactor dieOnConnectionFailure() to be usable here as well.
723 debugs(93, 2, typeName
<<
724 " TLS negotiation to " << service().cfg().uri
<< " failed");
725 service().noteConnectionFailed("failure");
726 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_SSL_START");
728 throw TexcHere("cannot connect to the TLS ICAP service");
731 debugs(93, 5, "TLS negotiation to " << service().cfg().uri
<< " complete");
735 // The socket could get closed while our callback was queued. Sync
736 // Connection. XXX: Connection::fd may already be stale/invalid here.
737 if (answer
.conn
->isOpen() && fd_table
[answer
.conn
->fd
].closing()) {
738 answer
.conn
->noteClosure();
739 service().noteConnectionFailed("external TLS connection closure");
740 static const auto d
= MakeNamedErrorDetail("ICAP_XACT_SSL_CLOSE");
742 throw TexcHere("external closure of the TLS ICAP service connection");
745 useIcapConnection(answer
.conn
);