comparison OrthancFramework/Sources/HttpServer/HttpServer.cpp @ 4454:f20a7655fb1c

Fix upload of multiple DICOM files using one single POST call to "multipart/form-data"
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 19 Jan 2021 12:03:49 +0100
parents 6a6017027162
children 57b1a36645ae
comparison
equal deleted inserted replaced
4453:4f8e77c650e8 4454:f20a7655fb1c
30 #include "../Logging.h" 30 #include "../Logging.h"
31 #include "../OrthancException.h" 31 #include "../OrthancException.h"
32 #include "../TemporaryFile.h" 32 #include "../TemporaryFile.h"
33 #include "HttpToolbox.h" 33 #include "HttpToolbox.h"
34 #include "IHttpHandler.h" 34 #include "IHttpHandler.h"
35 #include "MultipartStreamReader.h"
36 #include "StringHttpOutput.h"
35 37
36 #if ORTHANC_ENABLE_PUGIXML == 1 38 #if ORTHANC_ENABLE_PUGIXML == 1
37 # include "IWebDavBucket.h" 39 # include "IWebDavBucket.h"
38 #endif 40 #endif
39 41
75 #define ORTHANC_REALM "Orthanc Secure Area" 77 #define ORTHANC_REALM "Orthanc Secure Area"
76 78
77 79
78 namespace Orthanc 80 namespace Orthanc
79 { 81 {
80 static const char MULTIPART_FORM[] = "multipart/form-data; boundary=";
81 static unsigned int MULTIPART_FORM_LENGTH = sizeof(MULTIPART_FORM) / sizeof(char) - 1;
82
83
84 namespace 82 namespace
85 { 83 {
86 // Anonymous namespace to avoid clashes between compilation modules 84 // Anonymous namespace to avoid clashes between compilation modules
87 class MongooseOutputStream : public IHttpOutputStream 85 class MongooseOutputStream : public IHttpOutputStream
88 { 86 {
93 explicit MongooseOutputStream(struct mg_connection* connection) : 91 explicit MongooseOutputStream(struct mg_connection* connection) :
94 connection_(connection) 92 connection_(connection)
95 { 93 {
96 } 94 }
97 95
98 virtual void Send(bool isHeader, const void* buffer, size_t length) 96 virtual void Send(bool isHeader,
97 const void* buffer,
98 size_t length) ORTHANC_OVERRIDE
99 { 99 {
100 if (length > 0) 100 if (length > 0)
101 { 101 {
102 int status = mg_write(connection_, buffer, length); 102 int status = mg_write(connection_, buffer, length);
103 if (status != static_cast<int>(length)) 103 if (status != static_cast<int>(length))
106 throw OrthancException(ErrorCode_NetworkProtocol); 106 throw OrthancException(ErrorCode_NetworkProtocol);
107 } 107 }
108 } 108 }
109 } 109 }
110 110
111 virtual void OnHttpStatusReceived(HttpStatus status) 111 virtual void OnHttpStatusReceived(HttpStatus status) ORTHANC_OVERRIDE
112 { 112 {
113 // Ignore this 113 // Ignore this
114 } 114 }
115 115
116 virtual void DisableKeepAlive() 116 virtual void DisableKeepAlive() ORTHANC_OVERRIDE
117 { 117 {
118 #if ORTHANC_ENABLE_MONGOOSE == 1 118 #if ORTHANC_ENABLE_MONGOOSE == 1
119 throw OrthancException(ErrorCode_NotImplemented, 119 throw OrthancException(ErrorCode_NotImplemented,
120 "Only available if using CivetWeb"); 120 "Only available if using CivetWeb");
121 121
145 PostDataStatus_Failure 145 PostDataStatus_Failure
146 }; 146 };
147 } 147 }
148 148
149 149
150 // TODO Move this to external file 150 namespace
151 151 {
152 152 class ChunkedFile : public ChunkedBuffer
153 class ChunkedFile : public ChunkedBuffer 153 {
154 { 154 private:
155 private: 155 std::string filename_;
156 std::string filename_; 156
157 157 public:
158 public: 158 explicit ChunkedFile(const std::string& filename) :
159 explicit ChunkedFile(const std::string& filename) : 159 filename_(filename)
160 filename_(filename) 160 {
161 { 161 }
162 } 162
163 163 const std::string& GetFilename() const
164 const std::string& GetFilename() const 164 {
165 { 165 return filename_;
166 return filename_; 166 }
167 } 167 };
168 }; 168 }
169 169
170 170
171 171
172 class ChunkStore : public boost::noncopyable 172 class HttpServer::ChunkStore : public boost::noncopyable
173 { 173 {
174 private: 174 private:
175 typedef std::list<ChunkedFile*> Content; 175 typedef std::list<ChunkedFile*> Content;
176 Content content_; 176 Content content_;
177 unsigned int numPlaces_; 177 unsigned int numPlaces_;
222 { 222 {
223 Clear(); 223 Clear();
224 } 224 }
225 225
226 PostDataStatus Store(std::string& completed, 226 PostDataStatus Store(std::string& completed,
227 const char* chunkData, 227 const void* chunkData,
228 size_t chunkSize, 228 size_t chunkSize,
229 const std::string& filename, 229 const std::string& filename,
230 size_t filesize) 230 size_t filesize)
231 { 231 {
232 boost::mutex::scoped_lock lock(mutex_); 232 boost::mutex::scoped_lock lock(mutex_);
300 { 300 {
301 } 301 }
302 }; 302 };
303 303
304 304
305 ChunkStore& HttpServer::GetChunkStore() 305 class HttpServer::MultipartFormDataHandler : public MultipartStreamReader::IHandler
306 { 306 {
307 return pimpl_->chunkStore_; 307 private:
308 } 308 IHttpHandler& handler_;
309 309 ChunkStore& chunkStore_;
310 const std::string& remoteIp_;
311 const std::string& username_;
312 const UriComponents& uri_;
313 bool isJQueryUploadChunk_;
314 std::string jqueryUploadFileName_;
315 size_t jqueryUploadFileSize_;
316
317 void HandleInternal(const MultipartStreamReader::HttpHeaders& headers,
318 const void* part,
319 size_t size)
320 {
321 StringHttpOutput stringOutput;
322 HttpOutput fakeOutput(stringOutput, false);
323 HttpToolbox::GetArguments getArguments;
324
325 if (!handler_.Handle(fakeOutput, RequestOrigin_RestApi, remoteIp_.c_str(), username_.c_str(),
326 HttpMethod_Post, uri_, headers, getArguments, part, size))
327 {
328 throw OrthancException(ErrorCode_UnknownResource);
329 }
330 }
331
332 public:
333 MultipartFormDataHandler(IHttpHandler& handler,
334 ChunkStore& chunkStore,
335 const std::string& remoteIp,
336 const std::string& username,
337 const UriComponents& uri,
338 const MultipartStreamReader::HttpHeaders& headers) :
339 handler_(handler),
340 chunkStore_(chunkStore),
341 remoteIp_(remoteIp),
342 username_(username),
343 uri_(uri),
344 isJQueryUploadChunk_(false),
345 jqueryUploadFileSize_(0) // Dummy initialization
346 {
347 typedef HttpToolbox::Arguments::const_iterator Iterator;
348
349 Iterator requestedWith = headers.find("x-requested-with");
350 if (requestedWith != headers.end() &&
351 requestedWith->second != "XMLHttpRequest")
352 {
353 throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-Requested-With\" should be "
354 "\"XMLHttpRequest\" in multipart uploads");
355 }
356
357 Iterator fileName = headers.find("x-file-name");
358 Iterator fileSize = headers.find("x-file-size");
359 if (fileName != headers.end() ||
360 fileSize != headers.end())
361 {
362 if (fileName == headers.end())
363 {
364 throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Name\" is missing");
365 }
366
367 if (fileSize == headers.end())
368 {
369 throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" is missing");
370 }
371
372 isJQueryUploadChunk_ = true;
373 jqueryUploadFileName_ = fileName->second;
374
375 try
376 {
377 int64_t s = boost::lexical_cast<int64_t>(fileSize->second);
378 if (s < 0)
379 {
380 throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" has negative value");
381 }
382 else
383 {
384 jqueryUploadFileSize_ = static_cast<size_t>(s);
385 if (static_cast<int64_t>(jqueryUploadFileSize_) != s)
386 {
387 throw OrthancException(ErrorCode_NotEnoughMemory);
388 }
389 }
390 }
391 catch (boost::bad_lexical_cast& e)
392 {
393 throw OrthancException(ErrorCode_NetworkProtocol, "HTTP header \"X-File-Size\" is not an integer");
394 }
395 }
396 }
397
398 virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers,
399 const void* part,
400 size_t size) ORTHANC_OVERRIDE
401 {
402 if (isJQueryUploadChunk_)
403 {
404 std::string completedFile;
405
406 PostDataStatus status = chunkStore_.Store(completedFile, part, size, jqueryUploadFileName_, jqueryUploadFileSize_);
407
408 switch (status)
409 {
410 case PostDataStatus_Failure:
411 throw OrthancException(ErrorCode_NetworkProtocol, "Error in the multipart form upload");
412
413 case PostDataStatus_Success:
414 assert(completedFile.size() == jqueryUploadFileSize_);
415 HandleInternal(headers, completedFile.empty() ? NULL : completedFile.c_str(), completedFile.size());
416 break;
417
418 case PostDataStatus_Pending:
419 break;
420
421 default:
422 throw OrthancException(ErrorCode_InternalError);
423 }
424 }
425 else
426 {
427 HandleInternal(headers, part, size);
428 }
429 }
430 };
431
432
433 void HttpServer::ProcessMultipartFormData(const std::string& remoteIp,
434 const std::string& username,
435 const UriComponents& uri,
436 const std::map<std::string, std::string>& headers,
437 const std::string& body,
438 const std::string& boundary)
439 {
440 MultipartFormDataHandler handler(GetHandler(), pimpl_->chunkStore_, remoteIp, username, uri, headers);
441
442 MultipartStreamReader reader(boundary);
443 reader.SetHandler(handler);
444 reader.AddChunk(body);
445 reader.CloseStream();
446 }
447
310 448
311 static PostDataStatus ReadBodyWithContentLength(std::string& body, 449 static PostDataStatus ReadBodyWithContentLength(std::string& body,
312 struct mg_connection *connection, 450 struct mg_connection *connection,
313 const std::string& contentLength) 451 const std::string& contentLength)
314 { 452 {
443 581
444 return PostDataStatus_Success; 582 return PostDataStatus_Success;
445 } 583 }
446 } 584 }
447 585
448 586
449 static PostDataStatus ParseMultipartForm(std::string &completedFile,
450 struct mg_connection *connection,
451 const HttpToolbox::Arguments& headers,
452 const std::string& contentType,
453 ChunkStore& chunkStore)
454 {
455 std::string boundary = "--" + contentType.substr(MULTIPART_FORM_LENGTH);
456
457 std::string body;
458 PostDataStatus status = ReadBodyToString(body, connection, headers);
459
460 if (status != PostDataStatus_Success)
461 {
462 return status;
463 }
464
465 /*for (HttpToolbox::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++)
466 {
467 std::cout << "Header [" << i->first << "] = " << i->second << "\n";
468 }
469 printf("CHUNK\n");*/
470
471 typedef HttpToolbox::Arguments::const_iterator ArgumentIterator;
472
473 ArgumentIterator requestedWith = headers.find("x-requested-with");
474 ArgumentIterator fileName = headers.find("x-file-name");
475 ArgumentIterator fileSizeStr = headers.find("x-file-size");
476
477 if (requestedWith != headers.end() &&
478 requestedWith->second != "XMLHttpRequest")
479 {
480 return PostDataStatus_Failure;
481 }
482
483 size_t fileSize = 0;
484 if (fileSizeStr != headers.end())
485 {
486 try
487 {
488 fileSize = boost::lexical_cast<size_t>(fileSizeStr->second);
489 }
490 catch (boost::bad_lexical_cast&)
491 {
492 return PostDataStatus_Failure;
493 }
494 }
495
496 typedef boost::find_iterator<std::string::iterator> FindIterator;
497 typedef boost::iterator_range<char*> Range;
498
499 //chunkStore.Print();
500
501 // TODO - Refactor using class "MultipartStreamReader"
502 try
503 {
504 FindIterator last;
505 for (FindIterator it =
506 make_find_iterator(body, boost::first_finder(boundary));
507 it!=FindIterator();
508 ++it)
509 {
510 if (last != FindIterator())
511 {
512 Range part(&last->back(), &it->front());
513 Range content = boost::find_first(part, "\r\n\r\n");
514 if (/*content != Range()*/!content.empty())
515 {
516 Range c(&content.back() + 1, &it->front() - 2);
517 size_t chunkSize = c.size();
518
519 if (chunkSize > 0)
520 {
521 const char* chunkData = &c.front();
522
523 if (fileName == headers.end())
524 {
525 // This file is stored in a single chunk
526 completedFile.resize(chunkSize);
527 memcpy(&completedFile[0], chunkData, chunkSize);
528 return PostDataStatus_Success;
529 }
530 else
531 {
532 return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize);
533 }
534 }
535 }
536 }
537
538 last = it;
539 }
540 }
541 catch (std::length_error&)
542 {
543 return PostDataStatus_Failure;
544 }
545
546 return PostDataStatus_Pending;
547 }
548
549
550 enum AccessMode 587 enum AccessMode
551 { 588 {
552 AccessMode_Forbidden, 589 AccessMode_Forbidden,
553 AccessMode_AuthorizationToken, 590 AccessMode_AuthorizationToken,
554 AccessMode_RegisteredUser 591 AccessMode_RegisteredUser
1261 bool found = false; 1298 bool found = false;
1262 1299
1263 // Extract the body of the request for PUT and POST, or process 1300 // Extract the body of the request for PUT and POST, or process
1264 // the body as a stream 1301 // the body as a stream
1265 1302
1266 // TODO Avoid unneccessary memcopy of the body
1267
1268 std::string body; 1303 std::string body;
1269 if (method == HttpMethod_Post || 1304 if (method == HttpMethod_Post ||
1270 method == HttpMethod_Put) 1305 method == HttpMethod_Put)
1271 { 1306 {
1272 PostDataStatus status; 1307 PostDataStatus status;
1273 1308
1274 bool isMultipartForm = false; 1309 bool isMultipartForm = false;
1275 1310
1311 std::string type, subType, boundary;
1276 HttpToolbox::Arguments::const_iterator ct = headers.find("content-type"); 1312 HttpToolbox::Arguments::const_iterator ct = headers.find("content-type");
1277 if (ct != headers.end() && 1313 if (method == HttpMethod_Post &&
1278 ct->second.size() >= MULTIPART_FORM_LENGTH && 1314 ct != headers.end() &&
1279 !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH)) 1315 MultipartStreamReader::ParseMultipartContentType(type, subType, boundary, ct->second) &&
1316 type == "multipart/form-data")
1280 { 1317 {
1281 /** 1318 /**
1282 * The user uses the "upload" form of Orthanc Explorer, for 1319 * The user uses the "upload" form of Orthanc Explorer, for
1283 * file uploads through a HTML form. 1320 * file uploads through a HTML form.
1284 **/ 1321 **/
1285 status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore());
1286 isMultipartForm = true; 1322 isMultipartForm = true;
1323
1324 status = ReadBodyToString(body, connection, headers);
1325 if (status == PostDataStatus_Success)
1326 {
1327 server.ProcessMultipartFormData(remoteIp, username, uri, headers, body, boundary);
1328 output.SendStatus(HttpStatus_200_Ok);
1329 return;
1330 }
1287 } 1331 }
1288 1332
1289 if (!isMultipartForm) 1333 if (!isMultipartForm)
1290 { 1334 {
1291 std::unique_ptr<IHttpHandler::IChunkedRequestReader> stream; 1335 std::unique_ptr<IHttpHandler::IChunkedRequestReader> stream;