1 /* $Id: ncbicgir.cpp 506589 2016-07-08 18:45:26Z grichenk $
2 * ===========================================================================
3 *
4 * PUBLIC DOMAIN NOTICE
5 * National Center for Biotechnology Information
6 *
7 * This software/database is a "United States Government Work" under the
8 * terms of the United States Copyright Act. It was written as part of
9 * the author's official duties as a United States Government employee and
10 * thus cannot be copyrighted. This software/database is freely available
11 * to the public for use. The National Library of Medicine and the U.S.
12 * Government have not placed any restriction on its use or reproduction.
13 *
14 * Although all reasonable efforts have been taken to ensure the accuracy
15 * and reliability of the software and data, the NLM and the U.S.
16 * Government do not and cannot warrant the performance or results that
17 * may be obtained by using this software or data. The NLM and the U.S.
18 * Government disclaim all warranties, express or implied, including
19 * warranties of performance, merchantability or fitness for any particular
20 * purpose.
21 *
22 * Please cite the author in any work or product based on this material.
23 *
24 * ===========================================================================
25 *
26 * Authors: Eugene Vasilchenko, Denis Vakatov
27 *
28 * File Description:
29 * CCgiResponse -- CGI response generator class
30 *
31 */
32
33
34 #include <ncbi_pch.hpp>
35 #include <corelib/request_ctx.hpp>
36 #include <cgi/ncbicgir.hpp>
37 #include <cgi/cgi_exception.hpp>
38 #include <cgi/cgi_session.hpp>
39 #include <cgi/cgictx.hpp>
40 #include <cgi/cgiapp.hpp>
41 #include <corelib/ncbi_safe_static.hpp>
42 #include <cgi/error_codes.hpp>
43 #include <time.h>
44
45 #ifdef HAVE_UNISTD_H
46 # include <unistd.h>
47 #else
48 # define STDOUT_FILENO 1
49 #endif
50
51
52 #define NCBI_USE_ERRCODE_X Cgi_Response
53
54
55 BEGIN_NCBI_SCOPE
56
57
58 const char* CCgiResponse::sm_ContentTypeName = "Content-Type";
59 const char* CCgiResponse::sm_LocationName = "Location";
60 const char* CCgiResponse::sm_ContentTypeDefault = "text/html";
61 const char* CCgiResponse::sm_ContentTypeMixed = "multipart/mixed";
62 const char* CCgiResponse::sm_ContentTypeRelated = "multipart/related";
63 const char* CCgiResponse::sm_ContentTypeXMR = "multipart/x-mixed-replace";
64 const char* CCgiResponse::sm_ContentDispoName = "Content-Disposition";
65 const char* CCgiResponse::sm_FilenamePrefix = "attachment; filename=\"";
66 const char* CCgiResponse::sm_HTTPStatusName = "Status";
67 const char* CCgiResponse::sm_HTTPStatusDefault = "200 OK";
68 const char* CCgiResponse::sm_BoundaryPrefix = "NCBI_CGI_Boundary_";
69 const char* CCgiResponse::sm_CacheControl = "Cache-Control";
70 const char* CCgiResponse::sm_AcceptRanges = "Accept-Ranges";
71 const char* CCgiResponse::sm_AcceptRangesBytes = "bytes";
72 const char* CCgiResponse::sm_ContentRange = "Content-Range";
73
74 NCBI_PARAM_DEF_IN_SCOPE(bool, CGI, ThrowOnBadOutput, true, CCgiResponse);
75 NCBI_PARAM_DEF_IN_SCOPE(bool, CGI, ExceptionAfterHEAD, false, CCgiResponse);
76
77
s_ZeroTime(const tm & date)78 inline bool s_ZeroTime(const tm& date)
79 {
80 static const tm kZeroTime = { 0 };
81 return ::memcmp(&date, &kZeroTime, sizeof(tm)) == 0;
82 }
83
84
CCgiResponse(CNcbiOstream * os,int ofd)85 CCgiResponse::CCgiResponse(CNcbiOstream* os, int ofd)
86 : m_IsRawCgi(false),
87 m_IsMultipart(eMultipart_none),
88 m_BetweenParts(false),
89 m_Output(NULL),
90 m_OutputFD(0),
91 m_HeaderWritten(false),
92 m_RequireWriteHeader(true),
93 m_RequestMethod(CCgiRequest::eMethod_Other),
94 m_Session(NULL),
95 m_DisableTrackingCookie(false),
96 m_Request(0),
97 m_ChunkedTransfer(false)
98 {
99 SetOutput(os ? os : &NcbiCout,
100 os ? ofd : STDOUT_FILENO // "os" on this line is NOT a typo
101 );
102 }
103
104
~CCgiResponse(void)105 CCgiResponse::~CCgiResponse(void)
106 {
107 x_RestoreOutputExceptions();
108 }
109
110
HaveHeaderValue(const string & name) const111 bool CCgiResponse::HaveHeaderValue(const string& name) const
112 {
113 return m_HeaderValues.find(name) != m_HeaderValues.end();
114 }
115
116
GetHeaderValue(const string & name) const117 string CCgiResponse::GetHeaderValue(const string &name) const
118 {
119 TMap::const_iterator ptr = m_HeaderValues.find(name);
120
121 return (ptr == m_HeaderValues.end()) ? kEmptyStr : ptr->second;
122 }
123
124
RemoveHeaderValue(const string & name)125 void CCgiResponse::RemoveHeaderValue(const string& name)
126 {
127 m_HeaderValues.erase(name);
128 }
129
130
x_ValidateHeader(const string & name,const string & value) const131 bool CCgiResponse::x_ValidateHeader(const string& name, const string& value) const
132 {
133 // Very basic validation of names - prohibit CR/LF.
134 if (name.find("\n", 0) != NPOS) {
135 return false;
136 }
137 // Values may contain [CR/]LF, but only if followed by space or tab.
138 size_t pos = value.find("\n", 0);
139 while (pos != NPOS) {
140 ++pos;
141 if (pos >= value.size()) break;
142 if (value[pos] != ' ' && value[pos] != '\t') {
143 return false;
144 }
145 pos = value.find("\n", pos);
146 }
147 return true;
148 }
149
150
SetHeaderValue(const string & name,const string & value)151 void CCgiResponse::SetHeaderValue(const string& name, const string& value)
152 {
153 if ( value.empty() ) {
154 RemoveHeaderValue(name);
155 } else {
156 if ( x_ValidateHeader(name, value) ) {
157 m_HeaderValues[name] = value;
158 }
159 else {
160 NCBI_THROW(CCgiResponseException, eBadHeaderValue,
161 "CCgiResponse::SetHeaderValue() -- "
162 "invalid header name or value: " +
163 name + "=" + value);
164 }
165 }
166 }
167
168
SetHeaderValue(const string & name,const struct tm & date)169 void CCgiResponse::SetHeaderValue(const string& name, const struct tm& date)
170 {
171 if ( s_ZeroTime(date) ) {
172 RemoveHeaderValue(name);
173 return;
174 }
175
176 char buff[64];
177 if ( !::strftime(buff, sizeof(buff),
178 "%a, %d %b %Y %H:%M:%S GMT", &date) ) {
179 NCBI_THROW(CCgiErrnoException, eErrno,
180 "CCgiResponse::SetHeaderValue() -- strftime() failed");
181 }
182 SetHeaderValue(name, buff);
183 }
184
185
SetHeaderValue(const string & name,const CTime & date)186 void CCgiResponse::SetHeaderValue(const string& name, const CTime& date)
187 {
188 if ( date.IsEmpty()) {
189 RemoveHeaderValue(name);
190 return;
191 }
192 SetHeaderValue(name,
193 date.GetGmtTime().AsString("w, D b Y h:m:s") + " GMT");
194 }
195
196
SetStatus(unsigned int code,const string & reason)197 void CCgiResponse::SetStatus(unsigned int code, const string& reason)
198 {
199 if (code < 100) {
200 THROW1_TRACE(runtime_error,
201 "CCgiResponse::SetStatus() -- code too small, below 100");
202 }
203 if (code > 999) {
204 THROW1_TRACE(runtime_error,
205 "CCgiResponse::SetStatus() -- code too big, exceeds 999");
206 }
207 SetHeaderValue(sm_HTTPStatusName, NStr::UIntToString(code) + ' ' +
208 (reason.empty() ?
209 CCgiException::GetStdStatusMessage(CCgiException::EStatusCode(code))
210 : reason));
211 CDiagContext::GetRequestContext().SetRequestStatus(code);
212 }
213
214
x_RestoreOutputExceptions(void)215 void CCgiResponse::x_RestoreOutputExceptions(void)
216 {
217 if (m_Output && m_ThrowOnBadOutput.Get()) {
218 m_Output->exceptions(m_OutputExpt);
219 }
220 }
221
222
SetOutput(CNcbiOstream * output,int fd)223 void CCgiResponse::SetOutput(CNcbiOstream* output, int fd)
224 {
225 x_RestoreOutputExceptions();
226
227 m_HeaderWritten = false;
228 m_Output = output;
229 m_OutputFD = fd;
230
231 // Make the output stream to throw on write if it's in a bad state
232 if (m_Output && m_ThrowOnBadOutput.Get()) {
233 m_OutputExpt = m_Output->exceptions();
234 m_Output->exceptions(IOS_BASE::badbit | IOS_BASE::failbit);
235 }
236 }
237
238
GetOutput(void) const239 CNcbiOstream* CCgiResponse::GetOutput(void) const
240 {
241 bool client_int_ok = TClientConnIntOk::GetDefault() ||
242 (AcceptRangesBytes() && !HaveContentRange());
243
244 if (m_Output &&
245 !client_int_ok &&
246 !(m_RequestMethod == CCgiRequest::eMethod_HEAD && m_HeaderWritten) &&
247 (m_Output->rdstate() & (IOS_BASE::badbit | IOS_BASE::failbit)) != 0 &&
248 m_ThrowOnBadOutput.Get()) {
249 ERR_POST_X(1, Severity(TClientConnIntSeverity::GetDefault()) <<
250 "CCgiResponse::GetOutput() -- output stream is in bad state");
251 const_cast<CCgiResponse*>(this)->SetThrowOnBadOutput(false);
252 }
253 return m_Output;
254 }
255
256
out(void) const257 CNcbiOstream& CCgiResponse::out(void) const
258 {
259 if ( !m_Output ) {
260 THROW1_TRACE(runtime_error, "CCgiResponse::out() on NULL out.stream");
261 }
262 return *GetOutput();
263 }
264
265
WriteHeader(CNcbiOstream & os) const266 CNcbiOstream& CCgiResponse::WriteHeader(CNcbiOstream& os) const
267 {
268 if (&os == m_Output) {
269 if (m_HeaderWritten) {
270 NCBI_THROW(CCgiResponseException, eDoubleHeader,
271 "CCgiResponse::WriteHeader() -- called more than once");
272 }
273 else {
274 m_HeaderWritten = true;
275 }
276 }
277
278 // HTTP status line (if "raw CGI" response)
279 bool skip_status = false;
280 if ( IsRawCgi() ) {
281 string status = GetHeaderValue(sm_HTTPStatusName);
282 if ( status.empty() ) {
283 status = sm_HTTPStatusDefault;
284 } else {
285 skip_status = true; // filter out the status from the HTTP header
286 }
287 os << "HTTP/1.1 " << status << HTTP_EOL;
288 }
289
290 if (m_IsMultipart != eMultipart_none
291 && CCgiUserAgent().GetEngine() == CCgiUserAgent::eEngine_IE) {
292 // MSIE requires multipart responses to start with these extra
293 // headers, which confuse other browsers. :-/
294 os << sm_ContentTypeName << ": message/rfc822" << HTTP_EOL << HTTP_EOL
295 << "Mime-Version: 1.0" << HTTP_EOL;
296 }
297
298 // Dirty hack (JQuery JSONP for browsers that don't support CORS)
299 if ( !m_JQuery_Callback.empty() ) {
300 CCgiResponse* self = const_cast<CCgiResponse*>(this);
301 if (m_IsMultipart == eMultipart_none)
302 self->SetHeaderValue(sm_ContentTypeName, "text/javascript");
303 else
304 self->m_JQuery_Callback.erase();
305 }
306
307 // Default content type (if it's not specified by user already)
308 switch (m_IsMultipart) {
309 case eMultipart_none:
310 if (!HaveHeaderValue(sm_ContentTypeName) &&
311 // Do not set content type if status is '204 No Content'
312 CDiagContext::GetRequestContext().GetRequestStatus() != CRequestStatus::e204_NoContent) {
313 os << sm_ContentTypeName << ": " << sm_ContentTypeDefault
314 << HTTP_EOL;
315 }
316 break;
317 case eMultipart_mixed:
318 os << sm_ContentTypeName << ": " << sm_ContentTypeMixed
319 << "; boundary=" << m_Boundary << HTTP_EOL;
320 break;
321 case eMultipart_related:
322 os << sm_ContentTypeName << ": " << sm_ContentTypeRelated
323 << "; type=" << (HaveHeaderValue(sm_ContentTypeName)
324 ? sm_ContentTypeName : sm_ContentTypeDefault)
325 << "; boundary=" << m_Boundary << HTTP_EOL;
326 break;
327 case eMultipart_replace:
328 os << sm_ContentTypeName << ": " << sm_ContentTypeXMR
329 << "; boundary=" << m_Boundary << HTTP_EOL;
330 break;
331 }
332
333 if (m_Session) {
334 const CCgiCookie* const scookie = m_Session->GetSessionCookie();
335 if (scookie) {
336 const_cast<CCgiResponse*>(this)->m_Cookies.Add(*scookie);
337 }
338 }
339 if (!m_DisableTrackingCookie && m_TrackingCookie.get()) {
340 CCgiResponse* self = const_cast<CCgiResponse*>(this);
341 self->m_Cookies.Add(*m_TrackingCookie);
342 self->SetHeaderValue(TCGI_TrackingTagName::GetDefault(),
343 m_TrackingCookie->GetValue());
344 CRequestContext& rctx = GetDiagContext().GetRequestContext();
345 self->SetHeaderValue("NCBI-PHID", rctx.GetNextSubHitID("m_"));
346 // Prevent storing the page in public caches.
347 string cc = GetHeaderValue(sm_CacheControl);
348 if ( cc.empty() ) {
349 cc = "private";
350 }
351 else {
352 // 'private' already present?
353 if (NStr::FindNoCase(cc, "private") == NPOS) {
354 // no - check for 'public'
355 size_t pos = NStr::FindNoCase(cc, "public");
356 if (pos != NPOS) {
357 ERR_POST_X_ONCE(3, Warning <<
358 "Cache-Control already set to 'public', "
359 "switching to 'private'");
360 NStr::ReplaceInPlace(cc, "public", "private", pos, 1);
361 }
362 else if (NStr::FindNoCase(cc, "no-cache") == NPOS) {
363 cc.append(", private");
364 }
365 }
366 }
367 self->SetHeaderValue(sm_CacheControl, cc);
368 }
369
370 // Cookies (if any)
371 if ( !m_Cookies.Empty() ) {
372 os << m_Cookies;
373 }
374
375 // All header lines (in alphabetical order)
376 ITERATE (TMap, i, m_HeaderValues) {
377 if (skip_status && NStr::EqualNocase(i->first, sm_HTTPStatusName)) {
378 continue;
379 } else if (m_IsMultipart != eMultipart_none
380 && NStr::StartsWith(i->first, "Content-", NStr::eNocase)) {
381 continue;
382 }
383 os << i->first << ": " << i->second << HTTP_EOL;
384 }
385 bool chunked_transfer = GetChunkedTransferEnabled();
386 if ( chunked_transfer ) {
387 // Chunked encoding must be the last one.
388 os << "Transfer-Encoding: chunked" << HTTP_EOL;
389 // Add Trailer if necessary.
390 if ( !m_TrailerValues.empty() ) {
391 string trailer;
392 ITERATE(TMap, it, m_TrailerValues) {
393 if ( !trailer.empty() ) {
394 trailer.append(", ");
395 }
396 trailer.append(it->first);
397 }
398 os << "Trailer: " << trailer << HTTP_EOL;
399 }
400 }
401
402 if (m_IsMultipart != eMultipart_none) { // proceed with first part
403 os << HTTP_EOL << "--" << m_Boundary << HTTP_EOL;
404 if ( !HaveHeaderValue(sm_ContentTypeName) ) {
405 os << sm_ContentTypeName << ": " << sm_ContentTypeDefault
406 << HTTP_EOL;
407 }
408 for (TMap::const_iterator it = m_HeaderValues.lower_bound("Content-");
409 it != m_HeaderValues.end()
410 && NStr::StartsWith(it->first, "Content-", NStr::eNocase);
411 ++it) {
412 os << it->first << ": " << it->second << HTTP_EOL;
413 }
414 }
415
416 // End of header (empty line)
417 os << HTTP_EOL;
418
419 // Dirty hack (JQuery JSONP for browsers that don't support CORS)
420 if ( !m_JQuery_Callback.empty() ) {
421 os << m_JQuery_Callback << '(';
422 }
423
424 CCgiStreamWrapper* wrapper = dynamic_cast<CCgiStreamWrapper*>(m_Output);
425 if ( wrapper ) {
426 if (m_RequestMethod == CCgiRequest::eMethod_HEAD && &os == m_Output) {
427 try {
428 wrapper->SetWriterMode(CCgiStreamWrapper::eBlockWrites);
429 }
430 catch (ios_base::failure&) {
431 }
432 if ( m_ExceptionAfterHEAD.Get() ) {
433 // Optionally stop processing request immediately. The exception
434 // should not be handles by ProcessRequest, but must go up to
435 // the Run() to work correctly.
436 NCBI_CGI_THROW_WITH_STATUS(CCgiHeadException, eHeaderSent,
437 "HEAD response sent.", CCgiException::e200_Ok);
438 }
439 }
440 else if ( chunked_transfer ) {
441 // Chunked encoding is enabled either through the environment,
442 // or due to a header set by the user.
443 try {
444 wrapper->SetWriterMode(CCgiStreamWrapper::eChunkedWrites);
445 }
446 catch (ios_base::failure&) {
447 }
448 }
449 }
450
451 return os;
452 }
453
454
Finalize(void) const455 void CCgiResponse::Finalize(void) const
456 {
457 if (m_RequireWriteHeader && !m_HeaderWritten) {
458 ERR_POST_X(5, "CCgiResponse::WriteHeader() has not been called - "
459 "HTTP header can be missing.");
460 }
461 if (!m_JQuery_Callback.empty() && m_Output && m_HeaderWritten) {
462 *m_Output << ')';
463 }
464 }
465
466
SetFilename(const string & name,size_t size)467 void CCgiResponse::SetFilename(const string& name, size_t size)
468 {
469 string disposition = sm_FilenamePrefix + NStr::PrintableString(name) + '"';
470 if (size > 0) {
471 disposition += "; size=";
472 disposition += NStr::SizetToString(size);
473 }
474 SetHeaderValue(sm_ContentDispoName, disposition);
475 }
476
477
BeginPart(const string & name,const string & type_in,CNcbiOstream & os,size_t size)478 void CCgiResponse::BeginPart(const string& name, const string& type_in,
479 CNcbiOstream& os, size_t size)
480 {
481 _ASSERT(m_IsMultipart != eMultipart_none);
482 if ( !m_BetweenParts ) {
483 os << HTTP_EOL << "--" << m_Boundary << HTTP_EOL;
484 }
485
486 string type = type_in;
487 if (type.empty() /* && m_IsMultipart == eMultipart_replace */) {
488 type = GetHeaderValue(sm_ContentTypeName);
489 }
490 os << sm_ContentTypeName << ": "
491 << (type.empty() ? sm_ContentTypeDefault : type) << HTTP_EOL;
492
493 if ( !name.empty() ) {
494 os << sm_ContentDispoName << ": " << sm_FilenamePrefix
495 << Printable(name) << '"';
496 if (size > 0) {
497 os << "; size=" << size;
498 }
499 os << HTTP_EOL;
500 } else if (m_IsMultipart != eMultipart_replace) {
501 ERR_POST_X(2, Warning << "multipart content contains anonymous part");
502 }
503
504 os << HTTP_EOL;
505 }
506
507
EndPart(CNcbiOstream & os)508 void CCgiResponse::EndPart(CNcbiOstream& os)
509 {
510 _ASSERT(m_IsMultipart != eMultipart_none);
511 if ( !m_BetweenParts ) {
512 os << HTTP_EOL << "--" << m_Boundary << HTTP_EOL << NcbiFlush;
513 }
514 m_BetweenParts = true;
515 }
516
517
EndLastPart(CNcbiOstream & os)518 void CCgiResponse::EndLastPart(CNcbiOstream& os)
519 {
520 _ASSERT(m_IsMultipart != eMultipart_none);
521 os << HTTP_EOL << "--" << m_Boundary << "--" << HTTP_EOL << NcbiFlush;
522 m_IsMultipart = eMultipart_none; // forbid adding more parts
523 }
524
525
Flush(void) const526 void CCgiResponse::Flush(void) const
527 {
528 CNcbiOstream* os = GetOutput();
529 if (!os || !os->good()) {
530 return; // Don't try to flush NULL or broken output
531 }
532 *os << NcbiFlush;
533 }
534
535
SetTrackingCookie(const string & name,const string & value,const string & domain,const string & path,const CTime & exp_time)536 void CCgiResponse::SetTrackingCookie(const string& name, const string& value,
537 const string& domain, const string& path,
538 const CTime& exp_time)
539 {
540 m_TrackingCookie.reset(new CCgiCookie(name, value, domain, path));
541 if ( !exp_time.IsEmpty() ) {
542 m_TrackingCookie->SetExpTime(exp_time);
543 }
544 else {
545 // Set the cookie for one year by default.
546 CTime def_exp(CTime::eCurrent, CTime::eGmt);
547 def_exp.AddYear(1);
548 m_TrackingCookie->SetExpTime(def_exp);
549 }
550 }
551
552
DisableTrackingCookie(void)553 void CCgiResponse::DisableTrackingCookie(void)
554 {
555 m_DisableTrackingCookie = true;
556 }
557
558
SetThrowOnBadOutput(bool throw_on_bad_output)559 void CCgiResponse::SetThrowOnBadOutput(bool throw_on_bad_output)
560 {
561 m_ThrowOnBadOutput.Set(throw_on_bad_output);
562 if (m_Output && throw_on_bad_output) {
563 m_OutputExpt = m_Output->exceptions();
564 m_Output->exceptions(IOS_BASE::badbit | IOS_BASE::failbit);
565 }
566 }
567
568
SetExceptionAfterHEAD(bool expt_after_head)569 void CCgiResponse::SetExceptionAfterHEAD(bool expt_after_head)
570 {
571 m_ExceptionAfterHEAD.Set(expt_after_head);
572 }
573
574
AcceptRangesBytes(void) const575 bool CCgiResponse::AcceptRangesBytes(void) const
576 {
577 string accept_ranges = NStr::TruncateSpaces(GetHeaderValue(sm_AcceptRanges));
578 return NStr::EqualNocase(accept_ranges, sm_AcceptRangesBytes);
579 }
580
581
HaveContentRange(void) const582 bool CCgiResponse::HaveContentRange(void) const
583 {
584 return HaveHeaderValue(sm_ContentRange);
585 }
586
587
InitCORSHeaders(const string &,const string &)588 void CCgiResponse::InitCORSHeaders(const string& /*origin*/,
589 const string& /*jquery_callback*/)
590 {
591 // This method is deprecated and does nothing.
592 // Use CCgiContext::ProcessCORSRequest().
593 }
594
595
SetRetryContext(const CRetryContext & ctx)596 void CCgiResponse::SetRetryContext(const CRetryContext& ctx)
597 {
598 CRetryContext::TValues values;
599 ctx.GetValues(values);
600 ITERATE(CRetryContext::TValues, it, values) {
601 SetHeaderValue(it->first, it->second);
602 }
603 }
604
605
606 enum ECgiChunkedTransfer {
607 eChunked_Default, // Use the hardcoded settings.
608 eChunked_Disable, // Don't use chunked encoding.
609 eChunked_Enable // Enable chunked encoding for HTTP/1.1.
610 };
611
NCBI_PARAM_ENUM_ARRAY(ECgiChunkedTransfer,CGI,ChunkedTransfer)612 NCBI_PARAM_ENUM_ARRAY(ECgiChunkedTransfer, CGI, ChunkedTransfer)
613 {
614 {"Disable", eChunked_Disable},
615 {"Enable", eChunked_Enable},
616 };
617
618 NCBI_PARAM_ENUM_DECL(ECgiChunkedTransfer, CGI, ChunkedTransfer);
619 NCBI_PARAM_ENUM_DEF_EX(ECgiChunkedTransfer, CGI, ChunkedTransfer, eChunked_Default,
620 eParam_NoThread, CGI_CHUNKED_TRANSFER);
621 typedef NCBI_PARAM_TYPE(CGI, ChunkedTransfer) TCGI_ChunkedTransfer;
622
623 NCBI_PARAM_DECL(size_t, CGI, ChunkSize);
624 NCBI_PARAM_DEF_EX(size_t, CGI, ChunkSize, 4096,
625 eParam_NoThread, CGI_CHUNK_SIZE);
626 typedef NCBI_PARAM_TYPE(CGI, ChunkSize) TCGI_ChunkSize;
627
628
GetChunkSize(void)629 size_t CCgiResponse::GetChunkSize(void)
630 {
631 return TCGI_ChunkSize::GetDefault();
632 }
633
634
x_ClientSupportsChunkedTransfer(const CNcbiEnvironment & env)635 bool CCgiResponse::x_ClientSupportsChunkedTransfer(const CNcbiEnvironment& env)
636 {
637 // Auto-enable chunked output for HTTP/1.1.
638 const string& protocol = env.Get("SERVER_PROTOCOL");
639 return !protocol.empty() && !NStr::StartsWith(protocol, "HTTP/1.0", NStr::eNocase);
640 }
641
642
GetChunkedTransferEnabled(void) const643 bool CCgiResponse::GetChunkedTransferEnabled(void) const
644 {
645 switch ( TCGI_ChunkedTransfer::GetDefault() ) {
646 case eChunked_Default:
647 if ( !m_ChunkedTransfer ) return false;
648 break;
649 case eChunked_Disable:
650 return false;
651 default:
652 break;
653 }
654 return m_Request &&
655 x_ClientSupportsChunkedTransfer(m_Request->GetEnvironment());
656 }
657
658
SetChunkedTransferEnabled(bool value)659 void CCgiResponse::SetChunkedTransferEnabled(bool value)
660 {
661 if ( m_HeaderWritten ) {
662 // Ignore attempts to enable chunked transfer if HTTP header
663 // have been written.
664 ERR_POST_X(6, "Attempt to enable chunked transfer after writing "
665 "HTTP header");
666 return;
667 }
668 m_ChunkedTransfer = value;
669 }
670
671
FinishChunkedTransfer(void)672 void CCgiResponse::FinishChunkedTransfer(void)
673 {
674 CCgiStreamWrapper* wrapper = dynamic_cast<CCgiStreamWrapper*>(m_Output);
675 if (wrapper && wrapper->GetWriterMode() == CCgiStreamWrapper::eChunkedWrites) {
676 // Reset wrapper mode to normal if it was chunked. This will write end chunk
677 // and trailer.
678 wrapper->FinishChunkedTransfer(&m_TrailerValues);
679 // Block writes.
680 wrapper->SetWriterMode(CCgiStreamWrapper::eBlockWrites);
681 }
682 }
683
684
AbortChunkedTransfer(void)685 void CCgiResponse::AbortChunkedTransfer(void)
686 {
687 CCgiStreamWrapper* wrapper = dynamic_cast<CCgiStreamWrapper*>(m_Output);
688 if (wrapper && wrapper->GetWriterMode() == CCgiStreamWrapper::eChunkedWrites) {
689 wrapper->AbortChunkedTransfer();
690 }
691 }
692
693
CanSendTrailer(void) const694 bool CCgiResponse::CanSendTrailer(void) const
695 {
696 if (m_HeaderWritten || !GetChunkedTransferEnabled()) return false;
697 if ( !m_TrailerEnabled.get() ) {
698 m_TrailerEnabled.reset(new bool(false));
699 const string& te = m_Request->GetRandomProperty("TE");
700 list<string> parts;
701 NStr::Split(te, " ,", parts, NStr::fSplit_MergeDelimiters | NStr::fSplit_Truncate);
702 ITERATE(list<string>, it, parts) {
703 if (NStr::EqualNocase(*it, "trailers")) {
704 *m_TrailerEnabled = true;
705 break;
706 }
707 }
708 }
709 return *m_TrailerEnabled;
710 }
711
712
AddTrailer(const string & name)713 void CCgiResponse::AddTrailer(const string& name)
714 {
715 if ( !CanSendTrailer() ) return;
716 m_TrailerValues[name] = "";
717 }
718
719
RemoveTrailer(const string & name)720 void CCgiResponse::RemoveTrailer(const string& name)
721 {
722 m_TrailerValues.erase(name);
723 }
724
725
HaveTrailer(const string & name) const726 bool CCgiResponse::HaveTrailer(const string& name) const
727 {
728 return m_TrailerValues.find(name) != m_TrailerValues.end();
729 }
730
731
GetTrailerValue(const string & name) const732 string CCgiResponse::GetTrailerValue(const string &name) const
733 {
734 TMap::const_iterator ptr = m_TrailerValues.find(name);
735 return (ptr == m_TrailerValues.end()) ? kEmptyStr : ptr->second;
736 }
737
738
SetTrailerValue(const string & name,const string & value)739 void CCgiResponse::SetTrailerValue(const string& name, const string& value)
740 {
741 if ( !HaveTrailer(name) ) {
742 ERR_POST_X(7, "Can not set trailer not announced in HTTP header: "
743 << name);
744 return;
745 }
746 if ( x_ValidateHeader(name, value) ) {
747 m_TrailerValues[name] = value;
748 }
749 else {
750 NCBI_THROW(CCgiResponseException, eBadHeaderValue,
751 "CCgiResponse::SetTrailerValue() -- "
752 "invalid trailer name or value: " +
753 name + "=" + value);
754 }
755 }
756
757
758 END_NCBI_SCOPE
759