1 /*
2 * Copyright (C) 1996-2021 The Squid Software Foundation and contributors
3 *
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.
7 */
8
9 /* DEBUG: section 86 ESI processing */
10
11 #include "squid.h"
12
13 #if USE_SQUID_ESI
14
15 #include "client_side.h"
16 #include "client_side_request.h"
17 #include "esi/Include.h"
18 #include "esi/VarState.h"
19 #include "fatal.h"
20 #include "http/Stream.h"
21 #include "HttpReply.h"
22 #include "log/access_log.h"
23
24 CBDATA_CLASS_INIT (ESIStreamContext);
25
26 /* other */
27 static CSCB esiBufferRecipient;
28 static CSD esiBufferDetach;
29 /* esiStreamContext */
30 static ESIStreamContext *ESIStreamContextNew (ESIIncludePtr);
31
32 /* ESI TO CONSIDER:
33 * 1. retry failed upstream requests
34 */
35
36 /* Detach from a buffering stream
37 */
38 void
esiBufferDetach(clientStreamNode * node,ClientHttpRequest * http)39 esiBufferDetach (clientStreamNode *node, ClientHttpRequest *http)
40 {
41 /* Detach ourselves */
42 clientStreamDetach (node, http);
43 }
44
45 /**
46 * Write a chunk of data to a client 'socket'.
47 * If the reply is present, send the reply headers down the wire too.
48 *
49 * Pre-condition:
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.
54 *
55 \note
56 * Bug 975, bug 1566 : delete rep; 2006/09/02: TS, #975
57 *
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
61 */
62 void
esiBufferRecipient(clientStreamNode * node,ClientHttpRequest * http,HttpReply * rep,StoreIOBuffer receivedData)63 esiBufferRecipient (clientStreamNode *node, ClientHttpRequest *http, HttpReply *rep, StoreIOBuffer receivedData)
64 {
65 /* Test preconditions */
66 assert (node != NULL);
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
72 * assert for now. */
73 assert (cbdataReferenceValid (node));
74 assert (node->node.next == NULL);
75 assert (http->getConn() == NULL);
76
77 ESIStreamContext::Pointer esiStream = dynamic_cast<ESIStreamContext *>(node->data.getRaw());
78 assert (esiStream.getRaw() != NULL);
79 /* If segments become more flexible, ignore thisNode */
80 assert (receivedData.length <= sizeof(esiStream->localbuffer->buf));
81 assert (!esiStream->finished);
82
83 debugs (86,5, HERE << "rep " << rep << " body " << receivedData.data << " len " << receivedData.length);
84 assert (node->readBuffer.offset == receivedData.offset || receivedData.length == 0);
85
86 /* trivial case */
87
88 if (http->out.offset != 0) {
89 assert(rep == NULL);
90 } else {
91 if (rep) {
92 if (rep->sline.status() != Http::scOkay) {
93 rep = NULL;
94 esiStream->include->includeFail (esiStream);
95 esiStream->finished = 1;
96 httpRequestFree (http);
97 return;
98 }
99
100 #if HEADERS_LOG
101 /* should be done in the store rather than every recipient? */
102 headersLog(0, 0, http->request->method, rep);
103
104 #endif
105 rep = NULL;
106 }
107 }
108
109 if (receivedData.data && receivedData.length) {
110 http->out.offset += receivedData.length;
111
112 if (receivedData.data >= esiStream->localbuffer->buf &&
113 receivedData.data < &esiStream->localbuffer->buf[sizeof(esiStream->localbuffer->buf)]) {
114 /* original static buffer */
115
116 if (receivedData.data != esiStream->localbuffer->buf) {
117 /* But not the start of it */
118 memmove(esiStream->localbuffer->buf, receivedData.data, receivedData.length);
119 }
120
121 esiStream->localbuffer->len = receivedData.length;
122 } else {
123 assert (esiStream->buffer.getRaw() != NULL);
124 esiStream->buffer->len = receivedData.length;
125 }
126 }
127
128 /* EOF / Read error / aborted entry */
129 if (rep == NULL && receivedData.data == NULL && receivedData.length == 0) {
130 /* TODO: get stream status to test the entry for aborts */
131 debugs(86, 5, HERE << "Finished reading upstream data in subrequest");
132 esiStream->include->subRequestDone (esiStream, true);
133 esiStream->finished = 1;
134 httpRequestFree (http);
135 return;
136 }
137
138 /* after the write to the user occurs, (ie here, or in a callback)
139 * we call */
140 if (clientHttpRequestStatus(-1, http)) {
141 /* TODO: Does thisNode if block leak htto ? */
142 /* XXX when reviewing ESI this is the first place to look */
143 node->data = NULL;
144 esiStream->finished = 1;
145 esiStream->include->includeFail (esiStream);
146 return;
147 };
148
149 switch (clientStreamStatus (node, http)) {
150
151 case STREAM_UNPLANNED_COMPLETE: /* fallthru ok */
152
153 case STREAM_COMPLETE: /* ok */
154 debugs(86, 3, "ESI subrequest finished OK");
155 esiStream->include->subRequestDone (esiStream, true);
156 esiStream->finished = 1;
157 httpRequestFree (http);
158 return;
159
160 case STREAM_FAILED:
161 debugs(86, DBG_IMPORTANT, "ESI subrequest failed transfer");
162 esiStream->include->includeFail (esiStream);
163 esiStream->finished = 1;
164 httpRequestFree (http);
165 return;
166
167 case STREAM_NONE: {
168 StoreIOBuffer tempBuffer;
169
170 if (!esiStream->buffer.getRaw()) {
171 esiStream->buffer = esiStream->localbuffer;
172 }
173
174 esiStream->buffer = esiStream->buffer->tail();
175
176 if (esiStream->buffer->len) {
177 esiStream->buffer->next = new ESISegment;
178 esiStream->buffer = esiStream->buffer->next;
179 }
180
181 tempBuffer.offset = http->out.offset;
182 tempBuffer.length = sizeof (esiStream->buffer->buf);
183 tempBuffer.data = esiStream->buffer->buf;
184 /* now just read into 'buffer' */
185 clientStreamRead (node, http, tempBuffer);
186 debugs(86, 5, HERE << "Requested more data for ESI subrequest");
187 }
188
189 break;
190
191 default:
192 fatal ("Hit unreachable code in esiBufferRecipient\n");
193 }
194
195 }
196
197 /* esiStream functions */
~ESIStreamContext()198 ESIStreamContext::~ESIStreamContext()
199 {
200 freeResources();
201 }
202
203 void
freeResources()204 ESIStreamContext::freeResources()
205 {
206 debugs(86, 5, "Freeing stream context resources.");
207 buffer = NULL;
208 localbuffer = NULL;
209 include = NULL;
210 }
211
212 ESIStreamContext *
ESIStreamContextNew(ESIIncludePtr include)213 ESIStreamContextNew (ESIIncludePtr include)
214 {
215 ESIStreamContext *rv = new ESIStreamContext;
216 rv->include = include;
217 return rv;
218 }
219
220 /* ESIInclude */
~ESIInclude()221 ESIInclude::~ESIInclude()
222 {
223 debugs(86, 5, "ESIInclude::Free " << this);
224 ESISegmentFreeList (srccontent);
225 ESISegmentFreeList (altcontent);
226 cbdataReferenceDone (varState);
227 safe_free (srcurl);
228 safe_free (alturl);
229 }
230
231 void
finish()232 ESIInclude::finish()
233 {
234 parent = NULL;
235 }
236
237 ESIElement::Pointer
makeCacheable() const238 ESIInclude::makeCacheable() const
239 {
240 return new ESIInclude (*this);
241 }
242
243 ESIElement::Pointer
makeUsable(esiTreeParentPtr newParent,ESIVarState & newVarState) const244 ESIInclude::makeUsable(esiTreeParentPtr newParent, ESIVarState &newVarState) const
245 {
246 ESIInclude *resultI = new ESIInclude (*this);
247 ESIElement::Pointer result = resultI;
248 resultI->parent = newParent;
249 resultI->varState = cbdataReference (&newVarState);
250
251 if (resultI->srcurl)
252 resultI->src = ESIStreamContextNew (resultI);
253
254 if (resultI->alturl)
255 resultI->alt = ESIStreamContextNew (resultI);
256
257 return result;
258 }
259
ESIInclude(ESIInclude const & old)260 ESIInclude::ESIInclude(ESIInclude const &old) :
261 varState(NULL),
262 srcurl(NULL),
263 alturl(NULL),
264 parent(NULL),
265 started(false),
266 sent(false)
267 {
268 memset(&flags, 0, sizeof(flags));
269 flags.onerrorcontinue = old.flags.onerrorcontinue;
270
271 if (old.srcurl)
272 srcurl = xstrdup(old.srcurl);
273
274 if (old.alturl)
275 alturl = xstrdup(old.alturl);
276 }
277
278 void
prepareRequestHeaders(HttpHeader & tempheaders,ESIVarState * vars)279 ESIInclude::prepareRequestHeaders(HttpHeader &tempheaders, ESIVarState *vars)
280 {
281 tempheaders.update(&vars->header());
282 tempheaders.removeHopByHopEntries();
283 }
284
285 void
Start(ESIStreamContext::Pointer stream,char const * url,ESIVarState * vars)286 ESIInclude::Start (ESIStreamContext::Pointer stream, char const *url, ESIVarState *vars)
287 {
288 if (!stream.getRaw())
289 return;
290
291 HttpHeader tempheaders(hoRequest);
292
293 prepareRequestHeaders(tempheaders, vars);
294
295 /* Ensure variable state is clean */
296 vars->feedData(url, strlen (url));
297
298 /* tempUrl is eaten by the request */
299 char const *tempUrl = vars->extractChar ();
300
301 debugs(86, 5, "ESIIncludeStart: Starting subrequest with url '" << tempUrl << "'");
302 const MasterXaction::Pointer mx = new MasterXaction(XactionInitiator::initEsi);
303 if (clientBeginRequest(Http::METHOD_GET, tempUrl, esiBufferRecipient, esiBufferDetach, stream.getRaw(), &tempheaders, stream->localbuffer->buf, HTTP_REQBUF_SZ, mx)) {
304 debugs(86, DBG_CRITICAL, "starting new ESI subrequest failed");
305 }
306
307 tempheaders.clean();
308 }
309
ESIInclude(esiTreeParentPtr aParent,int attrcount,char const ** attr,ESIContext * aContext)310 ESIInclude::ESIInclude(esiTreeParentPtr aParent, int attrcount, char const **attr, ESIContext *aContext) :
311 varState(NULL),
312 srcurl(NULL),
313 alturl(NULL),
314 parent(aParent),
315 started(false),
316 sent(false)
317 {
318 assert (aContext);
319 memset(&flags, 0, sizeof(flags));
320
321 for (int i = 0; i < attrcount && attr[i]; i += 2) {
322 if (!strcmp(attr[i],"src")) {
323 /* Start a request for thisNode url */
324 debugs(86, 5, "ESIIncludeNew: Requesting source '" << attr[i+1] << "'");
325
326 /* TODO: don't assert on thisNode, ignore the duplicate */
327 assert (src.getRaw() == NULL);
328 src = ESIStreamContextNew (this);
329 assert (src.getRaw() != NULL);
330 srcurl = xstrdup(attr[i+1]);
331 } else if (!strcmp(attr[i],"alt")) {
332 /* Start a secondary request for thisNode url */
333 /* TODO: make a config parameter to wait on requesting alt's
334 * for the src to fail
335 */
336 debugs(86, 5, "ESIIncludeNew: Requesting alternate '" << attr[i+1] << "'");
337
338 assert (alt.getRaw() == NULL); /* TODO: FIXME */
339 alt = ESIStreamContextNew (this);
340 assert (alt.getRaw() != NULL);
341 alturl = xstrdup(attr[i+1]);
342 } else if (!strcmp(attr[i],"onerror")) {
343 if (!strcmp(attr[i+1], "continue")) {
344 flags.onerrorcontinue = 1;
345 } else {
346 /* ignore mistyped attributes */
347 debugs(86, DBG_IMPORTANT, "invalid value for onerror='" << attr[i+1] << "'");
348 }
349 } else {
350 /* ignore mistyped attributes. TODO:? error on these for user feedback - config parameter needed
351 */
352 }
353 }
354
355 varState = cbdataReference(aContext->varState);
356 }
357
358 void
start()359 ESIInclude::start()
360 {
361 /* prevent freeing ourselves */
362 ESIIncludePtr foo(this);
363
364 if (started)
365 return;
366
367 started = true;
368
369 if (src.getRaw()) {
370 Start (src, srcurl, varState);
371 Start (alt, alturl, varState);
372 } else {
373 alt = NULL;
374
375 debugs(86, DBG_IMPORTANT, "ESIIncludeNew: esi:include with no src attributes");
376
377 flags.failed = 1;
378 }
379 }
380
381 void
render(ESISegment::Pointer output)382 ESIInclude::render(ESISegment::Pointer output)
383 {
384 if (sent)
385 return;
386
387 ESISegment::Pointer myout;
388
389 debugs(86, 5, "ESIIncludeRender: Rendering include " << this);
390
391 assert (flags.finished || (flags.failed && flags.onerrorcontinue));
392
393 if (flags.failed && flags.onerrorcontinue) {
394 return;
395 }
396
397 /* Render the content */
398 if (srccontent.getRaw()) {
399 myout = srccontent;
400 srccontent = NULL;
401 } else if (altcontent.getRaw()) {
402 myout = altcontent;
403 altcontent = NULL;
404 } else
405 fatal ("ESIIncludeRender called with no content, and no failure!\n");
406
407 assert (output->next == NULL);
408
409 output->next = myout;
410
411 sent = true;
412 }
413
414 esiProcessResult_t
process(int dovars)415 ESIInclude::process (int dovars)
416 {
417 /* Prevent refcount race leading to free */
418 Pointer me (this);
419 start();
420 debugs(86, 5, "ESIIncludeRender: Processing include " << this);
421
422 if (flags.failed) {
423 if (flags.onerrorcontinue)
424 return ESI_PROCESS_COMPLETE;
425 else
426 return ESI_PROCESS_FAILED;
427 }
428
429 if (!flags.finished) {
430 if (flags.onerrorcontinue)
431 return ESI_PROCESS_PENDING_WONTFAIL;
432 else
433 return ESI_PROCESS_PENDING_MAYFAIL;
434 }
435
436 return ESI_PROCESS_COMPLETE;
437 }
438
439 void
includeFail(ESIStreamContext::Pointer stream)440 ESIInclude::includeFail (ESIStreamContext::Pointer stream)
441 {
442 subRequestDone (stream, false);
443 }
444
445 bool
dataNeeded() const446 ESIInclude::dataNeeded() const
447 {
448 return !(flags.finished || flags.failed);
449 }
450
451 void
subRequestDone(ESIStreamContext::Pointer stream,bool success)452 ESIInclude::subRequestDone (ESIStreamContext::Pointer stream, bool success)
453 {
454 if (!dataNeeded())
455 return;
456
457 if (stream == src) {
458 debugs(86, 3, "ESIInclude::subRequestDone: " << srcurl);
459
460 if (success) {
461 /* copy the lead segment */
462 debugs(86, 3, "ESIIncludeSubRequestDone: Src OK - include PASSED.");
463 assert (!srccontent.getRaw());
464 ESISegment::ListTransfer (stream->localbuffer, srccontent);
465 /* we're done! */
466 flags.finished = 1;
467 } else {
468 /* Fail if there is no alt being retrieved */
469 debugs(86, 3, "ESIIncludeSubRequestDone: Src FAILED");
470
471 if (!(alt.getRaw() || altcontent.getRaw())) {
472 debugs(86, 3, "ESIIncludeSubRequestDone: Include FAILED - No ALT");
473 flags.failed = 1;
474 } else if (altcontent.getRaw()) {
475 debugs(86, 3, "ESIIncludeSubRequestDone: Include PASSED - ALT already Complete");
476 /* ALT was already retrieved, we are done */
477 flags.finished = 1;
478 }
479 }
480
481 src = NULL;
482 } else if (stream == alt) {
483 debugs(86, 3, "ESIInclude::subRequestDone: " << alturl);
484
485 if (success) {
486 debugs(86, 3, "ESIIncludeSubRequestDone: ALT OK.");
487 /* copy the lead segment */
488 assert (!altcontent.getRaw());
489 ESISegment::ListTransfer (stream->localbuffer, altcontent);
490 /* we're done! */
491
492 if (!(src.getRaw() || srccontent.getRaw())) {
493 /* src already failed, kick ESI processor */
494 debugs(86, 3, "ESIIncludeSubRequestDone: Include PASSED - SRC already failed.");
495 flags.finished = 1;
496 }
497 } else {
498 if (!(src.getRaw() || srccontent.getRaw())) {
499 debugs(86, 3, "ESIIncludeSubRequestDone: ALT FAILED, Include FAILED - SRC already failed");
500 /* src already failed */
501 flags.failed = 1;
502 }
503 }
504
505 alt = NULL;
506 } else {
507 fatal ("ESIIncludeSubRequestDone: non-owned stream found!\n");
508 }
509
510 if (flags.finished || flags.failed) {
511 /* Kick ESI Processor */
512 debugs (86, 5, "ESIInclude " << this <<
513 " SubRequest " << stream.getRaw() <<
514 " completed, kicking processor , status " <<
515 (flags.finished ? "OK" : "FAILED"));
516 /* There is a race condition - and we have no reproducible test case -
517 * during a subrequest the parent will get set to NULL, which is not
518 * meant to be possible. Rather than killing squid, we let it leak
519 * memory but complain in the log.
520 *
521 * Someone wanting to debug this could well start by running squid with
522 * a hardware breakpoint set to this location.
523 * Its probably due to parent being set to null - by a call to
524 * 'this.finish' while the subrequest is still not completed.
525 */
526 if (parent.getRaw() == NULL) {
527 debugs (86, 0, "ESIInclude::subRequestDone: Sub request completed "
528 "after finish() called and parent unlinked. Unable to "
529 "continue handling the request, and may be memory leaking. "
530 "See http://www.squid-cache.org/bugs/show_bug.cgi?id=951 - we "
531 "are looking for a reproducible test case. This will require "
532 "an ESI template with includes, probably with alt-options, "
533 "and we're likely to need traffic dumps to allow us to "
534 "reconstruct the exact tcp handling sequences to trigger this "
535 "rather elusive bug.");
536 return;
537 }
538 assert (parent.getRaw());
539
540 if (!flags.failed) {
541 sent = true;
542 parent->provideData (srccontent.getRaw() ? srccontent:altcontent,this);
543
544 if (srccontent.getRaw())
545 srccontent = NULL;
546 else
547 altcontent = NULL;
548 } else if (flags.onerrorcontinue) {
549 /* render nothing but inform of completion */
550
551 if (!sent) {
552 sent = true;
553 parent->provideData (new ESISegment, this);
554 } else
555 assert (0);
556 } else
557 parent->fail(this, "esi:include could not be completed.");
558 }
559 }
560
561 #endif /* USE_SQUID_ESI */
562
563