1 /**
2  * Orthanc - A Lightweight, RESTful DICOM Store
3  * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4  * Department, University Hospital of Liege, Belgium
5  * Copyright (C) 2017-2021 Osimis S.A., Belgium
6  *
7  * This program is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public License
9  * as published by the Free Software Foundation, either version 3 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this program. If not, see
19  * <http://www.gnu.org/licenses/>.
20  **/
21 
22 
23 #include "../PrecompiledHeaders.h"
24 #include "HttpOutput.h"
25 
26 #include "../ChunkedBuffer.h"
27 #include "../Compression/GzipCompressor.h"
28 #include "../Compression/ZlibCompressor.h"
29 #include "../Logging.h"
30 #include "../OrthancException.h"
31 #include "../Toolbox.h"
32 
33 #include <iostream>
34 #include <vector>
35 #include <stdio.h>
36 #include <boost/lexical_cast.hpp>
37 
38 
39 #if ORTHANC_ENABLE_CIVETWEB == 1
40 #  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
41 #    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
42 #  endif
43 #endif
44 
45 
46 namespace Orthanc
47 {
StateMachine(IHttpOutputStream & stream,bool isKeepAlive)48   HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
49                                          bool isKeepAlive) :
50     stream_(stream),
51     state_(State_WritingHeader),
52     status_(HttpStatus_200_Ok),
53     hasContentLength_(false),
54     contentLength_(0),
55     contentPosition_(0),
56     keepAlive_(isKeepAlive)
57   {
58   }
59 
~StateMachine()60   HttpOutput::StateMachine::~StateMachine()
61   {
62     if (state_ != State_Done)
63     {
64       //asm volatile ("int3;");
65       //LOG(ERROR) << "This HTTP answer does not contain any body";
66     }
67 
68     if (hasContentLength_ && contentPosition_ != contentLength_)
69     {
70       LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body";
71     }
72   }
73 
74 
SetHttpStatus(HttpStatus status)75   void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status)
76   {
77     if (state_ != State_WritingHeader)
78     {
79       throw OrthancException(ErrorCode_BadSequenceOfCalls);
80     }
81 
82     status_ = status;
83   }
84 
85 
SetContentLength(uint64_t length)86   void HttpOutput::StateMachine::SetContentLength(uint64_t length)
87   {
88     if (state_ != State_WritingHeader)
89     {
90       throw OrthancException(ErrorCode_BadSequenceOfCalls);
91     }
92 
93     hasContentLength_ = true;
94     contentLength_ = length;
95   }
96 
SetContentType(const char * contentType)97   void HttpOutput::StateMachine::SetContentType(const char* contentType)
98   {
99     AddHeader("Content-Type", contentType);
100   }
101 
SetContentFilename(const char * filename)102   void HttpOutput::StateMachine::SetContentFilename(const char* filename)
103   {
104     // TODO Escape double quotes
105     AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\"");
106   }
107 
SetCookie(const std::string & cookie,const std::string & value)108   void HttpOutput::StateMachine::SetCookie(const std::string& cookie,
109                                            const std::string& value)
110   {
111     if (state_ != State_WritingHeader)
112     {
113       throw OrthancException(ErrorCode_BadSequenceOfCalls);
114     }
115 
116     // TODO Escape "=" characters
117     AddHeader("Set-Cookie", cookie + "=" + value);
118   }
119 
120 
AddHeader(const std::string & header,const std::string & value)121   void HttpOutput::StateMachine::AddHeader(const std::string& header,
122                                            const std::string& value)
123   {
124     if (state_ != State_WritingHeader)
125     {
126       throw OrthancException(ErrorCode_BadSequenceOfCalls);
127     }
128 
129     headers_.push_back(header + ": " + value + "\r\n");
130   }
131 
ClearHeaders()132   void HttpOutput::StateMachine::ClearHeaders()
133   {
134     if (state_ != State_WritingHeader)
135     {
136       throw OrthancException(ErrorCode_BadSequenceOfCalls);
137     }
138 
139     headers_.clear();
140   }
141 
SendBody(const void * buffer,size_t length)142   void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length)
143   {
144     if (state_ == State_Done)
145     {
146       if (length == 0)
147       {
148         return;
149       }
150       else
151       {
152         throw OrthancException(ErrorCode_BadSequenceOfCalls,
153                                "Because of keep-alive connections, the entire body must "
154                                "be sent at once or Content-Length must be given");
155       }
156     }
157 
158     if (state_ == State_WritingMultipart)
159     {
160       throw OrthancException(ErrorCode_InternalError);
161     }
162 
163     if (state_ == State_WritingHeader)
164     {
165       // Send the HTTP header before writing the body
166 
167       stream_.OnHttpStatusReceived(status_);
168 
169       std::string s = "HTTP/1.1 " +
170         boost::lexical_cast<std::string>(status_) +
171         " " + std::string(EnumerationToString(status_)) +
172         "\r\n";
173 
174       if (keepAlive_)
175       {
176         s += "Connection: keep-alive\r\n";
177 
178         /**
179          * [LIFY-2311] The "Keep-Alive" HTTP header was missing in
180          * Orthanc <= 1.8.0, which notably caused failures if
181          * uploading DICOM instances by applying Java's
182          * "org.apache.http.client.methods.HttpPost()" on "/instances"
183          * URI, if "PoolingHttpClientConnectionManager" was in used. A
184          * workaround was to manually set a timeout for the keep-alive
185          * client to, say, 200 milliseconds, by using
186          * "HttpClients.custom().setKeepAliveStrategy((httpResponse,httpContext)->200)".
187          * Note that the "timeout" value can only be integer in the
188          * HTTP header, so we can't use the milliseconds granularity.
189          **/
190         s += ("Keep-Alive: timeout=" +
191               boost::lexical_cast<std::string>(CIVETWEB_KEEP_ALIVE_TIMEOUT_SECONDS) + "\r\n");
192       }
193       else
194       {
195         s += "Connection: close\r\n";
196       }
197 
198       for (std::list<std::string>::const_iterator
199              it = headers_.begin(); it != headers_.end(); ++it)
200       {
201         s += *it;
202       }
203 
204       if (status_ != HttpStatus_200_Ok)
205       {
206         hasContentLength_ = false;
207       }
208 
209       uint64_t contentLength = (hasContentLength_ ? contentLength_ : length);
210       s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n";
211 
212       stream_.Send(true, s.c_str(), s.size());
213       state_ = State_WritingBody;
214     }
215 
216     if (hasContentLength_ &&
217         contentPosition_ + length > contentLength_)
218     {
219       throw OrthancException(ErrorCode_BadSequenceOfCalls,
220                              "The body size exceeds what was declared with SetContentSize()");
221     }
222 
223     if (length > 0)
224     {
225       stream_.Send(false, buffer, length);
226       contentPosition_ += length;
227     }
228 
229     if (!hasContentLength_ ||
230         contentPosition_ == contentLength_)
231     {
232       state_ = State_Done;
233     }
234   }
235 
236 
CloseBody()237   void HttpOutput::StateMachine::CloseBody()
238   {
239     switch (state_)
240     {
241       case State_WritingHeader:
242         SetContentLength(0);
243         SendBody(NULL, 0);
244         break;
245 
246       case State_WritingBody:
247         if (!hasContentLength_ ||
248             contentPosition_ == contentLength_)
249         {
250           state_ = State_Done;
251         }
252         else
253         {
254           throw OrthancException(ErrorCode_BadSequenceOfCalls,
255                                  "The body size has not reached what was declared with SetContentSize()");
256         }
257 
258         break;
259 
260       case State_WritingMultipart:
261         throw OrthancException(ErrorCode_BadSequenceOfCalls,
262                                "Cannot invoke CloseBody() with multipart outputs");
263 
264       case State_Done:
265         return;  // Ignore
266 
267       default:
268         throw OrthancException(ErrorCode_InternalError);
269     }
270   }
271 
272 
GetPreferredCompression(size_t bodySize) const273   HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const
274   {
275 #if 0
276     // TODO Do not compress small files?
277     if (bodySize < 512)
278     {
279       return HttpCompression_None;
280     }
281 #endif
282 
283     // Prefer "gzip" over "deflate" if the choice is offered
284 
285     if (isGzipAllowed_)
286     {
287       return HttpCompression_Gzip;
288     }
289     else if (isDeflateAllowed_)
290     {
291       return HttpCompression_Deflate;
292     }
293     else
294     {
295       return HttpCompression_None;
296     }
297   }
298 
299 
HttpOutput(IHttpOutputStream & stream,bool isKeepAlive)300   HttpOutput::HttpOutput(IHttpOutputStream &stream,
301                          bool isKeepAlive) :
302     stateMachine_(stream, isKeepAlive),
303     isDeflateAllowed_(false),
304     isGzipAllowed_(false)
305   {
306   }
307 
SetDeflateAllowed(bool allowed)308   void HttpOutput::SetDeflateAllowed(bool allowed)
309   {
310     isDeflateAllowed_ = allowed;
311   }
312 
IsDeflateAllowed() const313   bool HttpOutput::IsDeflateAllowed() const
314   {
315     return isDeflateAllowed_;
316   }
317 
SetGzipAllowed(bool allowed)318   void HttpOutput::SetGzipAllowed(bool allowed)
319   {
320     isGzipAllowed_ = allowed;
321   }
322 
IsGzipAllowed() const323   bool HttpOutput::IsGzipAllowed() const
324   {
325     return isGzipAllowed_;
326   }
327 
328 
SendMethodNotAllowed(const std::string & allowed)329   void HttpOutput::SendMethodNotAllowed(const std::string& allowed)
330   {
331     stateMachine_.ClearHeaders();
332     stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed);
333     stateMachine_.AddHeader("Allow", allowed);
334     stateMachine_.SendBody(NULL, 0);
335   }
336 
337 
SendStatus(HttpStatus status,const char * message,size_t messageSize)338   void HttpOutput::SendStatus(HttpStatus status,
339 			      const char* message,
340 			      size_t messageSize)
341   {
342     if (status == HttpStatus_301_MovedPermanently ||
343         //status == HttpStatus_401_Unauthorized ||
344         status == HttpStatus_405_MethodNotAllowed)
345     {
346       throw OrthancException(ErrorCode_ParameterOutOfRange,
347                              "Please use the dedicated methods to this HTTP status code in HttpOutput");
348     }
349 
350     stateMachine_.SetHttpStatus(status);
351     stateMachine_.SendBody(message, messageSize);
352   }
353 
SendStatus(HttpStatus status)354   void HttpOutput::SendStatus(HttpStatus status)
355   {
356     SendStatus(status, NULL, 0);
357   }
358 
SendStatus(HttpStatus status,const std::string & message)359   void HttpOutput::SendStatus(HttpStatus status, const std::string &message)
360   {
361     SendStatus(status, message.c_str(), message.size());
362   }
363 
SetContentType(MimeType contentType)364   void HttpOutput::SetContentType(MimeType contentType)
365   {
366     stateMachine_.SetContentType(EnumerationToString(contentType));
367   }
368 
SetContentType(const std::string & contentType)369   void HttpOutput::SetContentType(const std::string &contentType)
370   {
371     stateMachine_.SetContentType(contentType.c_str());
372   }
373 
SetContentFilename(const char * filename)374   void HttpOutput::SetContentFilename(const char *filename)
375   {
376     stateMachine_.SetContentFilename(filename);
377   }
378 
SetCookie(const std::string & cookie,const std::string & value)379   void HttpOutput::SetCookie(const std::string &cookie, const std::string &value)
380   {
381     stateMachine_.SetCookie(cookie, value);
382   }
383 
AddHeader(const std::string & key,const std::string & value)384   void HttpOutput::AddHeader(const std::string &key, const std::string &value)
385   {
386     stateMachine_.AddHeader(key, value);
387   }
388 
389 
Redirect(const std::string & path)390   void HttpOutput::Redirect(const std::string& path)
391   {
392     stateMachine_.ClearHeaders();
393     stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently);
394     stateMachine_.AddHeader("Location", path);
395     stateMachine_.SendBody(NULL, 0);
396   }
397 
398 
SendUnauthorized(const std::string & realm)399   void HttpOutput::SendUnauthorized(const std::string& realm)
400   {
401     stateMachine_.ClearHeaders();
402     stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized);
403     stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
404     stateMachine_.SendBody(NULL, 0);
405   }
406 
StartMultipart(const std::string & subType,const std::string & contentType)407   void HttpOutput::StartMultipart(const std::string &subType, const std::string &contentType)
408   {
409     stateMachine_.StartMultipart(subType, contentType);
410   }
411 
SendMultipartItem(const void * item,size_t size,const std::map<std::string,std::string> & headers)412   void HttpOutput::SendMultipartItem(const void *item,
413                                      size_t size,
414                                      const std::map<std::string, std::string> &headers)
415   {
416     stateMachine_.SendMultipartItem(item, size, headers);
417   }
418 
CloseMultipart()419   void HttpOutput::CloseMultipart()
420   {
421     stateMachine_.CloseMultipart();
422   }
423 
IsWritingMultipart() const424   bool HttpOutput::IsWritingMultipart() const
425   {
426     return stateMachine_.GetState() == StateMachine::State_WritingMultipart;
427   }
428 
429 
Answer(const void * buffer,size_t length)430   void HttpOutput::Answer(const void* buffer,
431                           size_t length)
432   {
433     if (length == 0)
434     {
435       AnswerEmpty();
436       return;
437     }
438 
439     HttpCompression compression = GetPreferredCompression(length);
440 
441     if (compression == HttpCompression_None)
442     {
443       stateMachine_.SetContentLength(length);
444       stateMachine_.SendBody(buffer, length);
445       return;
446     }
447 
448     std::string compressed, encoding;
449 
450     switch (compression)
451     {
452       case HttpCompression_Deflate:
453       {
454         encoding = "deflate";
455         ZlibCompressor compressor;
456         // Do not prefix the buffer with its uncompressed size, to be compatible with "deflate"
457         compressor.SetPrefixWithUncompressedSize(false);
458         compressor.Compress(compressed, buffer, length);
459         break;
460       }
461 
462       case HttpCompression_Gzip:
463       {
464         encoding = "gzip";
465         GzipCompressor compressor;
466         compressor.Compress(compressed, buffer, length);
467         break;
468       }
469 
470       default:
471         throw OrthancException(ErrorCode_InternalError);
472     }
473 
474     LOG(TRACE) << "Compressing a HTTP answer using " << encoding;
475 
476     // The body is empty, do not use HTTP compression
477     if (compressed.size() == 0)
478     {
479       AnswerEmpty();
480     }
481     else
482     {
483       stateMachine_.AddHeader("Content-Encoding", encoding);
484       stateMachine_.SetContentLength(compressed.size());
485       stateMachine_.SendBody(compressed.c_str(), compressed.size());
486     }
487 
488     stateMachine_.CloseBody();
489   }
490 
491 
Answer(const std::string & str)492   void HttpOutput::Answer(const std::string& str)
493   {
494     Answer(str.size() == 0 ? NULL : str.c_str(), str.size());
495   }
496 
497 
AnswerEmpty()498   void HttpOutput::AnswerEmpty()
499   {
500     stateMachine_.CloseBody();
501   }
502 
503 
CheckHeadersCompatibilityWithMultipart() const504   void HttpOutput::StateMachine::CheckHeadersCompatibilityWithMultipart() const
505   {
506     for (std::list<std::string>::const_iterator
507            it = headers_.begin(); it != headers_.end(); ++it)
508     {
509       if (!Toolbox::StartsWith(*it, "Set-Cookie: "))
510       {
511         throw OrthancException(ErrorCode_BadSequenceOfCalls,
512                                "The only headers that can be set in multipart answers "
513                                "are Set-Cookie (here: " + *it + " is set)");
514       }
515     }
516   }
517 
518 
PrepareMultipartMainHeader(std::string & boundary,std::string & contentTypeHeader,const std::string & subType,const std::string & contentType)519   static void PrepareMultipartMainHeader(std::string& boundary,
520                                          std::string& contentTypeHeader,
521                                          const std::string& subType,
522                                          const std::string& contentType)
523   {
524     if (subType != "mixed" &&
525         subType != "related")
526     {
527       throw OrthancException(ErrorCode_ParameterOutOfRange);
528     }
529 
530     /**
531      * Fix for issue 54 ("Decide what to do wrt. quoting of multipart
532      * answers"). The "type" parameter in the "Content-Type" HTTP
533      * header must be quoted if it contains a forward slash "/". This
534      * is necessary for DICOMweb compatibility with OsiriX, but breaks
535      * compatibility with old releases of the client in the Orthanc
536      * DICOMweb plugin <= 0.3 (releases >= 0.4 work fine).
537      *
538      * Full history is available at the following locations:
539      * - In changeset 2248:69b0f4e8a49b:
540      *   # hg history -v -r 2248
541      * - https://bugs.orthanc-server.com/show_bug.cgi?id=54
542      * - https://groups.google.com/d/msg/orthanc-users/65zhIM5xbKI/TU5Q1_LhAwAJ
543      **/
544     std::string tmp;
545     if (contentType.find('/') == std::string::npos)
546     {
547       // No forward slash in the content type
548       tmp = contentType;
549     }
550     else
551     {
552       // Quote the content type because of the forward slash
553       tmp = "\"" + contentType + "\"";
554     }
555 
556     boundary = Toolbox::GenerateUuid() + "-" + Toolbox::GenerateUuid();
557 
558     /**
559      * Fix for issue #165: "Encapsulation boundaries must not appear
560      * within the encapsulations, and must be no longer than 70
561      * characters, not counting the two leading hyphens."
562      * https://tools.ietf.org/html/rfc1521
563      * https://bugs.orthanc-server.com/show_bug.cgi?id=165
564      **/
565     if (boundary.size() != 36 + 1 + 36)  // one UUID contains 36 characters
566     {
567       throw OrthancException(ErrorCode_InternalError);
568     }
569 
570     boundary = boundary.substr(0, 70);
571 
572     contentTypeHeader = ("multipart/" + subType + "; type=" + tmp + "; boundary=" + boundary);
573   }
574 
575 
StartMultipart(const std::string & subType,const std::string & contentType)576   void HttpOutput::StateMachine::StartMultipart(const std::string& subType,
577                                                 const std::string& contentType)
578   {
579     if (state_ != State_WritingHeader)
580     {
581       throw OrthancException(ErrorCode_BadSequenceOfCalls);
582     }
583 
584     if (status_ != HttpStatus_200_Ok)
585     {
586       SendBody(NULL, 0);
587       return;
588     }
589 
590     stream_.OnHttpStatusReceived(status_);
591 
592     std::string header = "HTTP/1.1 200 OK\r\n";
593 
594     if (keepAlive_)
595     {
596 #if ORTHANC_ENABLE_MONGOOSE == 1
597       throw OrthancException(ErrorCode_NotImplemented,
598                              "Multipart answers are not implemented together "
599                              "with keep-alive connections if using Mongoose");
600 
601 #elif ORTHANC_ENABLE_CIVETWEB == 1
602 #  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
603       // Turn off Keep-Alive for multipart answers
604       // https://github.com/civetweb/civetweb/issues/727
605       stream_.DisableKeepAlive();
606       header += "Connection: close\r\n";
607 #  else
608       // The function "mg_disable_keep_alive()" is not available,
609       // let's continue with Keep-Alive. Performance of WADO-RS will
610       // decrease.
611       header += "Connection: keep-alive\r\n";
612 #  endif
613 
614 #else
615 #  error Please support your embedded Web server here
616 #endif
617     }
618     else
619     {
620       header += "Connection: close\r\n";
621     }
622 
623     // Possibly add the cookies
624     CheckHeadersCompatibilityWithMultipart();
625 
626     for (std::list<std::string>::const_iterator
627            it = headers_.begin(); it != headers_.end(); ++it)
628     {
629       header += *it;
630     }
631 
632     std::string contentTypeHeader;
633     PrepareMultipartMainHeader(multipartBoundary_, contentTypeHeader, subType, contentType);
634     multipartContentType_ = contentType;
635     header += ("Content-Type: " + contentTypeHeader + "\r\n\r\n");
636 
637     stream_.Send(true, header.c_str(), header.size());
638     state_ = State_WritingMultipart;
639   }
640 
641 
PrepareMultipartItemHeader(std::string & target,size_t length,const std::map<std::string,std::string> & headers,const std::string & boundary,const std::string & contentType)642   static void PrepareMultipartItemHeader(std::string& target,
643                                          size_t length,
644                                          const std::map<std::string, std::string>& headers,
645                                          const std::string& boundary,
646                                          const std::string& contentType)
647   {
648     target = "--" + boundary + "\r\n";
649 
650     bool hasContentType = false;
651     bool hasContentLength = false;
652     bool hasMimeVersion = false;
653 
654     for (std::map<std::string, std::string>::const_iterator
655            it = headers.begin(); it != headers.end(); ++it)
656     {
657       target += it->first + ": " + it->second + "\r\n";
658 
659       std::string tmp;
660       Toolbox::ToLowerCase(tmp, it->first);
661 
662       if (tmp == "content-type")
663       {
664         hasContentType = true;
665       }
666 
667       if (tmp == "content-length")
668       {
669         hasContentLength = true;
670       }
671 
672       if (tmp == "mime-version")
673       {
674         hasMimeVersion = true;
675       }
676     }
677 
678     if (!hasContentType)
679     {
680       target += "Content-Type: " + contentType + "\r\n";
681     }
682 
683     if (!hasContentLength)
684     {
685       target += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n";
686     }
687 
688     if (!hasMimeVersion)
689     {
690       target += "MIME-Version: 1.0\r\n\r\n";
691     }
692   }
693 
694 
SendMultipartItem(const void * item,size_t length,const std::map<std::string,std::string> & headers)695   void HttpOutput::StateMachine::SendMultipartItem(const void* item,
696                                                    size_t length,
697                                                    const std::map<std::string, std::string>& headers)
698   {
699     if (state_ != State_WritingMultipart)
700     {
701       throw OrthancException(ErrorCode_BadSequenceOfCalls);
702     }
703 
704     std::string header;
705     PrepareMultipartItemHeader(header, length, headers, multipartBoundary_, multipartContentType_);
706     stream_.Send(false, header.c_str(), header.size());
707 
708     if (length > 0)
709     {
710       stream_.Send(false, item, length);
711     }
712 
713     stream_.Send(false, "\r\n", 2);
714   }
715 
716 
CloseMultipart()717   void HttpOutput::StateMachine::CloseMultipart()
718   {
719     if (state_ != State_WritingMultipart)
720     {
721       throw OrthancException(ErrorCode_BadSequenceOfCalls);
722     }
723 
724     // The two lines below might throw an exception, if the client has
725     // closed the connection. Such an error is ignored.
726     try
727     {
728       std::string header = "--" + multipartBoundary_ + "--\r\n";
729       stream_.Send(false, header.c_str(), header.size());
730     }
731     catch (OrthancException&)
732     {
733     }
734 
735     state_ = State_Done;
736   }
737 
738 
AnswerStreamAsBuffer(HttpOutput & output,IHttpStreamAnswer & stream)739   static void AnswerStreamAsBuffer(HttpOutput& output,
740                                    IHttpStreamAnswer& stream)
741   {
742     ChunkedBuffer buffer;
743 
744     while (stream.ReadNextChunk())
745     {
746       if (stream.GetChunkSize() > 0)
747       {
748         buffer.AddChunk(stream.GetChunkContent(), stream.GetChunkSize());
749       }
750     }
751 
752     std::string s;
753     buffer.Flatten(s);
754 
755     output.SetContentType(stream.GetContentType());
756 
757     std::string filename;
758     if (stream.HasContentFilename(filename))
759     {
760       output.SetContentFilename(filename.c_str());
761     }
762 
763     output.Answer(s);
764   }
765 
766 
Answer(IHttpStreamAnswer & stream)767   void HttpOutput::Answer(IHttpStreamAnswer& stream)
768   {
769     HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_);
770 
771     switch (compression)
772     {
773       case HttpCompression_None:
774       {
775         if (isGzipAllowed_ || isDeflateAllowed_)
776         {
777           // New in Orthanc 1.5.7: Compress streams without built-in
778           // compression, if requested by the "Accept-Encoding" HTTP
779           // header
780           AnswerStreamAsBuffer(*this, stream);
781           return;
782         }
783 
784         break;
785       }
786 
787       case HttpCompression_Gzip:
788         stateMachine_.AddHeader("Content-Encoding", "gzip");
789         break;
790 
791       case HttpCompression_Deflate:
792         stateMachine_.AddHeader("Content-Encoding", "deflate");
793         break;
794 
795       default:
796         throw OrthancException(ErrorCode_ParameterOutOfRange);
797     }
798 
799     stateMachine_.SetContentLength(stream.GetContentLength());
800 
801     std::string contentType = stream.GetContentType();
802     if (contentType.empty())
803     {
804       contentType = MIME_BINARY;
805     }
806 
807     stateMachine_.SetContentType(contentType.c_str());
808 
809     std::string filename;
810     if (stream.HasContentFilename(filename))
811     {
812       SetContentFilename(filename.c_str());
813     }
814 
815     while (stream.ReadNextChunk())
816     {
817       stateMachine_.SendBody(stream.GetChunkContent(),
818                              stream.GetChunkSize());
819     }
820 
821     stateMachine_.CloseBody();
822   }
823 
824 
AnswerMultipartWithoutChunkedTransfer(const std::string & subType,const std::string & contentType,const std::vector<const void * > & parts,const std::vector<size_t> & sizes,const std::vector<const std::map<std::string,std::string> * > & headers)825   void HttpOutput::AnswerMultipartWithoutChunkedTransfer(
826     const std::string& subType,
827     const std::string& contentType,
828     const std::vector<const void*>& parts,
829     const std::vector<size_t>& sizes,
830     const std::vector<const std::map<std::string, std::string>*>& headers)
831   {
832     if (parts.size() != sizes.size())
833     {
834       throw OrthancException(ErrorCode_ParameterOutOfRange);
835     }
836 
837     stateMachine_.CheckHeadersCompatibilityWithMultipart();
838 
839     std::string boundary, contentTypeHeader;
840     PrepareMultipartMainHeader(boundary, contentTypeHeader, subType, contentType);
841     SetContentType(contentTypeHeader);
842 
843     std::map<std::string, std::string> empty;
844 
845     ChunkedBuffer chunked;
846     for (size_t i = 0; i < parts.size(); i++)
847     {
848       std::string partHeader;
849       PrepareMultipartItemHeader(partHeader, sizes[i], headers[i] == NULL ? empty : *headers[i],
850                                  boundary, contentType);
851 
852       chunked.AddChunk(partHeader);
853       chunked.AddChunk(parts[i], sizes[i]);
854       chunked.AddChunk("\r\n");
855     }
856 
857     chunked.AddChunk("--" + boundary + "--\r\n");
858 
859     std::string body;
860     chunked.Flatten(body);
861     Answer(body);
862   }
863 }
864