2 * Copyright (C) 1996-2025 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 58 HTTP Reply (Response) */
12 #include "acl/AclSizeLimit.h"
13 #include "acl/FilledChecklist.h"
14 #include "base/EnumIterator.h"
16 #include "http/ContentLengthInterpreter.h"
18 #include "HttpHdrCc.h"
19 #include "HttpHdrContRange.h"
20 #include "HttpHdrSc.h"
21 #include "HttpReply.h"
22 #include "HttpRequest.h"
24 #include "sbuf/Stream.h"
25 #include "SquidConfig.h"
26 #include "SquidMath.h"
30 HttpReply::HttpReply():
31 Http::Message(hoReply
),
35 surrogate_control(nullptr),
39 content_range(nullptr)
44 HttpReply::~HttpReply()
55 pstate
= Http::Message::psReadyToParseStartLine
;
59 void HttpReply::reset()
62 // reset should not reset the protocol; could have made protoPrefix a
63 // virtual function instead, but it is not clear whether virtual methods
64 // are allowed with MEMPROXY_CLASS() and whether some cbdata void*
65 // conversions are not going to kill virtual tables
66 const String pfx
= protoPrefix
;
75 // we used to assert that the pipe is NULL, but now the message only
76 // points to a pipe that is owned and initiated by another object.
83 bodySizeMax
= -2; // hack: make calculatedBodySizeMax() false
87 HttpReply::packHeadersUsingFastPacker(Packable
&p
) const
95 HttpReply::packHeadersUsingSlowPacker(Packable
&p
) const
99 packHeadersUsingFastPacker(buf
);
100 p
.append(buf
.content(), buf
.contentSize());
104 HttpReply::packInto(MemBuf
&buf
) const
106 packHeadersUsingFastPacker(buf
);
110 /* create memBuf, create mem-based packer, pack, destroy packer, return MemBuf */
112 HttpReply::pack() const
114 MemBuf
*mb
= new MemBuf
;
121 HttpReply::MakeConnectionEstablished() {
123 HttpReplyPointer
rep(new HttpReply
);
124 rep
->sline
.set(Http::ProtocolVersion(), Http::scOkay
, "Connection established");
129 HttpReply::make304() const
131 static const Http::HdrType ImsEntries
[] = {Http::HdrType::DATE
, Http::HdrType::CONTENT_TYPE
, Http::HdrType::EXPIRES
, Http::HdrType::LAST_MODIFIED
, /* eof */ Http::HdrType::OTHER
};
133 const HttpReplyPointer
rv(new HttpReply
);
137 /* rv->content_length; */
139 rv
->last_modified
= last_modified
;
140 rv
->expires
= expires
;
141 rv
->content_type
= content_type
;
142 /* rv->content_range */
144 rv
->sline
.set(Http::ProtocolVersion(), Http::scNotModified
, nullptr);
146 for (t
= 0; ImsEntries
[t
] != Http::HdrType::OTHER
; ++t
) {
147 if ((e
= header
.findEntry(ImsEntries
[t
])))
148 rv
->header
.addEntry(e
->clone());
152 rv
->putCc(*cache_control
);
159 HttpReply::packed304Reply() const
161 /* Not as efficient as skipping the header duplication,
162 * but easier to maintain
164 const auto temp
= make304();
165 MemBuf
*rv
= temp
->pack();
170 HttpReply::setHeaders(Http::StatusCode status
, const char *reason
,
171 const char *ctype
, int64_t clen
, time_t lmt
, time_t expiresTime
)
174 sline
.set(Http::ProtocolVersion(), status
, reason
);
176 hdr
->putStr(Http::HdrType::SERVER
, visible_appname_string
);
177 hdr
->putStr(Http::HdrType::MIME_VERSION
, "1.0");
178 hdr
->putTime(Http::HdrType::DATE
, squid_curtime
);
181 hdr
->putStr(Http::HdrType::CONTENT_TYPE
, ctype
);
182 content_type
= ctype
;
184 content_type
= String();
187 hdr
->putInt64(Http::HdrType::CONTENT_LENGTH
, clen
);
189 if (expiresTime
>= 0)
190 hdr
->putTime(Http::HdrType::EXPIRES
, expiresTime
);
192 if (lmt
> 0) /* this used to be lmt != 0 @?@ */
193 hdr
->putTime(Http::HdrType::LAST_MODIFIED
, lmt
);
195 date
= squid_curtime
;
197 content_length
= clen
;
199 expires
= expiresTime
;
205 HttpReply::redirect(Http::StatusCode status
, const char *loc
)
208 sline
.set(Http::ProtocolVersion(), status
, nullptr);
210 hdr
->putStr(Http::HdrType::SERVER
, visible_appname_string
);
211 hdr
->putTime(Http::HdrType::DATE
, squid_curtime
);
212 hdr
->putInt64(Http::HdrType::CONTENT_LENGTH
, 0);
213 hdr
->putStr(Http::HdrType::LOCATION
, loc
);
214 date
= squid_curtime
;
218 /* compare the validators of two replies.
220 * 0 = they do not match
223 HttpReply::validatorsMatch(HttpReply
const * otherRep
) const
227 /* Numbers first - easiest to check */
229 /* TODO: remove -1 bypass */
231 if (content_length
!= otherRep
->content_length
232 && content_length
> -1 &&
233 otherRep
->content_length
> -1)
237 one
= header
.getStrOrList(Http::HdrType::ETAG
);
239 two
= otherRep
->header
.getStrOrList(Http::HdrType::ETAG
);
241 if (one
.size()==0 || two
.size()==0 || one
.caseCmp(two
)!=0 ) {
247 if (last_modified
!= otherRep
->last_modified
)
251 one
= header
.getStrOrList(Http::HdrType::CONTENT_MD5
);
253 two
= otherRep
->header
.getStrOrList(Http::HdrType::CONTENT_MD5
);
255 if (one
.size()==0 || two
.size()==0 || one
.caseCmp(two
)!=0 ) {
265 HttpReply::recreateOnNotModified(const HttpReply
&reply304
) const
267 // If enough 304s do not update, then this expensive checking is cheaper
268 // than blindly storing reply prefix identical to the already stored one.
269 if (!header
.needUpdate(&reply304
.header
))
272 const Pointer cloned
= clone();
273 cloned
->header
.update(&reply304
.header
);
274 cloned
->hdrCacheClean();
275 cloned
->header
.compact();
276 cloned
->hdrCacheInit();
280 /* internal routines */
283 HttpReply::hdrExpirationTime()
285 /* The s-maxage and max-age directive takes priority over Expires */
290 * Conservatively handle the case when we have a max-age
291 * header, but no Date for reference?
293 if (cache_control
->hasSMaxAge(&maxAge
) || cache_control
->hasMaxAge(&maxAge
))
294 return (date
>= 0) ? date
+ maxAge
: squid_curtime
;
297 if (Config
.onoff
.vary_ignore_expire
&&
298 header
.has(Http::HdrType::VARY
)) {
299 const time_t d
= header
.getTime(Http::HdrType::DATE
);
300 const time_t e
= header
.getTime(Http::HdrType::EXPIRES
);
306 if (header
.has(Http::HdrType::EXPIRES
)) {
307 const time_t e
= header
.getTime(Http::HdrType::EXPIRES
);
309 * HTTP/1.0 says that robust implementations should consider
310 * bad or malformed Expires header as equivalent to "expires
313 return e
< 0 ? squid_curtime
: e
;
319 /* sync this routine when you update HttpReply struct */
321 HttpReply::hdrCacheInit()
323 Http::Message::hdrCacheInit();
325 http_ver
= sline
.version
;
326 content_length
= header
.getInt64(Http::HdrType::CONTENT_LENGTH
);
327 date
= header
.getTime(Http::HdrType::DATE
);
328 last_modified
= header
.getTime(Http::HdrType::LAST_MODIFIED
);
329 surrogate_control
= header
.getSc();
330 content_range
= (sline
.status() == Http::scPartialContent
) ?
331 header
.getContRange() : nullptr;
332 keep_alive
= persistent() ? 1 : 0;
333 const char *str
= header
.getStr(Http::HdrType::CONTENT_TYPE
);
336 content_type
.assign(str
, strcspn(str
, ";\t "));
338 content_type
= String();
340 /* be sure to set expires after date and cache-control */
341 expires
= hdrExpirationTime();
344 const HttpHdrContRange
*
345 HttpReply::contentRange() const
347 assert(!content_range
|| sline
.status() == Http::scPartialContent
);
348 return content_range
;
351 /* sync this routine when you update HttpReply struct */
353 HttpReply::hdrCacheClean()
355 content_type
.clean();
358 delete cache_control
;
359 cache_control
= nullptr;
362 if (surrogate_control
) {
363 delete surrogate_control
;
364 surrogate_control
= nullptr;
368 delete content_range
;
369 content_range
= nullptr;
374 * Returns the body size of a HTTP response
377 HttpReply::bodySize(const HttpRequestMethod
& method
) const
379 if (sline
.version
.major
< 1)
381 else if (method
.id() == Http::METHOD_HEAD
)
383 else if (sline
.status() == Http::scOkay
)
384 (void) 0; /* common case, continue */
385 else if (sline
.status() == Http::scNoContent
)
387 else if (sline
.status() == Http::scNotModified
)
389 else if (sline
.status() < Http::scOkay
)
392 return content_length
;
396 * Checks the first line of an HTTP Reply is valid.
397 * currently only checks "HTTP/" exists.
399 * NP: not all error cases are detected yet. Some are left for detection later in parse.
402 HttpReply::sanityCheckStartLine(const char *buf
, const size_t hdr_len
, Http::StatusCode
*error
)
404 // hack warning: using psize instead of size here due to type mismatches with MemBuf.
406 // content is long enough to possibly hold a reply
407 // 4 being magic size of a 3-digit number plus space delimiter
408 if (hdr_len
< (size_t)(protoPrefix
.psize() + 4)) {
410 debugs(58, 3, "Too small reply header (" << hdr_len
<< " bytes)");
411 *error
= Http::scInvalidHeader
;
417 // catch missing or mismatched protocol identifier
418 // allow special-case for ICY protocol (non-HTTP identifier) in response to faked HTTP request.
419 if (strncmp(buf
, "ICY", 3) == 0) {
421 pos
= protoPrefix
.psize();
424 if (protoPrefix
.cmp(buf
, protoPrefix
.size()) != 0) {
425 debugs(58, 3, "missing protocol prefix (" << protoPrefix
<< ") in '" << buf
<< "'");
426 *error
= Http::scInvalidHeader
;
430 // catch missing or negative status value (negative '-' is not a digit)
431 pos
= protoPrefix
.psize();
433 // skip arbitrary number of digits and a dot in the version portion
434 while ((size_t)pos
<= hdr_len
&& (*(buf
+pos
) == '.' || xisdigit(*(buf
+pos
)) ) ) ++pos
;
436 // catch missing version info
437 if (pos
== protoPrefix
.psize()) {
438 debugs(58, 3, "missing protocol version numbers (ie. " << protoPrefix
<< "/1.0) in '" << buf
<< "'");
439 *error
= Http::scInvalidHeader
;
444 // skip arbitrary number of spaces...
445 while ((size_t)pos
<= hdr_len
&& (char)*(buf
+pos
) == ' ') ++pos
;
447 if ((size_t)pos
< hdr_len
&& !xisdigit(*(buf
+pos
))) {
448 debugs(58, 3, "missing or invalid status number in '" << buf
<< "'");
449 *error
= Http::scInvalidHeader
;
457 HttpReply::parseFirstLine(const char *blk_start
, const char *blk_end
)
459 return sline
.parse(protoPrefix
, blk_start
, blk_end
);
463 HttpReply::parseTerminatedPrefix(const char * const terminatedBuf
, const size_t bufSize
)
465 auto error
= Http::scNone
;
466 const bool eof
= false; // TODO: Remove after removing atEnd from HttpHeader::parse()
467 if (parse(terminatedBuf
, bufSize
, eof
, &error
)) {
468 debugs(58, 7, "success after accumulating " << bufSize
<< " bytes and parsing " << hdr_sz
);
469 Assure(pstate
== Http::Message::psParsed
);
471 Assure(!Less(bufSize
, hdr_sz
)); // cannot parse more bytes than we have
472 return hdr_sz
; // success
475 Assure(pstate
!= Http::Message::psParsed
);
479 throw TextException(ToSBuf("failed to parse HTTP headers",
480 Debug::Extra
, "parser error code: ", error
,
481 Debug::Extra
, "accumulated unparsed bytes: ", bufSize
,
482 Debug::Extra
, "reply_header_max_size: ", Config
.maxReplyHeaderSize
),
486 debugs(58, 3, "need more bytes after accumulating " << bufSize
<< " out of " << Config
.maxReplyHeaderSize
);
488 // the parse() call above enforces Config.maxReplyHeaderSize limit
489 // XXX: Make this a strict comparison after fixing Http::Message::parse() enforcement
490 Assure(bufSize
<= Config
.maxReplyHeaderSize
);
491 return 0; // parsed nothing, need more data
495 HttpReply::prefixLen() const
497 return sline
.packedLength() + header
.len
+ 2;
501 HttpReply::configureContentLengthInterpreter(Http::ContentLengthInterpreter
&interpreter
)
503 interpreter
.applyStatusCodeRules(sline
.status());
507 HttpReply::parseHeader(Http1::Parser
&hp
)
509 Http::ContentLengthInterpreter clen
;
510 return Message::parseHeader(hp
, clen
);
513 /* handy: resets and returns -1 */
515 HttpReply::httpMsgParseError()
517 int result(Http::Message::httpMsgParseError());
518 /* indicate an error in the status line */
519 sline
.set(Http::ProtocolVersion(), Http::scInvalidHeader
);
524 * Indicate whether or not we would usually expect an entity-body
525 * along with this response
528 HttpReply::expectingBody(const HttpRequestMethod
& req_method
, int64_t& theSize
) const
530 bool expectBody
= true;
532 if (req_method
== Http::METHOD_HEAD
)
534 else if (sline
.status() == Http::scNoContent
)
536 else if (sline
.status() == Http::scNotModified
)
538 // TODO: Consider assuming that gray-area 0xx responses have bodies, like 9xx responses.
539 else if (sline
.status() < Http::scOkay
)
545 if (header
.chunked())
547 else if (content_length
>= 0)
548 theSize
= content_length
;
557 HttpReply::receivedBodyTooLarge(HttpRequest
& request
, int64_t receivedSize
)
559 calcMaxBodySize(request
);
560 debugs(58, 3, receivedSize
<< " >? " << bodySizeMax
);
561 return bodySizeMax
>= 0 && receivedSize
> bodySizeMax
;
565 HttpReply::expectedBodyTooLarge(HttpRequest
& request
)
567 calcMaxBodySize(request
);
568 debugs(58, 7, "bodySizeMax=" << bodySizeMax
);
570 if (bodySizeMax
< 0) // no body size limit
573 int64_t expectedSize
= -1;
574 if (!expectingBody(request
.method
, expectedSize
))
577 debugs(58, 6, expectedSize
<< " >? " << bodySizeMax
);
579 if (expectedSize
< 0) // expecting body of an unknown length
582 return expectedSize
> bodySizeMax
;
586 HttpReply::calcMaxBodySize(HttpRequest
& request
) const
588 // hack: -2 is used as "we have not calculated max body size yet" state
589 if (bodySizeMax
!= -2) // already tried
593 // short-circuit ACL testing if there are none configured
594 if (!Config
.ReplyBodySize
)
597 ACLFilledChecklist
ch(nullptr, &request
);
598 ch
.updateReply(this);
599 for (AclSizeLimit
*l
= Config
.ReplyBodySize
; l
; l
= l
-> next
) {
600 /* if there is no ACL list or if the ACLs listed match use this size value */
601 if (!l
->aclList
|| ch
.fastCheck(l
->aclList
).allowed()) {
602 debugs(58, 4, "bodySizeMax=" << bodySizeMax
);
603 bodySizeMax
= l
->size
; // may be -1
609 // XXX: check that this is sufficient for eCAP cloning
611 HttpReply::clone() const
613 HttpReply
*rep
= new HttpReply();
614 rep
->sline
= sline
; // used in hdrCacheInit() call below
615 rep
->header
.append(&header
);
617 rep
->hdr_sz
= hdr_sz
;
618 rep
->http_ver
= http_ver
;
619 rep
->pstate
= pstate
;
620 rep
->body_pipe
= body_pipe
;
622 // keep_alive is handled in hdrCacheInit()
627 HttpReply::inheritProperties(const Http::Message
*aMsg
)
629 const HttpReply
*aRep
= dynamic_cast<const HttpReply
*>(aMsg
);
632 keep_alive
= aRep
->keep_alive
;
633 sources
= aRep
->sources
;
638 HttpReply::olderThan(const HttpReply
*them
) const
640 if (!them
|| !them
->date
|| !date
)
642 return date
< them
->date
;
646 HttpReply::removeIrrelevantContentLength() {
647 if (Http::ProhibitsContentLength(sline
.status()))
648 if (header
.delById(Http::HdrType::CONTENT_LENGTH
))
649 debugs(58, 3, "Removing unexpected Content-Length header");