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 "adaptation/Answer.h"
13 #include "adaptation/icap/Config.h"
14 #include "adaptation/icap/ModXact.h"
15 #include "adaptation/icap/Options.h"
16 #include "adaptation/icap/OptXact.h"
17 #include "adaptation/icap/ServiceRep.h"
18 #include "base/TextException.h"
19 #include "comm/Connection.h"
20 #include "ConfigParser.h"
21 #include "debug/Stream.h"
24 #include "HttpReply.h"
26 #include "SquidConfig.h"
28 #define DEFAULT_ICAP_PORT 1344
29 #define DEFAULT_ICAPS_PORT 11344
31 CBDATA_NAMESPACED_CLASS_INIT(Adaptation::Icap
, ServiceRep
);
33 Adaptation::Icap::ServiceRep::ServiceRep(const ServiceConfigPointer
&svcCfg
):
34 AsyncJob("Adaptation::Icap::ServiceRep"), Adaptation::Service(svcCfg
),
35 theOptions(nullptr), theOptionsFetcher(nullptr), theLastUpdate(0),
38 connOverloadReported(false),
39 theIdleConns(nullptr),
40 isSuspended(nullptr), notifying(false),
41 updateScheduled(false),
42 wasAnnouncedUp(true), // do not announce an "up" service at startup
46 theIdleConns
= new IdleConnList("ICAP Service", nullptr);
49 Adaptation::Icap::ServiceRep::~ServiceRep()
53 Must(!theOptionsFetcher
);
59 Adaptation::Icap::ServiceRep::finalize()
61 Adaptation::Service::finalize();
63 // use /etc/services or default port if needed
64 const bool have_port
= cfg().port
>= 0;
67 if (cfg().protocol
.caseCmp("icaps") == 0)
68 serv
= getservbyname("icaps", "tcp");
70 serv
= getservbyname("icap", "tcp");
73 writeableCfg().port
= htons(serv
->s_port
);
75 writeableCfg().port
= cfg().protocol
.caseCmp("icaps") == 0 ? DEFAULT_ICAPS_PORT
: DEFAULT_ICAP_PORT
;
79 if (cfg().protocol
.caseCmp("icaps") == 0)
80 writeableCfg().secure
.encryptTransport
= true;
82 if (cfg().secure
.encryptTransport
) {
83 debugs(3, DBG_IMPORTANT
, "Initializing service " << cfg().resource
<< " SSL context");
84 sslContext
= writeableCfg().secure
.createClientContext(true);
87 if (!cfg().connectionEncryption
.configured())
88 writeableCfg().connectionEncryption
.defaultTo(cfg().secure
.encryptTransport
);
90 theSessionFailures
.configure(TheConfig
.oldest_service_failure
> 0 ?
91 TheConfig
.oldest_service_failure
: -1);
94 void Adaptation::Icap::ServiceRep::noteFailure()
96 const int failures
= theSessionFailures
.count(1);
97 debugs(93,4, " failure " << failures
<< " out of " <<
98 TheConfig
.service_failure_limit
<< " allowed in " <<
99 TheConfig
.oldest_service_failure
<< "sec " << status());
104 if (TheConfig
.service_failure_limit
>= 0 &&
105 failures
> TheConfig
.service_failure_limit
)
106 suspend("too many failures");
108 // TODO: Should bypass setting affect how much Squid tries to talk to
109 // the ICAP service that is currently unusable and is likely to remain
110 // so for some time? The current code says "no". Perhaps the answer
111 // should be configurable.
114 // TODO: getIdleConnection() and putConnection()/noteConnectionFailed() manage a
115 // "used connection slot" resource. Automate that resource tracking (RAII/etc.).
116 Comm::ConnectionPointer
117 Adaptation::Icap::ServiceRep::getIdleConnection(const bool retriableXact
)
119 Comm::ConnectionPointer connection
;
121 /* 2011-06-17: rousskov:
122 * There are two things that happen at the same time in pop(). Both are important.
123 * 1) Ensure that we can use a pconn for this transaction.
124 * 2) Ensure that the number of idle pconns does not grow without bounds.
126 * Both happen in the beginning of the transaction. Both are dictated by real-world problems.
127 * retriable means you can repeat the request if you suspect the first try failed due to a pconn race.
128 * HTTP and ICAP rules prohibit the use of pconns for non-retriable requests.
130 * If there are zero idle connections, (2) is irrelevant. (2) is only relevant when there are many
131 * idle connections and we should not open more connections without closing some idle ones,
132 * or instead of just opening a new connection and leaving idle connections as is.
133 * In other words, (2) tells us to close one FD for each new one we open due to retriable.
136 connection
= theIdleConns
->pop();
138 theIdleConns
->closeN(1);
141 debugs(93,3, "got connection: " << connection
);
145 // pools connection if it is reusable or closes it
146 void Adaptation::Icap::ServiceRep::putConnection(const Comm::ConnectionPointer
&conn
, bool isReusable
, bool sendReset
, const char *comment
)
148 Must(Comm::IsConnOpen(conn
));
149 // do not pool an idle connection if we owe connections
150 if (isReusable
&& excessConnections() == 0) {
151 debugs(93, 3, "pushing pconn" << comment
);
152 theIdleConns
->push(conn
);
154 debugs(93, 3, (sendReset
? "RST" : "FIN") << "-closing " <<
156 // comm_close called from Connection::close will clear timeout
157 // TODO: add "bool sendReset = false" to Connection::close()?
159 comm_reset_close(conn
);
164 Must(theBusyConns
> 0);
166 // a connection slot released. Check if there are waiters....
170 // a wrapper to avoid exposing theIdleConns
171 void Adaptation::Icap::ServiceRep::noteConnectionUse(const Comm::ConnectionPointer
&conn
)
173 Must(Comm::IsConnOpen(conn
));
174 fd_table
[conn
->fd
].noteUse(); // pconn re-use, albeit not via PconnPool API
177 void Adaptation::Icap::ServiceRep::noteConnectionFailed(const char *comment
)
179 debugs(93, 3, "Connection failed: " << comment
);
183 void Adaptation::Icap::ServiceRep::setMaxConnections()
185 if (cfg().maxConn
>= 0)
186 theMaxConnections
= cfg().maxConn
;
187 else if (theOptions
&& theOptions
->max_connections
>= 0)
188 theMaxConnections
= theOptions
->max_connections
;
190 theMaxConnections
= -1;
194 if (::Config
.workers
> 1 )
195 theMaxConnections
/= ::Config
.workers
;
198 int Adaptation::Icap::ServiceRep::availableConnections() const
200 if (theMaxConnections
< 0)
203 // we are available if we can open or reuse connections
204 // in other words, if we will not create debt
205 int available
= max(0, theMaxConnections
- theBusyConns
);
207 if (!available
&& !connOverloadReported
) {
208 debugs(93, DBG_IMPORTANT
, "WARNING: ICAP Max-Connections limit " <<
209 "exceeded for service " << cfg().uri
<< ". Open connections now: " <<
210 theBusyConns
+ theIdleConns
->count() << ", including " <<
211 theIdleConns
->count() << " idle persistent connections.");
212 connOverloadReported
= true;
215 if (cfg().onOverload
== srvForce
)
221 // The number of connections which excess the Max-Connections limit
222 int Adaptation::Icap::ServiceRep::excessConnections() const
224 if (theMaxConnections
< 0)
227 // Waiters affect the number of needed connections but a needed
228 // connection may still be excessive from Max-Connections p.o.v.
229 // so we should not account for waiting transaction needs here.
230 const int debt
= theBusyConns
+ theIdleConns
->count() - theMaxConnections
;
237 void Adaptation::Icap::ServiceRep::noteGoneWaiter()
241 // in case the notified transaction did not take the connection slot
245 // called when a connection slot may become available
246 void Adaptation::Icap::ServiceRep::busyCheckpoint()
248 if (theNotificationWaiters
.empty()) // nobody is waiting for a slot
252 int available
= availableConnections();
255 // It is possible to have waiters when no limit on connections exist in
256 // case of reconfigure or because new Options received.
257 // In this case, notify all waiting transactions.
258 freed
= theNotificationWaiters
.size();
260 // avoid notifying more waiters than there will be available slots
261 const int notifiedWaiters
= theAllWaiters
- theNotificationWaiters
.size();
262 freed
= available
- notifiedWaiters
;
265 debugs(93,7, "Available connections: " << available
<<
266 " freed slots: " << freed
<<
267 " waiting in queue: " << theNotificationWaiters
.size());
269 while (freed
> 0 && !theNotificationWaiters
.empty()) {
270 Client i
= theNotificationWaiters
.front();
271 theNotificationWaiters
.pop_front();
272 ScheduleCallHere(i
.callback
);
273 i
.callback
= nullptr;
278 void Adaptation::Icap::ServiceRep::suspend(const char *reason
)
281 debugs(93,4, "keeping suspended, also for " << reason
);
283 isSuspended
= reason
;
284 debugs(93, DBG_IMPORTANT
, "suspending ICAP service for " << reason
);
285 scheduleUpdate(squid_curtime
+ TheConfig
.service_revival_delay
);
286 announceStatusChange("suspended", true);
290 bool Adaptation::Icap::ServiceRep::probed() const
292 return theLastUpdate
!= 0;
295 bool Adaptation::Icap::ServiceRep::hasOptions() const
297 return theOptions
&& theOptions
->valid() && theOptions
->fresh();
300 bool Adaptation::Icap::ServiceRep::up() const
302 return !isSuspended
&& hasOptions();
305 bool Adaptation::Icap::ServiceRep::availableForNew() const
308 int available
= availableConnections();
312 return (available
- theAllWaiters
> 0);
315 bool Adaptation::Icap::ServiceRep::availableForOld() const
319 int available
= availableConnections();
320 return (available
!= 0); // it is -1 (no limit) or has available slots
323 bool Adaptation::Icap::ServiceRep::wantsUrl(const SBuf
&urlPath
) const
326 return theOptions
->transferKind(urlPath
) != Adaptation::Icap::Options::xferIgnore
;
329 bool Adaptation::Icap::ServiceRep::wantsPreview(const SBuf
&urlPath
, size_t &wantedSize
) const
333 if (theOptions
->preview
< 0)
336 if (theOptions
->transferKind(urlPath
) != Adaptation::Icap::Options::xferPreview
)
339 wantedSize
= theOptions
->preview
;
344 bool Adaptation::Icap::ServiceRep::allows204() const
347 return true; // in the future, we may have ACLs to prevent 204s
350 bool Adaptation::Icap::ServiceRep::allows206() const
353 if (theOptions
->allow206
)
354 return true; // in the future, we may have ACLs to prevent 206s
359 void ServiceRep_noteTimeToUpdate(void *data
)
361 Adaptation::Icap::ServiceRep
*service
= static_cast<Adaptation::Icap::ServiceRep
*>(data
);
363 service
->noteTimeToUpdate();
366 void Adaptation::Icap::ServiceRep::noteTimeToUpdate()
369 updateScheduled
= false;
371 if (detached() || theOptionsFetcher
.set()) {
372 debugs(93,5, "ignores options update " << status());
376 debugs(93,5, "performs a regular options update " << status());
377 startGettingOptions();
380 void Adaptation::Icap::ServiceRep::noteTimeToNotify()
384 debugs(93,7, "notifies " << theClients
.size() << " clients " <<
387 // note: we must notify even if we are invalidated
389 Pointer us
= nullptr;
391 while (!theClients
.empty()) {
392 Client i
= theClients
.back();
393 theClients
.pop_back();
394 ScheduleCallHere(i
.callback
);
395 i
.callback
= nullptr;
401 void Adaptation::Icap::ServiceRep::callWhenAvailable(AsyncCall::Pointer
&cb
, bool priority
)
403 debugs(93,8, "ICAPServiceRep::callWhenAvailable");
406 Must(!theIdleConns
->count()); // or we should not be waiting
409 i
.service
= Pointer(this);
412 theNotificationWaiters
.push_front(i
);
414 theNotificationWaiters
.push_back(i
);
419 void Adaptation::Icap::ServiceRep::callWhenReady(AsyncCall::Pointer
&cb
)
423 debugs(93,5, "Adaptation::Icap::Service is asked to call " << *cb
<<
424 " when ready " << status());
426 Must(!broken()); // we do not wait for a broken service
429 i
.service
= Pointer(this); // TODO: is this really needed?
431 theClients
.push_back(i
);
433 if (theOptionsFetcher
.set() || notifying
)
434 return; // do nothing, we will be picked up in noteTimeToNotify()
436 if (needNewOptions())
437 startGettingOptions();
439 scheduleNotification();
442 void Adaptation::Icap::ServiceRep::scheduleNotification()
444 debugs(93,7, "will notify " << theClients
.size() << " clients");
445 CallJobHere(93, 5, this, Adaptation::Icap::ServiceRep
, noteTimeToNotify
);
448 bool Adaptation::Icap::ServiceRep::needNewOptions() const
450 return !detached() && !up();
453 void Adaptation::Icap::ServiceRep::changeOptions(Adaptation::Icap::Options
*newOptions
)
455 debugs(93,8, "changes options from " << theOptions
<< " to " <<
456 newOptions
<< ' ' << status());
459 theOptions
= newOptions
;
460 theSessionFailures
.clear();
461 isSuspended
= nullptr;
462 theLastUpdate
= squid_curtime
;
465 announceStatusChange("down after an options fetch failure", true);
468 void Adaptation::Icap::ServiceRep::checkOptions()
470 if (theOptions
== nullptr)
473 if (!theOptions
->valid()) {
474 debugs(93, DBG_IMPORTANT
, "WARNING: Squid got an invalid ICAP OPTIONS response " <<
475 "from service " << cfg().uri
<< "; error: " << theOptions
->error
);
480 * Issue a warning if the ICAP server returned methods in the
481 * options response that don't match the method from squid.conf.
484 if (!theOptions
->methods
.empty()) {
485 bool method_found
= false;
487 std::vector
<ICAP::Method
>::iterator iter
= theOptions
->methods
.begin();
489 while (iter
!= theOptions
->methods
.end()) {
491 if (*iter
== cfg().method
) {
496 method_list
.append(ICAP::methodStr(*iter
));
497 method_list
.append(" ", 1);
502 debugs(93, DBG_IMPORTANT
, "WARNING: Squid is configured to use ICAP method " <<
504 " for service " << cfg().uri
<<
505 " but OPTIONS response declares the methods are " << method_list
);
510 * Check the ICAP server's date header for clock skew
512 const int skew
= (int)(theOptions
->timestamp() - squid_curtime
);
513 if (abs(skew
) > theOptions
->ttl()) {
514 // TODO: If skew is negative, the option will be considered down
515 // because of stale options. We should probably change this.
516 debugs(93, DBG_IMPORTANT
, "ICAP service's clock is skewed by " << skew
<<
517 " seconds: " << cfg().uri
);
521 void Adaptation::Icap::ServiceRep::announceStatusChange(const char *downPhrase
, bool important
) const
523 if (wasAnnouncedUp
== up()) // no significant changes to announce
526 const char *what
= cfg().bypass
? "optional" : "essential";
527 const char *state
= wasAnnouncedUp
? downPhrase
: "up";
528 const int level
= important
? 1 :2;
529 debugs(93,level
, what
<< " ICAP service is " << state
<< ": " <<
530 cfg().uri
<< ' ' << status());
532 wasAnnouncedUp
= !wasAnnouncedUp
;
535 // we are receiving ICAP OPTIONS response headers here or NULL on failures
536 void Adaptation::Icap::ServiceRep::noteAdaptationAnswer(const Answer
&answer
)
538 Must(initiated(theOptionsFetcher
));
539 clearAdaptation(theOptionsFetcher
);
541 if (answer
.kind
== Answer::akError
) {
542 debugs(93,3, "failed to fetch options " << status());
543 handleNewOptions(nullptr);
547 Must(answer
.kind
== Answer::akForward
); // no akBlock for OPTIONS requests
548 const Http::Message
*msg
= answer
.message
.getRaw();
551 debugs(93,5, "is interpreting new options " << status());
553 Adaptation::Icap::Options
*newOptions
= nullptr;
554 if (const HttpReply
*r
= dynamic_cast<const HttpReply
*>(msg
)) {
555 newOptions
= new Adaptation::Icap::Options
;
556 newOptions
->configure(r
);
558 debugs(93, DBG_IMPORTANT
, "ICAP service got wrong options message " << status());
561 handleNewOptions(newOptions
);
564 // we (a) must keep trying to get OPTIONS and (b) are RefCounted so we
565 // must keep our job alive (XXX: until nobody needs us)
566 void Adaptation::Icap::ServiceRep::callException(const std::exception
&e
)
568 clearAdaptation(theOptionsFetcher
);
569 debugs(93,2, "ICAP probably failed to fetch options (" << e
.what() <<
571 handleNewOptions(nullptr);
574 void Adaptation::Icap::ServiceRep::handleNewOptions(Adaptation::Icap::Options
*newOptions
)
576 // new options may be NULL
577 changeOptions(newOptions
);
579 debugs(93,3, "got new options and is now " << status());
581 scheduleUpdate(optionsFetchTime());
583 // XXX: this whole feature bases on the false assumption a service only has one IP
585 const int excess
= excessConnections();
586 // if we owe connections and have idle pconns, close the latter
587 if (excess
&& theIdleConns
->count() > 0) {
588 const int n
= min(excess
, theIdleConns
->count());
589 debugs(93,5, "closing " << n
<< " pconns to relief debt");
590 theIdleConns
->closeN(n
);
593 scheduleNotification();
596 void Adaptation::Icap::ServiceRep::startGettingOptions()
598 Must(!theOptionsFetcher
);
599 debugs(93,6, "will get new options " << status());
601 // XXX: "this" here is "self"; works until refcounting API changes
602 theOptionsFetcher
= initiateAdaptation(
603 new Adaptation::Icap::OptXactLauncher(this));
604 // TODO: timeout in case Adaptation::Icap::OptXact never calls us back?
605 // Such a timeout should probably be a generic AsyncStart feature.
608 void Adaptation::Icap::ServiceRep::scheduleUpdate(time_t when
)
610 if (updateScheduled
) {
611 debugs(93,7, "reschedules update");
612 // XXX: check whether the event is there because AR saw
613 // an unreproducible eventDelete assertion on 2007/06/18
614 if (eventFind(&ServiceRep_noteTimeToUpdate
, this))
615 eventDelete(&ServiceRep_noteTimeToUpdate
, this);
617 debugs(93, DBG_IMPORTANT
, "ERROR: Squid BUG: ICAP service lost an update event.");
618 updateScheduled
= false;
621 debugs(93,7, "raw OPTIONS fetch at " << when
<< " or in " <<
622 (when
- squid_curtime
) << " sec");
623 debugs(93,9, "last fetched at " << theLastUpdate
<< " or " <<
624 (squid_curtime
- theLastUpdate
) << " sec ago");
626 /* adjust update time to prevent too-frequent updates */
628 if (when
< squid_curtime
)
629 when
= squid_curtime
;
631 // XXX: move hard-coded constants from here to Adaptation::Icap::TheConfig
632 const int minUpdateGap
= 30; // seconds
633 if (when
< theLastUpdate
+ minUpdateGap
)
634 when
= theLastUpdate
+ minUpdateGap
;
636 const int delay
= when
- squid_curtime
;
637 debugs(93,5, "will fetch OPTIONS in " << delay
<< " sec");
639 eventAdd("Adaptation::Icap::ServiceRep::noteTimeToUpdate",
640 &ServiceRep_noteTimeToUpdate
, this, delay
, 0, true);
641 updateScheduled
= true;
644 // returns absolute time when OPTIONS should be fetched
646 Adaptation::Icap::ServiceRep::optionsFetchTime() const
648 if (theOptions
&& theOptions
->valid()) {
649 const time_t expire
= theOptions
->expire();
650 debugs(93,7, "options expire on " << expire
<< " >= " << squid_curtime
);
652 // conservative estimate of how long the OPTIONS transaction will take
653 // XXX: move hard-coded constants from here to Adaptation::Icap::TheConfig
654 const int expectedWait
= 20; // seconds
656 // Unknown or invalid (too small) expiration times should not happen.
657 // Adaptation::Icap::Options should use the default TTL, and ICAP servers should not
658 // send invalid TTLs, but bugs and attacks happen.
659 if (expire
< expectedWait
)
660 return squid_curtime
;
662 return expire
- expectedWait
; // before the current options expire
665 // use revival delay as "expiration" time for a service w/o valid options
666 return squid_curtime
+ TheConfig
.service_revival_delay
;
669 Adaptation::Initiate
*
670 Adaptation::Icap::ServiceRep::makeXactLauncher(Http::Message
*virgin
,
671 HttpRequest
*cause
, AccessLogEntry::Pointer
&alp
)
673 return new Adaptation::Icap::ModXactLauncher(virgin
, cause
, alp
, this);
676 // returns a temporary string depicting service status, for debugging
677 const char *Adaptation::Icap::ServiceRep::status() const
687 buf
.append("down", 4);
689 buf
.append(",susp", 5);
692 buf
.append(",!opt", 5);
693 else if (!theOptions
->valid())
694 buf
.append(",!valid", 7);
695 else if (!theOptions
->fresh())
696 buf
.append(",stale", 6);
700 buf
.append(",detached", 9);
702 if (theOptionsFetcher
.set())
703 buf
.append(",fetch", 6);
706 buf
.append(",notif", 6);
708 if (const int failures
= theSessionFailures
.remembered())
709 buf
.appendf(",fail%d", failures
);
714 return buf
.content();
717 void Adaptation::Icap::ServiceRep::detach()
719 debugs(93,3, "detaching ICAP service: " << cfg().uri
<<
724 bool Adaptation::Icap::ServiceRep::detached() const
729 Adaptation::Icap::ConnWaiterDialer::ConnWaiterDialer(const CbcPointer
<Adaptation::Icap::ModXact
> &xact
,
730 Adaptation::Icap::ConnWaiterDialer::Parent::Method aHandler
):
731 Parent(xact
, aHandler
)
733 theService
= &xact
->service();
734 theService
->noteNewWaiter();
737 Adaptation::Icap::ConnWaiterDialer::ConnWaiterDialer(const Adaptation::Icap::ConnWaiterDialer
&aConnWaiter
): Parent(aConnWaiter
)
739 theService
= aConnWaiter
.theService
;
740 theService
->noteNewWaiter();
743 Adaptation::Icap::ConnWaiterDialer::~ConnWaiterDialer()
745 theService
->noteGoneWaiter();