comparison OrthancFramework/Sources/HttpServer/HttpOutput.cpp @ 4044:d25f4c0fa160 framework

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