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 86 ESI processing */
15 #include "client_side.h"
16 #include "client_side_request.h"
17 #include "esi/Include.h"
18 #include "esi/VarState.h"
20 #include "http/Stream.h"
21 #include "HttpReply.h"
22 #include "log/access_log.h"
24 CBDATA_CLASS_INIT (ESIStreamContext
);
27 static CSCB esiBufferRecipient
;
28 static CSD esiBufferDetach
;
29 /* esiStreamContext */
30 static ESIStreamContext
*ESIStreamContextNew (ESIIncludePtr
);
33 * 1. retry failed upstream requests
36 /* Detach from a buffering stream
39 esiBufferDetach (clientStreamNode
*node
, ClientHttpRequest
*http
)
41 /* Detach ourselves */
42 clientStreamDetach (node
, http
);
46 * Write a chunk of data to a client 'socket'.
47 * If the reply is present, send the reply headers down the wire too.
50 * The request is an internal ESI subrequest.
51 * data context is not NULL
52 * There are no more entries in the stream chain.
53 * The caller is responsible for creation and deletion of the Reply headers.
56 * Bug 975, bug 1566 : delete rep; 2006/09/02: TS, #975
58 * This was causing double-deletes. Its possible that not deleting
59 * it here will cause memory leaks, but if so, this delete should
60 * not be reinstated or it will trigger bug #975 again - RBC 20060903
63 esiBufferRecipient (clientStreamNode
*node
, ClientHttpRequest
*http
, HttpReply
*rep
, StoreIOBuffer receivedData
)
65 /* Test preconditions */
66 assert (node
!= nullptr);
67 /* ESI TODO: handle thisNode rather than asserting
68 * - it should only ever happen if we cause an
69 * abort and the callback chain loops back to
70 * here, so we can simply return. However, that
71 * itself shouldn't happen, so it stays as an
73 assert (cbdataReferenceValid (node
));
74 assert (node
->node
.next
== nullptr);
75 assert (http
->getConn() == nullptr);
77 ESIStreamContext::Pointer esiStream
= dynamic_cast<ESIStreamContext
*>(node
->data
.getRaw());
78 assert (esiStream
.getRaw() != nullptr);
79 /* If segments become more flexible, ignore thisNode */
80 assert (receivedData
.length
<= sizeof(esiStream
->localbuffer
->buf
));
81 assert (!esiStream
->finished
);
83 debugs (86,5, "rep " << rep
<< " body " << receivedData
.data
<< " len " << receivedData
.length
);
84 assert (node
->readBuffer
.offset
== receivedData
.offset
|| receivedData
.length
== 0);
88 if (http
->out
.offset
!= 0) {
89 assert(rep
== nullptr);
92 if (rep
->sline
.status() != Http::scOkay
) {
94 esiStream
->include
->includeFail (esiStream
);
95 esiStream
->finished
= 1;
96 httpRequestFree (http
);
104 if (receivedData
.data
&& receivedData
.length
) {
105 http
->out
.offset
+= receivedData
.length
;
107 if (receivedData
.data
>= esiStream
->localbuffer
->buf
&&
108 receivedData
.data
< &esiStream
->localbuffer
->buf
[sizeof(esiStream
->localbuffer
->buf
)]) {
109 /* original static buffer */
111 if (receivedData
.data
!= esiStream
->localbuffer
->buf
) {
112 /* But not the start of it */
113 memmove(esiStream
->localbuffer
->buf
, receivedData
.data
, receivedData
.length
);
116 esiStream
->localbuffer
->len
= receivedData
.length
;
118 assert (esiStream
->buffer
.getRaw() != nullptr);
119 esiStream
->buffer
->len
= receivedData
.length
;
123 /* EOF / Read error / aborted entry */
124 if (rep
== nullptr && receivedData
.data
== nullptr && receivedData
.length
== 0) {
125 /* TODO: get stream status to test the entry for aborts */
126 debugs(86, 5, "Finished reading upstream data in subrequest");
127 esiStream
->include
->subRequestDone (esiStream
, true);
128 esiStream
->finished
= 1;
129 httpRequestFree (http
);
133 switch (clientStreamStatus (node
, http
)) {
135 case STREAM_UNPLANNED_COMPLETE
:
136 case STREAM_COMPLETE
: /* ok */
137 debugs(86, 3, "ESI subrequest finished OK");
138 esiStream
->include
->subRequestDone (esiStream
, true);
139 esiStream
->finished
= 1;
140 httpRequestFree (http
);
144 debugs(86, DBG_IMPORTANT
, "ERROR: ESI subrequest failed transfer");
145 esiStream
->include
->includeFail (esiStream
);
146 esiStream
->finished
= 1;
147 httpRequestFree (http
);
151 StoreIOBuffer tempBuffer
;
153 if (!esiStream
->buffer
.getRaw()) {
154 esiStream
->buffer
= esiStream
->localbuffer
;
157 esiStream
->buffer
= esiStream
->buffer
->tail();
159 if (esiStream
->buffer
->len
) {
160 esiStream
->buffer
->next
= new ESISegment
;
161 esiStream
->buffer
= esiStream
->buffer
->next
;
164 tempBuffer
.offset
= http
->out
.offset
;
165 tempBuffer
.length
= sizeof (esiStream
->buffer
->buf
);
166 tempBuffer
.data
= esiStream
->buffer
->buf
;
167 /* now just read into 'buffer' */
168 clientStreamRead (node
, http
, tempBuffer
);
169 debugs(86, 5, "Requested more data for ESI subrequest");
175 fatal ("Hit unreachable code in esiBufferRecipient\n");
180 /* esiStream functions */
181 ESIStreamContext::~ESIStreamContext()
187 ESIStreamContext::freeResources()
189 debugs(86, 5, "Freeing stream context resources.");
191 localbuffer
= nullptr;
196 ESIStreamContextNew (ESIIncludePtr include
)
198 ESIStreamContext
*rv
= new ESIStreamContext
;
199 rv
->include
= include
;
204 ESIInclude::~ESIInclude()
206 debugs(86, 5, "ESIInclude::Free " << this);
207 ESISegmentFreeList (srccontent
);
208 ESISegmentFreeList (altcontent
);
209 cbdataReferenceDone (varState
);
221 ESIInclude::makeCacheable() const
223 return new ESIInclude (*this);
227 ESIInclude::makeUsable(esiTreeParentPtr newParent
, ESIVarState
&newVarState
) const
229 ESIInclude
*resultI
= new ESIInclude (*this);
230 ESIElement::Pointer result
= resultI
;
231 resultI
->parent
= newParent
;
232 resultI
->varState
= cbdataReference (&newVarState
);
235 resultI
->src
= ESIStreamContextNew (resultI
);
238 resultI
->alt
= ESIStreamContextNew (resultI
);
243 ESIInclude::ESIInclude(ESIInclude
const &old
) :
251 memset(&flags
, 0, sizeof(flags
));
252 flags
.onerrorcontinue
= old
.flags
.onerrorcontinue
;
255 srcurl
= xstrdup(old
.srcurl
);
258 alturl
= xstrdup(old
.alturl
);
262 ESIInclude::prepareRequestHeaders(HttpHeader
&tempheaders
, ESIVarState
*vars
)
264 tempheaders
.update(&vars
->header());
265 tempheaders
.removeHopByHopEntries();
269 ESIInclude::Start (ESIStreamContext::Pointer stream
, char const *url
, ESIVarState
*vars
)
271 if (!stream
.getRaw())
274 HttpHeader
tempheaders(hoRequest
);
276 prepareRequestHeaders(tempheaders
, vars
);
278 /* Ensure variable state is clean */
279 vars
->feedData(url
, strlen (url
));
281 /* tempUrl is eaten by the request */
282 char const *tempUrl
= vars
->extractChar ();
284 debugs(86, 5, "ESIIncludeStart: Starting subrequest with url '" << tempUrl
<< "'");
285 const auto mx
= MasterXaction::MakePortless
<XactionInitiator::initEsi
>();
286 if (clientBeginRequest(Http::METHOD_GET
, tempUrl
, esiBufferRecipient
, esiBufferDetach
, stream
.getRaw(), &tempheaders
, stream
->localbuffer
->buf
, HTTP_REQBUF_SZ
, mx
)) {
287 debugs(86, DBG_CRITICAL
, "ERROR: starting new ESI subrequest failed");
293 ESIInclude::ESIInclude(esiTreeParentPtr aParent
, int attrcount
, char const **attr
, ESIContext
*aContext
) :
302 memset(&flags
, 0, sizeof(flags
));
304 for (int i
= 0; i
< attrcount
&& attr
[i
]; i
+= 2) {
305 if (!strcmp(attr
[i
],"src")) {
306 /* Start a request for thisNode url */
307 debugs(86, 5, "ESIIncludeNew: Requesting source '" << attr
[i
+1] << "'");
309 /* TODO: don't assert on thisNode, ignore the duplicate */
310 assert (src
.getRaw() == nullptr);
311 src
= ESIStreamContextNew (this);
312 assert (src
.getRaw() != nullptr);
313 srcurl
= xstrdup(attr
[i
+1]);
314 } else if (!strcmp(attr
[i
],"alt")) {
315 /* Start a secondary request for thisNode url */
316 /* TODO: make a config parameter to wait on requesting alt's
317 * for the src to fail
319 debugs(86, 5, "ESIIncludeNew: Requesting alternate '" << attr
[i
+1] << "'");
321 assert (alt
.getRaw() == nullptr); /* TODO: fix? */
322 alt
= ESIStreamContextNew (this);
323 assert (alt
.getRaw() != nullptr);
324 alturl
= xstrdup(attr
[i
+1]);
325 } else if (!strcmp(attr
[i
],"onerror")) {
326 if (!strcmp(attr
[i
+1], "continue")) {
327 flags
.onerrorcontinue
= 1;
329 /* ignore mistyped attributes */
330 debugs(86, DBG_IMPORTANT
, "ERROR: invalid value for onerror='" << attr
[i
+1] << "'");
333 /* ignore mistyped attributes. TODO:? error on these for user feedback - config parameter needed
338 varState
= cbdataReference(aContext
->varState
);
344 /* prevent freeing ourselves */
345 ESIIncludePtr
foo(this);
353 Start (src
, srcurl
, varState
);
354 Start (alt
, alturl
, varState
);
358 debugs(86, DBG_IMPORTANT
, "ESIIncludeNew: esi:include with no src attributes");
365 ESIInclude::render(ESISegment::Pointer output
)
370 ESISegment::Pointer myout
;
372 debugs(86, 5, "ESIIncludeRender: Rendering include " << this);
374 assert (flags
.finished
|| (flags
.failed
&& flags
.onerrorcontinue
));
376 if (flags
.failed
&& flags
.onerrorcontinue
) {
380 /* Render the content */
381 if (srccontent
.getRaw()) {
383 srccontent
= nullptr;
384 } else if (altcontent
.getRaw()) {
386 altcontent
= nullptr;
388 fatal ("ESIIncludeRender called with no content, and no failure!\n");
390 assert (output
->next
== nullptr);
392 output
->next
= myout
;
398 ESIInclude::process(int)
400 /* Prevent refcount race leading to free */
403 debugs(86, 5, "ESIIncludeRender: Processing include " << this);
406 if (flags
.onerrorcontinue
)
407 return ESI_PROCESS_COMPLETE
;
409 return ESI_PROCESS_FAILED
;
412 if (!flags
.finished
) {
413 if (flags
.onerrorcontinue
)
414 return ESI_PROCESS_PENDING_WONTFAIL
;
416 return ESI_PROCESS_PENDING_MAYFAIL
;
419 return ESI_PROCESS_COMPLETE
;
423 ESIInclude::includeFail (ESIStreamContext::Pointer stream
)
425 subRequestDone (stream
, false);
429 ESIInclude::dataNeeded() const
431 return !(flags
.finished
|| flags
.failed
);
435 ESIInclude::subRequestDone (ESIStreamContext::Pointer stream
, bool success
)
441 debugs(86, 3, "ESIInclude::subRequestDone: " << srcurl
);
444 /* copy the lead segment */
445 debugs(86, 3, "ESIIncludeSubRequestDone: Src OK - include PASSED.");
446 assert (!srccontent
.getRaw());
447 ESISegment::ListTransfer (stream
->localbuffer
, srccontent
);
451 /* Fail if there is no alt being retrieved */
452 debugs(86, 3, "ESIIncludeSubRequestDone: Src FAILED");
454 if (!(alt
.getRaw() || altcontent
.getRaw())) {
455 debugs(86, 3, "ESIIncludeSubRequestDone: Include FAILED - No ALT");
457 } else if (altcontent
.getRaw()) {
458 debugs(86, 3, "ESIIncludeSubRequestDone: Include PASSED - ALT already Complete");
459 /* ALT was already retrieved, we are done */
465 } else if (stream
== alt
) {
466 debugs(86, 3, "ESIInclude::subRequestDone: " << alturl
);
469 debugs(86, 3, "ESIIncludeSubRequestDone: ALT OK.");
470 /* copy the lead segment */
471 assert (!altcontent
.getRaw());
472 ESISegment::ListTransfer (stream
->localbuffer
, altcontent
);
475 if (!(src
.getRaw() || srccontent
.getRaw())) {
476 /* src already failed, kick ESI processor */
477 debugs(86, 3, "ESIIncludeSubRequestDone: Include PASSED - SRC already failed.");
481 if (!(src
.getRaw() || srccontent
.getRaw())) {
482 debugs(86, 3, "ESIIncludeSubRequestDone: ALT FAILED, Include FAILED - SRC already failed");
483 /* src already failed */
490 fatal ("ESIIncludeSubRequestDone: non-owned stream found!\n");
493 if (flags
.finished
|| flags
.failed
) {
494 /* Kick ESI Processor */
495 debugs (86, 5, "ESIInclude " << this <<
496 " SubRequest " << stream
.getRaw() <<
497 " completed, kicking processor , status " <<
498 (flags
.finished
? "OK" : "FAILED"));
499 /* There is a race condition - and we have no reproducible test case -
500 * during a subrequest the parent will get set to NULL, which is not
501 * meant to be possible. Rather than killing squid, we let it leak
502 * memory but complain in the log.
504 * Someone wanting to debug this could well start by running squid with
505 * a hardware breakpoint set to this location.
506 * Its probably due to parent being set to null - by a call to
507 * 'this.finish' while the subrequest is still not completed.
509 if (parent
.getRaw() == nullptr) {
510 debugs(86, DBG_CRITICAL
, "ERROR: Squid Bug #951: ESIInclude::subRequestDone: Sub request completed "
511 "after finish() called and parent unlinked. Unable to "
512 "continue handling the request, and may be memory leaking. "
513 "See http://www.squid-cache.org/bugs/show_bug.cgi?id=951 - we "
514 "are looking for a reproducible test case. This will require "
515 "an ESI template with includes, probably with alt-options, "
516 "and we're likely to need traffic dumps to allow us to "
517 "reconstruct the exact tcp handling sequences to trigger this "
518 "rather elusive bug.");
521 assert (parent
.getRaw());
525 parent
->provideData (srccontent
.getRaw() ? srccontent
:altcontent
,this);
527 if (srccontent
.getRaw())
528 srccontent
= nullptr;
530 altcontent
= nullptr;
531 } else if (flags
.onerrorcontinue
) {
532 /* render nothing but inform of completion */
536 parent
->provideData (new ESISegment
, this);
540 parent
->fail(this, "esi:include could not be completed.");
544 #endif /* USE_SQUID_ESI */