Mercurial > hg > orthanc
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; |