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