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 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 "PrecompiledHeadersServer.h"
35 #include "OrthancRestApi/OrthancRestApi.h"
36 
37 #include "../../OrthancFramework/Sources/Compatibility.h"
38 #include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
39 #include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h"
40 #include "../../OrthancFramework/Sources/DicomNetworking/DicomServer.h"
41 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
42 #include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
43 #include "../../OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h"
44 #include "../../OrthancFramework/Sources/HttpServer/HttpServer.h"
45 #include "../../OrthancFramework/Sources/Logging.h"
46 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
47 #include "../Plugins/Engine/OrthancPlugins.h"
48 #include "Database/SQLiteDatabaseWrapper.h"
49 #include "EmbeddedResourceHttpHandler.h"
50 #include "OrthancConfiguration.h"
51 #include "OrthancFindRequestHandler.h"
52 #include "OrthancGetRequestHandler.h"
53 #include "OrthancInitialization.h"
54 #include "OrthancMoveRequestHandler.h"
55 #include "OrthancWebDav.h"
56 #include "ServerContext.h"
57 #include "ServerJobs/StorageCommitmentScpJob.h"
58 #include "ServerToolbox.h"
59 #include "StorageCommitmentReports.h"
60 
61 #include <boost/algorithm/string/predicate.hpp>
62 
63 
64 using namespace Orthanc;
65 
66 
67 static const char* const KEY_DICOM_TLS_PRIVATE_KEY = "DicomTlsPrivateKey";
68 static const char* const KEY_DICOM_TLS_ENABLED = "DicomTlsEnabled";
69 static const char* const KEY_DICOM_TLS_CERTIFICATE = "DicomTlsCertificate";
70 static const char* const KEY_DICOM_TLS_TRUSTED_CERTIFICATES = "DicomTlsTrustedCertificates";
71 static const char* const KEY_MAXIMUM_PDU_LENGTH = "MaximumPduLength";
72 static const char* const KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED = "DicomTlsRemoteCertificateRequired";
73 
74 
75 class OrthancStoreRequestHandler : public IStoreRequestHandler
76 {
77 private:
78   ServerContext& context_;
79 
80 public:
OrthancStoreRequestHandler(ServerContext & context)81   explicit OrthancStoreRequestHandler(ServerContext& context) :
82     context_(context)
83   {
84   }
85 
86 
Handle(DcmDataset & dicom,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)87   virtual void Handle(DcmDataset& dicom,
88                       const std::string& remoteIp,
89                       const std::string& remoteAet,
90                       const std::string& calledAet) ORTHANC_OVERRIDE
91   {
92     std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dicom));
93 
94     if (toStore->GetBufferSize() > 0)
95     {
96       toStore->SetOrigin(DicomInstanceOrigin::FromDicomProtocol
97                          (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
98 
99       std::string id;
100       context_.Store(id, *toStore, StoreInstanceMode_Default);
101     }
102   }
103 };
104 
105 
106 
107 class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
108 {
109 private:
110   ServerContext& context_;
111 
112 public:
OrthancStorageCommitmentRequestHandler(ServerContext & context)113   explicit OrthancStorageCommitmentRequestHandler(ServerContext& context) :
114     context_(context)
115   {
116   }
117 
HandleRequest(const std::string & transactionUid,const std::vector<std::string> & referencedSopClassUids,const std::vector<std::string> & referencedSopInstanceUids,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)118   virtual void HandleRequest(const std::string& transactionUid,
119                              const std::vector<std::string>& referencedSopClassUids,
120                              const std::vector<std::string>& referencedSopInstanceUids,
121                              const std::string& remoteIp,
122                              const std::string& remoteAet,
123                              const std::string& calledAet) ORTHANC_OVERRIDE
124   {
125     if (referencedSopClassUids.size() != referencedSopInstanceUids.size())
126     {
127       throw OrthancException(ErrorCode_InternalError);
128     }
129 
130     std::unique_ptr<StorageCommitmentScpJob> job(
131       new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet));
132 
133     for (size_t i = 0; i < referencedSopClassUids.size(); i++)
134     {
135       job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]);
136     }
137 
138     job->MarkAsReady();
139 
140     context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */);
141   }
142 
HandleReport(const std::string & transactionUid,const std::vector<std::string> & successSopClassUids,const std::vector<std::string> & successSopInstanceUids,const std::vector<std::string> & failedSopClassUids,const std::vector<std::string> & failedSopInstanceUids,const std::vector<StorageCommitmentFailureReason> & failureReasons,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)143   virtual void HandleReport(const std::string& transactionUid,
144                             const std::vector<std::string>& successSopClassUids,
145                             const std::vector<std::string>& successSopInstanceUids,
146                             const std::vector<std::string>& failedSopClassUids,
147                             const std::vector<std::string>& failedSopInstanceUids,
148                             const std::vector<StorageCommitmentFailureReason>& failureReasons,
149                             const std::string& remoteIp,
150                             const std::string& remoteAet,
151                             const std::string& calledAet) ORTHANC_OVERRIDE
152   {
153     if (successSopClassUids.size() != successSopInstanceUids.size() ||
154         failedSopClassUids.size() != failedSopInstanceUids.size() ||
155         failedSopClassUids.size() != failureReasons.size())
156     {
157       throw OrthancException(ErrorCode_InternalError);
158     }
159 
160     std::unique_ptr<StorageCommitmentReports::Report> report(
161       new StorageCommitmentReports::Report(remoteAet));
162 
163     for (size_t i = 0; i < successSopClassUids.size(); i++)
164     {
165       report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
166     }
167 
168     for (size_t i = 0; i < failedSopClassUids.size(); i++)
169     {
170       report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
171     }
172 
173     report->MarkAsComplete();
174 
175     context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
176   }
177 };
178 
179 
180 
181 class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
182 {
183 public:
IsSameAETitle(const std::string & aet1,const std::string & aet2)184   virtual bool IsSameAETitle(const std::string& aet1,
185                              const std::string& aet2) ORTHANC_OVERRIDE
186   {
187     OrthancConfiguration::ReaderLock lock;
188     return lock.GetConfiguration().IsSameAETitle(aet1, aet2);
189   }
190 
LookupAETitle(RemoteModalityParameters & modality,const std::string & aet)191   virtual bool LookupAETitle(RemoteModalityParameters& modality,
192                              const std::string& aet) ORTHANC_OVERRIDE
193   {
194     OrthancConfiguration::ReaderLock lock;
195     return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet);
196   }
197 };
198 
199 
200 class MyDicomServerFactory :
201   public IStoreRequestHandlerFactory,
202   public IFindRequestHandlerFactory,
203   public IMoveRequestHandlerFactory,
204   public IGetRequestHandlerFactory,
205   public IStorageCommitmentRequestHandlerFactory
206 {
207 private:
208   ServerContext& context_;
209 
210 public:
MyDicomServerFactory(ServerContext & context)211   explicit MyDicomServerFactory(ServerContext& context) : context_(context)
212   {
213   }
214 
ConstructStoreRequestHandler()215   virtual IStoreRequestHandler* ConstructStoreRequestHandler() ORTHANC_OVERRIDE
216   {
217     return new OrthancStoreRequestHandler(context_);
218   }
219 
ConstructFindRequestHandler()220   virtual IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE
221   {
222     std::unique_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
223 
224     {
225       OrthancConfiguration::ReaderLock lock;
226       result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
227       result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
228     }
229 
230     if (result->GetMaxResults() == 0)
231     {
232       LOG(INFO) << "No limit on the number of C-FIND results at the Patient, Study and Series levels";
233     }
234     else
235     {
236       LOG(INFO) << "Maximum " << result->GetMaxResults()
237                 << " results for C-FIND queries at the Patient, Study and Series levels";
238     }
239 
240     if (result->GetMaxInstances() == 0)
241     {
242       LOG(INFO) << "No limit on the number of C-FIND results at the Instance level";
243     }
244     else
245     {
246       LOG(INFO) << "Maximum " << result->GetMaxInstances()
247                 << " instances will be returned for C-FIND queries at the Instance level";
248     }
249 
250     return result.release();
251   }
252 
ConstructMoveRequestHandler()253   virtual IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE
254   {
255     return new OrthancMoveRequestHandler(context_);
256   }
257 
ConstructGetRequestHandler()258   virtual IGetRequestHandler* ConstructGetRequestHandler() ORTHANC_OVERRIDE
259   {
260     return new OrthancGetRequestHandler(context_);
261   }
262 
ConstructStorageCommitmentRequestHandler()263   virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() ORTHANC_OVERRIDE
264   {
265     return new OrthancStorageCommitmentRequestHandler(context_);
266   }
267 
268 
Done()269   void Done()
270   {
271   }
272 };
273 
274 
275 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
276 {
277 private:
278   ServerContext&  context_;
279   bool            alwaysAllowEcho_;
280   bool            alwaysAllowFind_;  // New in Orthanc 1.9.0
281   bool            alwaysAllowGet_;   // New in Orthanc 1.9.0
282   bool            alwaysAllowStore_;
283 
284 public:
OrthancApplicationEntityFilter(ServerContext & context)285   explicit OrthancApplicationEntityFilter(ServerContext& context) :
286     context_(context)
287   {
288     {
289       OrthancConfiguration::ReaderLock lock;
290       alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
291       alwaysAllowFind_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowFind", false);
292       alwaysAllowGet_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowGet", false);
293       alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
294     }
295 
296     if (alwaysAllowFind_)
297     {
298       LOG(WARNING) << "Security risk in DICOM SCP: C-FIND requests are always allowed, even from unknown modalities";
299     }
300 
301     if (alwaysAllowGet_)
302     {
303       LOG(WARNING) << "Security risk in DICOM SCP: C-GET requests are always allowed, even from unknown modalities";
304     }
305   }
306 
IsAllowedConnection(const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)307   virtual bool IsAllowedConnection(const std::string& remoteIp,
308                                    const std::string& remoteAet,
309                                    const std::string& calledAet) ORTHANC_OVERRIDE
310   {
311     LOG(INFO) << "Incoming connection from AET " << remoteAet
312               << " on IP " << remoteIp << ", calling AET " << calledAet;
313 
314     if (alwaysAllowEcho_ ||
315         alwaysAllowFind_ ||
316         alwaysAllowGet_ ||
317         alwaysAllowStore_)
318     {
319       return true;
320     }
321     else
322     {
323       OrthancConfiguration::ReaderLock lock;
324       return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp);
325     }
326   }
327 
ReportDisallowedCommand(const std::string & remoteIp,const std::string & remoteAet,DicomRequestType type)328   static void ReportDisallowedCommand(const std::string& remoteIp,
329                                       const std::string& remoteAet,
330                                       DicomRequestType type)
331   {
332     LOG(WARNING) << "Unable to check DICOM authorization for AET " << remoteAet
333                  << " on IP " << remoteIp << ": The DICOM command "
334                  << EnumerationToString(type) << " is not allowed for this modality "
335                  << "according to configuration option \"DicomModalities\"";
336   }
337 
338 
IsAllowedRequest(const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet,DicomRequestType type)339   virtual bool IsAllowedRequest(const std::string& remoteIp,
340                                 const std::string& remoteAet,
341                                 const std::string& calledAet,
342                                 DicomRequestType type) ORTHANC_OVERRIDE
343   {
344     LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
345               << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
346 
347     if (type == DicomRequestType_Echo &&
348         alwaysAllowEcho_)
349     {
350       // Incoming C-Echo requests are always accepted, even from unknown AET
351       return true;
352     }
353     else if (type == DicomRequestType_Find &&
354              alwaysAllowFind_)
355     {
356       // Incoming C-Find requests are always accepted, even from unknown AET
357       return true;
358     }
359     else if (type == DicomRequestType_Store &&
360              alwaysAllowStore_)
361     {
362       // Incoming C-Store requests are always accepted, even from unknown AET
363       return true;
364     }
365     else if (type == DicomRequestType_Get &&
366              alwaysAllowGet_)
367     {
368       // Incoming C-Get requests are always accepted, even from unknown AET
369       return true;
370     }
371     else
372     {
373       bool checkIp;
374       std::list<RemoteModalityParameters> modalities;
375 
376       {
377         OrthancConfiguration::ReaderLock lock;
378         lock.GetConfiguration().LookupDicomModalitiesUsingAETitle(modalities, remoteAet);
379         checkIp = lock.GetConfiguration().GetBooleanParameter("DicomCheckModalityHost", false);
380       }
381 
382       if (modalities.empty())
383       {
384         LOG(WARNING) << "Unable to check DICOM authorization for AET " << remoteAet
385                      << " on IP " << remoteIp << ": This AET is not listed in "
386                      << "configuration option \"DicomModalities\"";
387         return false;
388       }
389       else if (modalities.size() == 1)
390       {
391         // DicomCheckModalityHost is true: check if the IP match the configured IP
392         if (checkIp &&
393             remoteIp != modalities.front().GetHost())
394         {
395           LOG(WARNING) << "Unable to check DICOM authorization for AET " << remoteAet
396                        << " on IP " << remoteIp << ": Its IP address should be "
397                        << modalities.front().GetHost()
398                        << " according to configuration option \"DicomModalities\"";
399           return false;
400         }
401         else if (modalities.front().IsRequestAllowed(type))
402         {
403           return true;
404         }
405         else
406         {
407           ReportDisallowedCommand(remoteIp, remoteAet, type);
408           return false;
409         }
410       }
411       else
412       {
413         // If there are multiple modalities with the same AET, consider the one matching this IP
414         for (std::list<RemoteModalityParameters>::const_iterator
415                it = modalities.begin(); it != modalities.end(); ++it)
416         {
417           if (it->GetHost() == remoteIp)
418           {
419             if (it->IsRequestAllowed(type))
420             {
421               return true;
422             }
423             else
424             {
425               ReportDisallowedCommand(remoteIp, remoteAet, type);
426               return false;
427             }
428           }
429         }
430 
431         LOG(WARNING) << "Unable to check DICOM authorization for AET " << remoteAet
432                      << " on IP " << remoteIp << ": " << modalities.size()
433                      << " modalites found with this AET in configuration option "
434                      << "\"DicomModalities\", but none of them matches the IP";
435         return false;
436       }
437     }
438   }
439 
440 
GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax> & target,const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)441   virtual void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target,
442                                            const std::string& remoteIp,
443                                            const std::string& remoteAet,
444                                            const std::string& calledAet) ORTHANC_OVERRIDE
445   {
446     context_.GetAcceptedTransferSyntaxes(target);
447   }
448 
449 
IsUnknownSopClassAccepted(const std::string & remoteIp,const std::string & remoteAet,const std::string & calledAet)450   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
451                                          const std::string& remoteAet,
452                                          const std::string& calledAet) ORTHANC_OVERRIDE
453   {
454     return context_.IsUnknownSopClassAccepted();
455   }
456 };
457 
458 
459 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
460 {
461 private:
462   ServerContext&   context_;
463   OrthancPlugins*  plugins_;
464 
465 public:
MyIncomingHttpRequestFilter(ServerContext & context,OrthancPlugins * plugins)466   MyIncomingHttpRequestFilter(ServerContext& context,
467                               OrthancPlugins* plugins) :
468     context_(context),
469     plugins_(plugins)
470   {
471   }
472 
IsValidBearerToken(const std::string & token)473   virtual bool IsValidBearerToken(const std::string& token) ORTHANC_OVERRIDE
474   {
475 #if ORTHANC_ENABLE_PLUGINS == 1
476     return (plugins_ != NULL &&
477             plugins_->IsValidAuthorizationToken(token));
478 #else
479     return false;
480 #endif
481   }
482 
IsAllowed(HttpMethod method,const char * uri,const char * ip,const char * username,const HttpToolbox::Arguments & httpHeaders,const HttpToolbox::GetArguments & getArguments)483   virtual bool IsAllowed(HttpMethod method,
484                          const char* uri,
485                          const char* ip,
486                          const char* username,
487                          const HttpToolbox::Arguments& httpHeaders,
488                          const HttpToolbox::GetArguments& getArguments) ORTHANC_OVERRIDE
489   {
490 #if ORTHANC_ENABLE_PLUGINS == 1
491     if (plugins_ != NULL &&
492         !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
493     {
494       return false;
495     }
496 #endif
497 
498     static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
499 
500     LuaScripting::Lock lock(context_.GetLuaScripting());
501 
502     // Test if the instance must be filtered out
503     if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
504     {
505       LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
506 
507       switch (method)
508       {
509         case HttpMethod_Get:
510           call.PushString("GET");
511           break;
512 
513         case HttpMethod_Put:
514           call.PushString("PUT");
515           break;
516 
517         case HttpMethod_Post:
518           call.PushString("POST");
519           break;
520 
521         case HttpMethod_Delete:
522           call.PushString("DELETE");
523           break;
524 
525         default:
526           return true;
527       }
528 
529       call.PushString(uri);
530       call.PushString(ip);
531       call.PushString(username);
532       call.PushStringMap(httpHeaders);
533 
534       if (!call.ExecutePredicate())
535       {
536         LOG(INFO) << "An incoming HTTP request has been discarded by the filter";
537         return false;
538       }
539     }
540 
541     return true;
542   }
543 };
544 
545 
546 
547 class MyHttpExceptionFormatter : public IHttpExceptionFormatter
548 {
549 private:
550   bool             describeErrors_;
551   OrthancPlugins*  plugins_;
552 
553 public:
MyHttpExceptionFormatter(bool describeErrors,OrthancPlugins * plugins)554   MyHttpExceptionFormatter(bool describeErrors,
555                            OrthancPlugins* plugins) :
556     describeErrors_(describeErrors),
557     plugins_(plugins)
558   {
559   }
560 
Format(HttpOutput & output,const OrthancException & exception,HttpMethod method,const char * uri)561   virtual void Format(HttpOutput& output,
562                       const OrthancException& exception,
563                       HttpMethod method,
564                       const char* uri) ORTHANC_OVERRIDE
565   {
566     {
567       bool isPlugin = false;
568 
569 #if ORTHANC_ENABLE_PLUGINS == 1
570       if (plugins_ != NULL)
571       {
572         plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true);
573         isPlugin = true;
574       }
575 #endif
576 
577       if (!isPlugin)
578       {
579         LOG(ERROR) << "Exception in the HTTP handler: " << exception.What();
580       }
581     }
582 
583     Json::Value message = Json::objectValue;
584     ErrorCode errorCode = exception.GetErrorCode();
585     HttpStatus httpStatus = exception.GetHttpStatus();
586 
587     {
588       bool isPlugin = false;
589 
590 #if ORTHANC_ENABLE_PLUGINS == 1
591       if (plugins_ != NULL &&
592           plugins_->GetErrorDictionary().Format(message, httpStatus, exception))
593       {
594         errorCode = ErrorCode_Plugin;
595         isPlugin = true;
596       }
597 #endif
598 
599       if (!isPlugin)
600       {
601         message["Message"] = exception.What();
602       }
603     }
604 
605     if (!describeErrors_)
606     {
607       output.SendStatus(httpStatus);
608     }
609     else
610     {
611       message["Method"] = EnumerationToString(method);
612       message["Uri"] = uri;
613       message["HttpError"] = EnumerationToString(httpStatus);
614       message["HttpStatus"] = httpStatus;
615       message["OrthancError"] = EnumerationToString(errorCode);
616       message["OrthancStatus"] = errorCode;
617 
618       if (exception.HasDetails())
619       {
620         message["Details"] = exception.GetDetails();
621       }
622 
623       std::string info = message.toStyledString();
624       output.SendStatus(httpStatus, info);
625     }
626   }
627 };
628 
629 
PrintHelp(const char * path)630 static void PrintHelp(const char* path)
631 {
632   std::cout
633     << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl
634     << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl
635     << std::endl
636     << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl
637     << "case of a directory, all the JSON files it contains will be merged. " << std::endl
638     << "If no configuration path is given on the command line, a set of default " << std::endl
639     << "parameters is used. Please refer to the Orthanc Book for the full " << std::endl
640     << "instructions about how to use Orthanc <http://book.orthanc-server.com/>." << std::endl
641     << std::endl
642     << "Pay attention to the fact that the order of the options is important." << std::endl
643     << "Options are read left to right. In particular, options such as \"--verbose\" can " << std::endl
644     << "reset the value of other log-related options that were read before." << std::endl
645     << std::endl
646     << "The recommended set of options to debug DICOM communications is " << std::endl
647     << "\"--verbose --trace-dicom --logfile=dicom.log\"" << std::endl
648     << std::endl
649     << "Command-line options:" << std::endl
650     << "  --help\t\tdisplay this help and exit" << std::endl
651     << "  --logdir=[dir]\tdirectory where to store the log files" << std::endl
652     << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
653     << "  --logfile=[file]\tfile where to store the log of Orthanc" << std::endl
654     << "\t\t\t(by default, the log is dumped to stderr)" << std::endl
655     << "  --config=[file]\tcreate a sample configuration file and exit" << std::endl
656     << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl
657     << "  --errors\t\tprint the supported error codes and exit" << std::endl
658     << "  --verbose\t\tbe verbose in logs" << std::endl
659     << "  --trace\t\thighest verbosity in logs (for debug)" << std::endl
660     << "  --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl
661     << "\t\t\tdatabase (beware that the database will become" << std::endl
662     << "\t\t\tincompatible with former versions of Orthanc)" << std::endl
663     << "  --no-jobs\t\tdon't restart the jobs that were stored during" << std::endl
664     << "\t\t\tthe last execution of Orthanc" << std::endl
665     << "  --openapi=[file]\twrite the OpenAPI documentation and exit" << std::endl
666     << "\t\t\t(if \"file\" is \"-\", dumps to stdout)" << std::endl
667     << "  --cheatsheet=[file]\twrite the cheat sheet of REST API as CSV" << std::endl
668     << "\t\t\tand exit (if \"file\" is \"-\", dumps to stdout)" << std::endl
669     << "  --version\t\toutput version information and exit" << std::endl
670     << std::endl
671     << "Fine-tuning of log categories:" << std::endl;
672 
673   for (size_t i = 0; i < Logging::GetCategoriesCount(); i++)
674   {
675     const std::string name = Logging::GetCategoryName(i);
676     std::cout << "  --verbose-" << name
677               << "\tbe verbose in logs of category \"" << name << "\"" << std::endl;
678     std::cout << "  --trace-" << name
679               << "\tuse highest verbosity for logs of category \"" << name << "\"" << std::endl;
680   }
681 
682   std::cout
683     << std::endl
684     << "Exit status:" << std::endl
685     << "  0\tif success," << std::endl
686 #if defined(_WIN32)
687     << "  != 0\tif error (use the --errors option to get the list of possible errors)." << std::endl
688 #else
689     << "  -1\tif error (have a look at the logs)." << std::endl
690 #endif
691     << std::endl;
692 }
693 
694 
PrintVersion(const char * path)695 static void PrintVersion(const char* path)
696 {
697   std::cout
698     << path << " " << ORTHANC_VERSION << std::endl
699     << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
700     << "Copyright (C) 2017-2021 Osimis S.A. (Belgium)" << std::endl
701     << "Licensing GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>, with OpenSSL exception." << std::endl
702     << "This is free software: you are free to change and redistribute it." << std::endl
703     << "There is NO WARRANTY, to the extent permitted by law." << std::endl
704     << std::endl
705     << "Written by Sebastien Jodogne <s.jodogne@orthanc-labs.com>" << std::endl;
706 }
707 
708 
PrintErrorCode(ErrorCode code,const char * description)709 static void PrintErrorCode(ErrorCode code, const char* description)
710 {
711   std::cout
712     << std::right << std::setw(16)
713     << static_cast<int>(code)
714     << "   " << description << std::endl;
715 }
716 
717 
PrintErrors(const char * path)718 static void PrintErrors(const char* path)
719 {
720   std::cout
721     << path << " " << ORTHANC_VERSION << std::endl
722     << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
723     << std::endl << std::endl
724     << "List of error codes that could be returned by Orthanc:"
725     << std::endl << std::endl;
726 
727   // The content of the following brackets is automatically generated
728   // by the "GenerateErrorCodes.py" script
729   {
730     PrintErrorCode(ErrorCode_InternalError, "Internal error");
731     PrintErrorCode(ErrorCode_Success, "Success");
732     PrintErrorCode(ErrorCode_Plugin, "Error encountered within the plugin engine");
733     PrintErrorCode(ErrorCode_NotImplemented, "Not implemented yet");
734     PrintErrorCode(ErrorCode_ParameterOutOfRange, "Parameter out of range");
735     PrintErrorCode(ErrorCode_NotEnoughMemory, "The server hosting Orthanc is running out of memory");
736     PrintErrorCode(ErrorCode_BadParameterType, "Bad type for a parameter");
737     PrintErrorCode(ErrorCode_BadSequenceOfCalls, "Bad sequence of calls");
738     PrintErrorCode(ErrorCode_InexistentItem, "Accessing an inexistent item");
739     PrintErrorCode(ErrorCode_BadRequest, "Bad request");
740     PrintErrorCode(ErrorCode_NetworkProtocol, "Error in the network protocol");
741     PrintErrorCode(ErrorCode_SystemCommand, "Error while calling a system command");
742     PrintErrorCode(ErrorCode_Database, "Error with the database engine");
743     PrintErrorCode(ErrorCode_UriSyntax, "Badly formatted URI");
744     PrintErrorCode(ErrorCode_InexistentFile, "Inexistent file");
745     PrintErrorCode(ErrorCode_CannotWriteFile, "Cannot write to file");
746     PrintErrorCode(ErrorCode_BadFileFormat, "Bad file format");
747     PrintErrorCode(ErrorCode_Timeout, "Timeout");
748     PrintErrorCode(ErrorCode_UnknownResource, "Unknown resource");
749     PrintErrorCode(ErrorCode_IncompatibleDatabaseVersion, "Incompatible version of the database");
750     PrintErrorCode(ErrorCode_FullStorage, "The file storage is full");
751     PrintErrorCode(ErrorCode_CorruptedFile, "Corrupted file (e.g. inconsistent MD5 hash)");
752     PrintErrorCode(ErrorCode_InexistentTag, "Inexistent tag");
753     PrintErrorCode(ErrorCode_ReadOnly, "Cannot modify a read-only data structure");
754     PrintErrorCode(ErrorCode_IncompatibleImageFormat, "Incompatible format of the images");
755     PrintErrorCode(ErrorCode_IncompatibleImageSize, "Incompatible size of the images");
756     PrintErrorCode(ErrorCode_SharedLibrary, "Error while using a shared library (plugin)");
757     PrintErrorCode(ErrorCode_UnknownPluginService, "Plugin invoking an unknown service");
758     PrintErrorCode(ErrorCode_UnknownDicomTag, "Unknown DICOM tag");
759     PrintErrorCode(ErrorCode_BadJson, "Cannot parse a JSON document");
760     PrintErrorCode(ErrorCode_Unauthorized, "Bad credentials were provided to an HTTP request");
761     PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file");
762     PrintErrorCode(ErrorCode_DatabasePlugin, "The plugin implementing a custom database back-end does not fulfill the proper interface");
763     PrintErrorCode(ErrorCode_StorageAreaPlugin, "Error in the plugin implementing a custom storage area");
764     PrintErrorCode(ErrorCode_EmptyRequest, "The request is empty");
765     PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header");
766     PrintErrorCode(ErrorCode_NullPointer, "Cannot handle a NULL pointer");
767     PrintErrorCode(ErrorCode_DatabaseUnavailable, "The database is currently not available (probably a transient situation)");
768     PrintErrorCode(ErrorCode_CanceledJob, "This job was canceled");
769     PrintErrorCode(ErrorCode_BadGeometry, "Geometry error encountered in Stone");
770     PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates");
771     PrintErrorCode(ErrorCode_DiscontinuedAbi, "Calling a function that has been removed from the Orthanc Framework");
772     PrintErrorCode(ErrorCode_BadRange, "Incorrect range request");
773     PrintErrorCode(ErrorCode_DatabaseCannotSerialize, "Database could not serialize access due to concurrent update, the transaction should be retried");
774     PrintErrorCode(ErrorCode_Revision, "A bad revision number was provided, which might indicate conflict between multiple writers");
775     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
776     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
777     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
778     PrintErrorCode(ErrorCode_SQLiteStatementAlreadyUsed, "SQLite: This cached statement is already being referred to");
779     PrintErrorCode(ErrorCode_SQLiteExecute, "SQLite: Cannot execute a command");
780     PrintErrorCode(ErrorCode_SQLiteRollbackWithoutTransaction, "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)");
781     PrintErrorCode(ErrorCode_SQLiteCommitWithoutTransaction, "SQLite: Committing a nonexistent transaction");
782     PrintErrorCode(ErrorCode_SQLiteRegisterFunction, "SQLite: Unable to register a function");
783     PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
784     PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
785     PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
786     PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
787     PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
788     PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
789     PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
790     PrintErrorCode(ErrorCode_SQLiteTransactionBegin, "SQLite: Cannot start a transaction");
791     PrintErrorCode(ErrorCode_DirectoryOverFile, "The directory to be created is already occupied by a regular file");
792     PrintErrorCode(ErrorCode_FileStorageCannotWrite, "Unable to create a subdirectory or a file in the file storage");
793     PrintErrorCode(ErrorCode_DirectoryExpected, "The specified path does not point to a directory");
794     PrintErrorCode(ErrorCode_HttpPortInUse, "The TCP port of the HTTP server is privileged or already in use");
795     PrintErrorCode(ErrorCode_DicomPortInUse, "The TCP port of the DICOM server is privileged or already in use");
796     PrintErrorCode(ErrorCode_BadHttpStatusInRest, "This HTTP status is not allowed in a REST API");
797     PrintErrorCode(ErrorCode_RegularFileExpected, "The specified path does not point to a regular file");
798     PrintErrorCode(ErrorCode_PathToExecutable, "Unable to get the path to the executable");
799     PrintErrorCode(ErrorCode_MakeDirectory, "Cannot create a directory");
800     PrintErrorCode(ErrorCode_BadApplicationEntityTitle, "An application entity title (AET) cannot be empty or be longer than 16 characters");
801     PrintErrorCode(ErrorCode_NoCFindHandler, "No request handler factory for DICOM C-FIND SCP");
802     PrintErrorCode(ErrorCode_NoCMoveHandler, "No request handler factory for DICOM C-MOVE SCP");
803     PrintErrorCode(ErrorCode_NoCStoreHandler, "No request handler factory for DICOM C-STORE SCP");
804     PrintErrorCode(ErrorCode_NoApplicationEntityFilter, "No application entity filter");
805     PrintErrorCode(ErrorCode_NoSopClassOrInstance, "DicomUserConnection: Unable to find the SOP class and instance");
806     PrintErrorCode(ErrorCode_NoPresentationContext, "DicomUserConnection: No acceptable presentation context for modality");
807     PrintErrorCode(ErrorCode_DicomFindUnavailable, "DicomUserConnection: The C-FIND command is not supported by the remote SCP");
808     PrintErrorCode(ErrorCode_DicomMoveUnavailable, "DicomUserConnection: The C-MOVE command is not supported by the remote SCP");
809     PrintErrorCode(ErrorCode_CannotStoreInstance, "Cannot store an instance");
810     PrintErrorCode(ErrorCode_CreateDicomNotString, "Only string values are supported when creating DICOM instances");
811     PrintErrorCode(ErrorCode_CreateDicomOverrideTag, "Trying to override a value inherited from a parent module");
812     PrintErrorCode(ErrorCode_CreateDicomUseContent, "Use \"Content\" to inject an image into a new DICOM instance");
813     PrintErrorCode(ErrorCode_CreateDicomNoPayload, "No payload is present for one instance in the series");
814     PrintErrorCode(ErrorCode_CreateDicomUseDataUriScheme, "The payload of the DICOM instance must be specified according to Data URI scheme");
815     PrintErrorCode(ErrorCode_CreateDicomBadParent, "Trying to attach a new DICOM instance to an inexistent resource");
816     PrintErrorCode(ErrorCode_CreateDicomParentIsInstance, "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)");
817     PrintErrorCode(ErrorCode_CreateDicomParentEncoding, "Unable to get the encoding of the parent resource");
818     PrintErrorCode(ErrorCode_UnknownModality, "Unknown modality");
819     PrintErrorCode(ErrorCode_BadJobOrdering, "Bad ordering of filters in a job");
820     PrintErrorCode(ErrorCode_JsonToLuaTable, "Cannot convert the given JSON object to a Lua table");
821     PrintErrorCode(ErrorCode_CannotCreateLua, "Cannot create the Lua context");
822     PrintErrorCode(ErrorCode_CannotExecuteLua, "Cannot execute a Lua command");
823     PrintErrorCode(ErrorCode_LuaAlreadyExecuted, "Arguments cannot be pushed after the Lua function is executed");
824     PrintErrorCode(ErrorCode_LuaBadOutput, "The Lua function does not give the expected number of outputs");
825     PrintErrorCode(ErrorCode_NotLuaPredicate, "The Lua function is not a predicate (only true/false outputs allowed)");
826     PrintErrorCode(ErrorCode_LuaReturnsNoString, "The Lua function does not return a string");
827     PrintErrorCode(ErrorCode_StorageAreaAlreadyRegistered, "Another plugin has already registered a custom storage area");
828     PrintErrorCode(ErrorCode_DatabaseBackendAlreadyRegistered, "Another plugin has already registered a custom database back-end");
829     PrintErrorCode(ErrorCode_DatabaseNotInitialized, "Plugin trying to call the database during its initialization");
830     PrintErrorCode(ErrorCode_SslDisabled, "Orthanc has been built without SSL support");
831     PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series");
832     PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP");
833     PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
834     PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
835     PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
836     PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
837   }
838 
839   std::cout << std::endl;
840 }
841 
842 
843 
844 #if ORTHANC_ENABLE_PLUGINS == 1
LoadPlugins(OrthancPlugins & plugins)845 static void LoadPlugins(OrthancPlugins& plugins)
846 {
847   std::list<std::string> pathList;
848 
849   {
850     OrthancConfiguration::ReaderLock lock;
851     lock.GetConfiguration().GetListOfStringsParameter(pathList, "Plugins");
852   }
853 
854   for (std::list<std::string>::const_iterator
855          it = pathList.begin(); it != pathList.end(); ++it)
856   {
857     std::string path;
858 
859     {
860       OrthancConfiguration::ReaderLock lock;
861       path = lock.GetConfiguration().InterpretStringParameterAsPath(*it);
862     }
863 
864     LOG(WARNING) << "Loading plugin(s) from: " << path;
865     plugins.GetManager().RegisterPlugin(path);
866   }
867 }
868 #endif
869 
870 
871 
872 // Returns "true" if restart is required
WaitForExit(ServerContext & context,const OrthancRestApi & restApi)873 static bool WaitForExit(ServerContext& context,
874                         const OrthancRestApi& restApi)
875 {
876   LOG(WARNING) << "Orthanc has started";
877 
878 #if ORTHANC_ENABLE_PLUGINS == 1
879   if (context.HasPlugins())
880   {
881     context.GetPlugins().SignalOrthancStarted();
882   }
883 #endif
884 
885   context.GetLuaScripting().Start();
886   context.GetLuaScripting().Execute("Initialize");
887 
888   bool restart;
889 
890   for (;;)
891   {
892     ServerBarrierEvent event = SystemToolbox::ServerBarrier(restApi.LeaveBarrierFlag());
893     restart = restApi.IsResetRequestReceived();
894 
895     if (!restart &&
896         event == ServerBarrierEvent_Reload)
897     {
898       // Handling of SIGHUP
899 
900       OrthancConfiguration::ReaderLock lock;
901       if (lock.GetConfiguration().HasConfigurationChanged())
902       {
903         LOG(WARNING) << "A SIGHUP signal has been received, resetting Orthanc";
904         Logging::Flush();
905         restart = true;
906         break;
907       }
908       else
909       {
910         LOG(WARNING) << "A SIGHUP signal has been received, but is ignored "
911                      << "as the configuration has not changed on the disk";
912         Logging::Flush();
913         continue;
914       }
915     }
916     else
917     {
918       break;
919     }
920   }
921 
922   context.GetLuaScripting().Execute("Finalize");
923   context.GetLuaScripting().Stop();
924 
925 #if ORTHANC_ENABLE_PLUGINS == 1
926   if (context.HasPlugins())
927   {
928     context.GetPlugins().SignalOrthancStopped();
929   }
930 #endif
931 
932   if (restart)
933   {
934     LOG(WARNING) << "Reset request received, restarting Orthanc";
935   }
936 
937   // We're done
938   LOG(WARNING) << "Orthanc is stopping";
939 
940   return restart;
941 }
942 
943 
944 
StartHttpServer(ServerContext & context,const OrthancRestApi & restApi,OrthancPlugins * plugins)945 static bool StartHttpServer(ServerContext& context,
946                             const OrthancRestApi& restApi,
947                             OrthancPlugins* plugins)
948 {
949   bool httpServerEnabled;
950 
951   {
952     OrthancConfiguration::ReaderLock lock;
953     httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
954   }
955 
956   if (!httpServerEnabled)
957   {
958     LOG(WARNING) << "The HTTP server is disabled";
959     return WaitForExit(context, restApi);
960   }
961   else
962   {
963     MyIncomingHttpRequestFilter httpFilter(context, plugins);
964     HttpServer httpServer;
965     bool httpDescribeErrors;
966 
967 #if ORTHANC_ENABLE_MONGOOSE == 1
968     const bool defaultKeepAlive = false;
969 #elif ORTHANC_ENABLE_CIVETWEB == 1
970     const bool defaultKeepAlive = true;
971 #else
972 #  error "Either Mongoose or Civetweb must be enabled to compile this file"
973 #endif
974 
975     {
976       OrthancConfiguration::ReaderLock lock;
977 
978       httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
979 
980       // HTTP server
981       httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
982       httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
983       httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
984       httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
985       httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
986       httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
987       httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
988 
989       // Let's assume that the HTTP server is secure
990       context.SetHttpServerSecure(true);
991 
992       bool authenticationEnabled;
993       if (lock.GetConfiguration().LookupBooleanParameter(authenticationEnabled, "AuthenticationEnabled"))
994       {
995         httpServer.SetAuthenticationEnabled(authenticationEnabled);
996 
997         if (httpServer.IsRemoteAccessAllowed() &&
998             !authenticationEnabled)
999         {
1000           LOG(WARNING) << "====> Remote access is enabled while user authentication is explicitly disabled, "
1001                        << "your setup is POSSIBLY INSECURE <====";
1002           context.SetHttpServerSecure(false);
1003         }
1004       }
1005       else if (httpServer.IsRemoteAccessAllowed())
1006       {
1007         // Starting with Orthanc 1.5.8, it is impossible to enable
1008         // remote access without having explicitly disabled user
1009         // authentication.
1010         LOG(WARNING) << "Remote access is allowed but \"AuthenticationEnabled\" is not in the configuration, "
1011                      << "automatically enabling HTTP authentication for security";
1012         httpServer.SetAuthenticationEnabled(true);
1013       }
1014       else
1015       {
1016         // If Orthanc only listens on the localhost, it is OK to have
1017         // "AuthenticationEnabled" disabled
1018         httpServer.SetAuthenticationEnabled(false);
1019       }
1020 
1021       bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer);
1022 
1023       if (httpServer.IsAuthenticationEnabled() &&
1024           !hasUsers)
1025       {
1026         if (httpServer.IsRemoteAccessAllowed())
1027         {
1028           /**
1029            * Starting with Orthanc 1.5.8, if no user is explicitly
1030            * defined while remote access is allowed, we create a
1031            * default user, and Orthanc Explorer shows a warning
1032            * message about an "Insecure setup". This convention is
1033            * used in Docker images "jodogne/orthanc",
1034            * "jodogne/orthanc-plugins" and "osimis/orthanc".
1035            **/
1036           LOG(WARNING) << "====> HTTP authentication is enabled, but no user is declared. "
1037                        << "Creating a default user: Review your configuration option \"RegisteredUsers\". "
1038                        << "Your setup is INSECURE <====";
1039 
1040           context.SetHttpServerSecure(false);
1041 
1042           // This is the username/password of the default user in Orthanc.
1043           httpServer.RegisterUser("orthanc", "orthanc");
1044         }
1045         else
1046         {
1047           LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, "
1048                        << "check the value of configuration option \"RegisteredUsers\"";
1049         }
1050       }
1051 
1052       if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
1053       {
1054         std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
1055           lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
1056         httpServer.SetSslEnabled(true);
1057         httpServer.SetSslCertificate(certificate.c_str());
1058 
1059         // Default to TLS 1.2 as SSL minimum
1060         // See https://github.com/civetweb/civetweb/blob/master/docs/UserManual.md "ssl_protocol_version" for mapping
1061         static const unsigned int TLS_1_2 = 4;
1062         unsigned int minimumVersion = lock.GetConfiguration().GetUnsignedIntegerParameter("SslMinimumProtocolVersion", TLS_1_2);
1063         httpServer.SetSslMinimumVersion(minimumVersion);
1064 
1065         static const char* SSL_CIPHERS_ACCEPTED = "SslCiphersAccepted";
1066 
1067         std::list<std::string> ciphers;
1068 
1069         if (lock.GetJson().type() == Json::objectValue &&
1070             lock.GetJson().isMember(SSL_CIPHERS_ACCEPTED))
1071         {
1072           lock.GetConfiguration().GetListOfStringsParameter(ciphers, SSL_CIPHERS_ACCEPTED);
1073         }
1074         else
1075         {
1076           // Defaults to FIPS 140-2 ciphers
1077           CLOG(INFO, HTTP) << "No configuration option \"" << SSL_CIPHERS_ACCEPTED
1078                            << "\", will accept the FIPS 140-2 ciphers";
1079 
1080           ciphers.push_back("ECDHE-ECDSA-AES256-GCM-SHA384");
1081           ciphers.push_back("ECDHE-ECDSA-AES256-SHA384");
1082           ciphers.push_back("ECDHE-RSA-AES256-GCM-SHA384");
1083           ciphers.push_back("ECDHE-RSA-AES128-GCM-SHA256");
1084           ciphers.push_back("ECDHE-RSA-AES256-SHA384");
1085           ciphers.push_back("ECDHE-RSA-AES128-SHA256");
1086           ciphers.push_back("ECDHE-RSA-AES128-SHA");
1087           ciphers.push_back("ECDHE-RSA-AES256-SHA");
1088           ciphers.push_back("DHE-RSA-AES256-SHA");
1089           ciphers.push_back("DHE-RSA-AES128-SHA");
1090           ciphers.push_back("AES256-SHA");
1091           ciphers.push_back("AES128-SHA");
1092         }
1093 
1094         httpServer.SetSslCiphers(ciphers);
1095       }
1096       else
1097       {
1098         httpServer.SetSslEnabled(false);
1099       }
1100 
1101       if (lock.GetConfiguration().GetBooleanParameter("SslVerifyPeers", false))
1102       {
1103         std::string trustedClientCertificates = lock.GetConfiguration().InterpretStringParameterAsPath(
1104           lock.GetConfiguration().GetStringParameter("SslTrustedClientCertificates", "trustedCertificates.pem"));
1105         httpServer.SetSslVerifyPeers(true);
1106         httpServer.SetSslTrustedClientCertificates(trustedClientCertificates.c_str());
1107       }
1108       else
1109       {
1110         httpServer.SetSslVerifyPeers(false);
1111       }
1112 
1113       LOG(INFO) << "Version of Lua: " << LUA_VERSION;
1114 
1115       if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled", false))
1116       {
1117         context.SetExecuteLuaEnabled(true);
1118         LOG(WARNING) << "====> Remote LUA script execution is enabled.  Review your configuration option \"ExecuteLuaEnabled\". "
1119                      << "Your setup is POSSIBLY INSECURE <====";
1120       }
1121       else
1122       {
1123         context.SetExecuteLuaEnabled(false);
1124         LOG(WARNING) << "Remote LUA script execution is disabled";
1125       }
1126 
1127       if (lock.GetConfiguration().GetBooleanParameter("WebDavEnabled", true))
1128       {
1129         const bool allowDelete = lock.GetConfiguration().GetBooleanParameter("WebDavDeleteAllowed", false);
1130         const bool allowUpload = lock.GetConfiguration().GetBooleanParameter("WebDavUploadAllowed", true);
1131 
1132         UriComponents root;
1133         root.push_back("webdav");
1134         httpServer.Register(root, new OrthancWebDav(context, allowDelete, allowUpload));
1135       }
1136     }
1137 
1138     MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
1139 
1140     httpServer.SetIncomingHttpRequestFilter(httpFilter);
1141     httpServer.SetHttpExceptionFormatter(exceptionFormatter);
1142     httpServer.Register(context.GetHttpHandler());
1143 
1144     if (httpServer.GetPortNumber() < 1024)
1145     {
1146       LOG(WARNING) << "The HTTP port is privileged ("
1147                    << httpServer.GetPortNumber() << " is below 1024), "
1148                    << "make sure you run Orthanc as root/administrator";
1149     }
1150 
1151     httpServer.Start();
1152 
1153     bool restart = WaitForExit(context, restApi);
1154 
1155     httpServer.Stop();
1156     LOG(WARNING) << "    HTTP server has stopped";
1157 
1158     return restart;
1159   }
1160 }
1161 
1162 
StartDicomServer(ServerContext & context,const OrthancRestApi & restApi,OrthancPlugins * plugins)1163 static bool StartDicomServer(ServerContext& context,
1164                              const OrthancRestApi& restApi,
1165                              OrthancPlugins* plugins)
1166 {
1167   bool dicomServerEnabled;
1168 
1169   {
1170     OrthancConfiguration::ReaderLock lock;
1171     dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
1172   }
1173 
1174   if (!dicomServerEnabled)
1175   {
1176     LOG(WARNING) << "The DICOM server is disabled";
1177     return StartHttpServer(context, restApi, plugins);
1178   }
1179   else
1180   {
1181     MyDicomServerFactory serverFactory(context);
1182     OrthancApplicationEntityFilter dicomFilter(context);
1183     ModalitiesFromConfiguration modalities;
1184 
1185     // Setup the DICOM server
1186     DicomServer dicomServer;
1187     dicomServer.SetRemoteModalities(modalities);
1188     dicomServer.SetStoreRequestHandlerFactory(serverFactory);
1189     dicomServer.SetMoveRequestHandlerFactory(serverFactory);
1190     dicomServer.SetGetRequestHandlerFactory(serverFactory);
1191     dicomServer.SetFindRequestHandlerFactory(serverFactory);
1192     dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
1193 
1194     {
1195       OrthancConfiguration::ReaderLock lock;
1196       dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
1197       dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
1198       dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
1199       dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetOrthancAET());
1200 
1201       // Configuration of DICOM TLS for Orthanc SCP (since Orthanc 1.9.0)
1202       dicomServer.SetDicomTlsEnabled(lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_ENABLED, false));
1203       if (dicomServer.IsDicomTlsEnabled())
1204       {
1205         dicomServer.SetOwnCertificatePath(
1206           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
1207           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
1208         dicomServer.SetTrustedCertificatesPath(
1209           lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
1210       }
1211 
1212       dicomServer.SetMaximumPduLength(lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
1213 
1214       // New option in Orthanc 1.9.3
1215       dicomServer.SetRemoteCertificateRequired(
1216         lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
1217     }
1218 
1219 #if ORTHANC_ENABLE_PLUGINS == 1
1220     if (plugins != NULL)
1221     {
1222       if (plugins->HasWorklistHandler())
1223       {
1224         dicomServer.SetWorklistRequestHandlerFactory(*plugins);
1225       }
1226 
1227       if (plugins->HasFindHandler())
1228       {
1229         dicomServer.SetFindRequestHandlerFactory(*plugins);
1230       }
1231 
1232       if (plugins->HasMoveHandler())
1233       {
1234         dicomServer.SetMoveRequestHandlerFactory(*plugins);
1235       }
1236     }
1237 #endif
1238 
1239     dicomServer.SetApplicationEntityFilter(dicomFilter);
1240 
1241     if (dicomServer.GetPortNumber() < 1024)
1242     {
1243       LOG(WARNING) << "The DICOM port is privileged ("
1244                    << dicomServer.GetPortNumber() << " is below 1024), "
1245                    << "make sure you run Orthanc as root/administrator";
1246     }
1247 
1248     dicomServer.Start();
1249     LOG(WARNING) << "DICOM server listening with AET " << dicomServer.GetApplicationEntityTitle()
1250                  << " on port: " << dicomServer.GetPortNumber();
1251 
1252     bool restart = false;
1253     ErrorCode error = ErrorCode_Success;
1254 
1255     try
1256     {
1257       restart = StartHttpServer(context, restApi, plugins);
1258     }
1259     catch (OrthancException& e)
1260     {
1261       error = e.GetErrorCode();
1262     }
1263 
1264     dicomServer.Stop();
1265     LOG(WARNING) << "    DICOM server has stopped";
1266 
1267     serverFactory.Done();
1268 
1269     if (error != ErrorCode_Success)
1270     {
1271       throw OrthancException(error);
1272     }
1273 
1274     return restart;
1275   }
1276 }
1277 
1278 
ConfigureHttpHandler(ServerContext & context,OrthancPlugins * plugins,bool loadJobsFromDatabase)1279 static bool ConfigureHttpHandler(ServerContext& context,
1280                                  OrthancPlugins *plugins,
1281                                  bool loadJobsFromDatabase)
1282 {
1283 #if ORTHANC_ENABLE_PLUGINS == 1
1284   // By order of priority, first apply the "plugins" layer, so that
1285   // plugins can overwrite the built-in REST API of Orthanc
1286   if (plugins)
1287   {
1288     assert(context.HasPlugins());
1289     context.GetHttpHandler().Register(*plugins, false);
1290   }
1291 #endif
1292 
1293   // Secondly, apply the "static resources" layer
1294 #if ORTHANC_STANDALONE == 1
1295   EmbeddedResourceHttpHandler staticResources("/app", ServerResources::ORTHANC_EXPLORER);
1296 #else
1297   FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
1298 #endif
1299 
1300   // Do not register static resources if orthanc explorer is disabled
1301   bool orthancExplorerEnabled = false;
1302   {
1303     OrthancConfiguration::ReaderLock lock;
1304     orthancExplorerEnabled = lock.GetConfiguration().GetBooleanParameter(
1305         "OrthancExplorerEnabled", true);
1306   }
1307 
1308   if (orthancExplorerEnabled)
1309   {
1310     context.GetHttpHandler().Register(staticResources, false);
1311   }
1312   else
1313   {
1314     LOG(WARNING) << "Orthanc Explorer UI is disabled";
1315   }
1316 
1317   // Thirdly, consider the built-in REST API of Orthanc
1318   OrthancRestApi restApi(context, orthancExplorerEnabled);
1319   context.GetHttpHandler().Register(restApi, true);
1320 
1321   context.SetupJobsEngine(false /* not running unit tests */, loadJobsFromDatabase);
1322 
1323   bool restart = StartDicomServer(context, restApi, plugins);
1324 
1325   context.Stop();
1326 
1327   return restart;
1328 }
1329 
1330 
UpgradeDatabase(IDatabaseWrapper & database,IStorageArea & storageArea)1331 static void UpgradeDatabase(IDatabaseWrapper& database,
1332                             IStorageArea& storageArea)
1333 {
1334   // Upgrade the schema of the database, if needed
1335   unsigned int currentVersion = database.GetDatabaseVersion();
1336 
1337   LOG(WARNING) << "Starting the upgrade of the database schema";
1338   LOG(WARNING) << "Current database version: " << currentVersion;
1339   LOG(WARNING) << "Database version expected by Orthanc: " << ORTHANC_DATABASE_VERSION;
1340 
1341   if (currentVersion == ORTHANC_DATABASE_VERSION)
1342   {
1343     LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
1344     return;
1345   }
1346 
1347   if (currentVersion > ORTHANC_DATABASE_VERSION)
1348   {
1349     throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1350                            "The version of the database schema (" +
1351                            boost::lexical_cast<std::string>(currentVersion) +
1352                            ") is too recent for this version of Orthanc. Please upgrade Orthanc.");
1353   }
1354 
1355   LOG(WARNING) << "Upgrading the database from schema version "
1356                << currentVersion << " to " << ORTHANC_DATABASE_VERSION;
1357 
1358   try
1359   {
1360     database.Upgrade(ORTHANC_DATABASE_VERSION, storageArea);
1361   }
1362   catch (OrthancException&)
1363   {
1364     LOG(ERROR) << "Unable to run the automated upgrade, please use the replication instructions: "
1365                << "http://book.orthanc-server.com/users/replication.html";
1366     throw;
1367   }
1368 
1369   // Sanity check
1370   currentVersion = database.GetDatabaseVersion();
1371   if (ORTHANC_DATABASE_VERSION != currentVersion)
1372   {
1373     throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1374                            "The database schema was not properly upgraded, it is still at version " +
1375                            boost::lexical_cast<std::string>(currentVersion));
1376   }
1377   else
1378   {
1379     LOG(WARNING) << "The database schema was successfully upgraded, "
1380                  << "you can now start Orthanc without the \"--upgrade\" argument";
1381   }
1382 }
1383 
1384 
1385 
1386 namespace
1387 {
1388   class ServerContextConfigurator : public boost::noncopyable
1389   {
1390   private:
1391     ServerContext&   context_;
1392     OrthancPlugins*  plugins_;
1393 
1394   public:
ServerContextConfigurator(ServerContext & context,OrthancPlugins * plugins)1395     ServerContextConfigurator(ServerContext& context,
1396                               OrthancPlugins* plugins) :
1397       context_(context),
1398       plugins_(plugins)
1399     {
1400       {
1401         OrthancConfiguration::WriterLock lock;
1402         lock.GetConfiguration().SetServerIndex(context.GetIndex());
1403       }
1404 
1405 #if ORTHANC_ENABLE_PLUGINS == 1
1406       if (plugins_ != NULL)
1407       {
1408         plugins_->SetServerContext(context_);
1409         context_.SetPlugins(*plugins_);
1410         context_.GetIndex().SetMaxDatabaseRetries(plugins_->GetMaxDatabaseRetries());
1411       }
1412 #endif
1413     }
1414 
~ServerContextConfigurator()1415     ~ServerContextConfigurator()
1416     {
1417       {
1418         OrthancConfiguration::WriterLock lock;
1419         lock.GetConfiguration().ResetServerIndex();
1420       }
1421 
1422 #if ORTHANC_ENABLE_PLUGINS == 1
1423       if (plugins_ != NULL)
1424       {
1425         plugins_->ResetServerContext();
1426         context_.ResetPlugins();
1427       }
1428 #endif
1429     }
1430   };
1431 }
1432 
1433 
ConfigureServerContext(IDatabaseWrapper & database,IStorageArea & storageArea,OrthancPlugins * plugins,bool loadJobsFromDatabase)1434 static bool ConfigureServerContext(IDatabaseWrapper& database,
1435                                    IStorageArea& storageArea,
1436                                    OrthancPlugins *plugins,
1437                                    bool loadJobsFromDatabase)
1438 {
1439   size_t maxCompletedJobs;
1440 
1441   {
1442     OrthancConfiguration::ReaderLock lock;
1443 
1444     // These configuration options must be set before creating the
1445     // ServerContext, otherwise the possible Lua scripts will not be
1446     // able to properly issue HTTP/HTTPS queries
1447     HttpClient::ConfigureSsl(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
1448                              lock.GetConfiguration().InterpretStringParameterAsPath
1449                              (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
1450     HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
1451 
1452     // The value "0" below makes the class HttpClient use its default
1453     // value (DEFAULT_HTTP_TIMEOUT = 60 seconds in Orthanc 1.5.7)
1454     HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
1455 
1456     HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
1457 
1458     DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
1459 
1460     maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
1461 
1462     if (maxCompletedJobs == 0)
1463     {
1464       LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
1465     }
1466 
1467     // Configuration of DICOM TLS for Orthanc SCU (since Orthanc 1.9.0)
1468     DicomAssociationParameters::SetDefaultOwnCertificatePath(
1469       lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_PRIVATE_KEY, ""),
1470       lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_CERTIFICATE, ""));
1471     DicomAssociationParameters::SetDefaultTrustedCertificatesPath(
1472       lock.GetConfiguration().GetStringParameter(KEY_DICOM_TLS_TRUSTED_CERTIFICATES, ""));
1473     DicomAssociationParameters::SetDefaultMaximumPduLength(
1474       lock.GetConfiguration().GetUnsignedIntegerParameter(KEY_MAXIMUM_PDU_LENGTH, 16384));
1475 
1476     // New option in Orthanc 1.9.3
1477     DicomAssociationParameters::SetDefaultRemoteCertificateRequired(
1478       lock.GetConfiguration().GetBooleanParameter(KEY_DICOM_TLS_REMOTE_CERTIFICATE_REQUIRED, true));
1479   }
1480 
1481   ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
1482 
1483   {
1484     OrthancConfiguration::ReaderLock lock;
1485 
1486     context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
1487     context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
1488 
1489     // New option in Orthanc 1.4.2
1490     context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
1491 
1492     try
1493     {
1494       context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
1495     }
1496     catch (...)
1497     {
1498       context.GetIndex().SetMaximumPatientCount(0);
1499     }
1500 
1501     try
1502     {
1503       uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
1504       context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
1505     }
1506     catch (...)
1507     {
1508       context.GetIndex().SetMaximumStorageSize(0);
1509     }
1510   }
1511 
1512   {
1513     ServerContextConfigurator configurator(context, plugins);  // This calls "OrthancConfiguration::SetServerIndex()"
1514 
1515     {
1516       OrthancConfiguration::WriterLock lock;
1517       lock.GetConfiguration().LoadModalitiesAndPeers();
1518     }
1519 
1520     return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
1521   }
1522 }
1523 
1524 
ConfigureDatabase(IDatabaseWrapper & database,IStorageArea & storageArea,OrthancPlugins * plugins,bool upgradeDatabase,bool loadJobsFromDatabase)1525 static bool ConfigureDatabase(IDatabaseWrapper& database,
1526                               IStorageArea& storageArea,
1527                               OrthancPlugins *plugins,
1528                               bool upgradeDatabase,
1529                               bool loadJobsFromDatabase)
1530 {
1531   database.Open();
1532 
1533   unsigned int currentVersion = database.GetDatabaseVersion();
1534 
1535   if (upgradeDatabase)
1536   {
1537     UpgradeDatabase(database, storageArea);
1538     return false;  // Stop and don't restart Orthanc (cf. issue 29)
1539   }
1540   else if (currentVersion != ORTHANC_DATABASE_VERSION)
1541   {
1542     throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
1543                            "The database schema must be upgraded from version " +
1544                            boost::lexical_cast<std::string>(currentVersion) + " to " +
1545                            boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
1546                            ": Please run Orthanc with the \"--upgrade\" argument");
1547   }
1548 
1549   {
1550     static const char* const CHECK_REVISIONS = "CheckRevisions";
1551 
1552     OrthancConfiguration::ReaderLock lock;
1553 
1554     if (lock.GetConfiguration().GetBooleanParameter(CHECK_REVISIONS, false))
1555     {
1556       if (database.HasRevisionsSupport())
1557       {
1558         LOG(INFO) << "Handling of revisions is enabled, and the custom database back-end *has* "
1559                   << "support for revisions of metadata and attachments";
1560       }
1561       else
1562       {
1563         LOG(WARNING) << "The custom database back-end has *no* support for revisions of metadata and attachments, "
1564                      << "but configuration option \"" << CHECK_REVISIONS << "\" is set to \"true\"";
1565       }
1566 
1567       static const char* const STORE_MD5 = "StoreMD5ForAttachments";
1568 
1569       if (!lock.GetConfiguration().GetBooleanParameter(STORE_MD5, true))
1570       {
1571         throw OrthancException(
1572           ErrorCode_ParameterOutOfRange,
1573           "The revision system is enabled by configuration option \"" + std::string(CHECK_REVISIONS) +
1574           "\", but won't work properly for attachments if \"" + std::string(STORE_MD5) + "\" is set to \"false\"");
1575       }
1576     }
1577   }
1578 
1579   bool success = ConfigureServerContext
1580     (database, storageArea, plugins, loadJobsFromDatabase);
1581 
1582   database.Close();
1583 
1584   return success;
1585 }
1586 
1587 
ConfigurePlugins(int argc,char * argv[],bool upgradeDatabase,bool loadJobsFromDatabase)1588 static bool ConfigurePlugins(int argc,
1589                              char* argv[],
1590                              bool upgradeDatabase,
1591                              bool loadJobsFromDatabase)
1592 {
1593   std::unique_ptr<IDatabaseWrapper>  databasePtr;
1594   std::unique_ptr<IStorageArea>  storage;
1595 
1596 #if ORTHANC_ENABLE_PLUGINS == 1
1597   std::string databaseServerIdentifier;
1598   {
1599     OrthancConfiguration::ReaderLock lock;
1600     databaseServerIdentifier = lock.GetConfiguration().GetDatabaseServerIdentifier();
1601   }
1602 
1603   OrthancPlugins plugins(databaseServerIdentifier);
1604   plugins.SetCommandLineArguments(argc, argv);
1605   LoadPlugins(plugins);
1606 
1607   IDatabaseWrapper* database = NULL;
1608   if (plugins.HasDatabaseBackend())
1609   {
1610     LOG(WARNING) << "Using a custom database from plugins";
1611     database = &plugins.GetDatabaseBackend();
1612   }
1613   else
1614   {
1615     databasePtr.reset(CreateDatabaseWrapper());
1616     database = databasePtr.get();
1617   }
1618 
1619   if (plugins.HasStorageArea())
1620   {
1621     LOG(WARNING) << "Using a custom storage area from plugins";
1622     storage.reset(plugins.CreateStorageArea());
1623   }
1624   else
1625   {
1626     storage.reset(CreateStorageArea());
1627   }
1628 
1629   assert(database != NULL);
1630   assert(storage.get() != NULL);
1631 
1632   return ConfigureDatabase(*database, *storage, &plugins,
1633                            upgradeDatabase, loadJobsFromDatabase);
1634 
1635 #elif ORTHANC_ENABLE_PLUGINS == 0
1636   // The plugins are disabled
1637 
1638   databasePtr.reset(CreateDatabaseWrapper());
1639   storage.reset(CreateStorageArea());
1640 
1641   assert(databasePtr.get() != NULL);
1642   assert(storage.get() != NULL);
1643 
1644   return ConfigureDatabase(*databasePtr, *storage, NULL,
1645                            upgradeDatabase, loadJobsFromDatabase);
1646 
1647 #else
1648 #  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
1649 #endif
1650 }
1651 
1652 
StartOrthanc(int argc,char * argv[],bool upgradeDatabase,bool loadJobsFromDatabase)1653 static bool StartOrthanc(int argc,
1654                          char* argv[],
1655                          bool upgradeDatabase,
1656                          bool loadJobsFromDatabase)
1657 {
1658   return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase);
1659 }
1660 
1661 
SetCategoryVerbosity(const Verbosity verbosity,const std::string & category)1662 static bool SetCategoryVerbosity(const Verbosity verbosity,
1663                                  const std::string& category)
1664 {
1665   Logging::LogCategory c;
1666   if (LookupCategory(c, category))
1667   {
1668     SetCategoryVerbosity(c, verbosity);
1669     return true;
1670   }
1671   else
1672   {
1673     return false;
1674   }
1675 }
1676 
1677 
DisplayPerformanceWarning()1678 static bool DisplayPerformanceWarning()
1679 {
1680   (void) DisplayPerformanceWarning;   // Disable warning about unused function
1681   LOG(WARNING) << "Performance warning: Non-release build, runtime debug assertions are turned on";
1682   return true;
1683 }
1684 
1685 
main(int argc,char * argv[])1686 int main(int argc, char* argv[])
1687 {
1688   Logging::Initialize();
1689   SetGlobalVerbosity(Verbosity_Default);
1690 
1691   bool upgradeDatabase = false;
1692   bool loadJobsFromDatabase = true;
1693   const char* configurationFile = NULL;
1694 
1695 
1696   /**
1697    * Parse the command-line options.
1698    **/
1699 
1700   for (int i = 1; i < argc; i++)
1701   {
1702     std::string argument(argv[i]);
1703 
1704     if (argument.empty())
1705     {
1706       // Ignore empty arguments
1707     }
1708     else if (argument[0] != '-')
1709     {
1710       if (configurationFile != NULL)
1711       {
1712         LOG(ERROR) << "More than one configuration path were provided on the command line, aborting";
1713         return -1;
1714       }
1715       else
1716       {
1717         // Use the first argument that does not start with a "-" as
1718         // the configuration file
1719 
1720         // TODO WHAT IS THE ENCODING?
1721         configurationFile = argv[i];
1722       }
1723     }
1724     else if (argument == "--errors")
1725     {
1726       PrintErrors(argv[0]);
1727       return 0;
1728     }
1729     else if (argument == "--help")
1730     {
1731       PrintHelp(argv[0]);
1732       return 0;
1733     }
1734     else if (argument == "--version")
1735     {
1736       PrintVersion(argv[0]);
1737       return 0;
1738     }
1739     else if (argument == "--verbose")
1740     {
1741       SetGlobalVerbosity(Verbosity_Verbose);
1742     }
1743     else if (argument == "--trace")
1744     {
1745       SetGlobalVerbosity(Verbosity_Trace);
1746     }
1747     else if (boost::starts_with(argument, "--verbose-") &&
1748              SetCategoryVerbosity(Verbosity_Verbose, argument.substr(10)))
1749     {
1750       // New in Orthanc 1.8.1
1751     }
1752     else if (boost::starts_with(argument, "--trace-") &&
1753              SetCategoryVerbosity(Verbosity_Trace, argument.substr(8)))
1754     {
1755       // New in Orthanc 1.8.1
1756     }
1757     else if (boost::starts_with(argument, "--logdir="))
1758     {
1759       // TODO WHAT IS THE ENCODING?
1760       const std::string directory = argument.substr(9);
1761 
1762       try
1763       {
1764         Logging::SetTargetFolder(directory);
1765       }
1766       catch (OrthancException&)
1767       {
1768         LOG(ERROR) << "The directory where to store the log files ("
1769                    << directory << ") is inexistent, aborting.";
1770         return -1;
1771       }
1772     }
1773     else if (boost::starts_with(argument, "--logfile="))
1774     {
1775       // TODO WHAT IS THE ENCODING?
1776       const std::string file = argument.substr(10);
1777 
1778       try
1779       {
1780         Logging::SetTargetFile(file);
1781       }
1782       catch (OrthancException&)
1783       {
1784         LOG(ERROR) << "Cannot write to the specified log file ("
1785                    << file << "), aborting.";
1786         return -1;
1787       }
1788     }
1789     else if (argument == "--upgrade")
1790     {
1791       upgradeDatabase = true;
1792     }
1793     else if (argument == "--no-jobs")
1794     {
1795       loadJobsFromDatabase = false;
1796     }
1797     else if (boost::starts_with(argument, "--config="))
1798     {
1799       // TODO WHAT IS THE ENCODING?
1800       std::string configurationSample;
1801       GetFileResource(configurationSample, ServerResources::CONFIGURATION_SAMPLE);
1802 
1803 #if defined(_WIN32)
1804       // Replace UNIX newlines with DOS newlines
1805       boost::replace_all(configurationSample, "\n", "\r\n");
1806 #endif
1807 
1808       std::string target = argument.substr(9);
1809 
1810       try
1811       {
1812         if (target == "-")
1813         {
1814           // New in 1.5.8: Print to stdout
1815           std::cout << configurationSample;
1816         }
1817         else
1818         {
1819           SystemToolbox::WriteFile(configurationSample, target);
1820         }
1821         return 0;
1822       }
1823       catch (OrthancException&)
1824       {
1825         LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
1826         return -1;
1827       }
1828     }
1829     else if (boost::starts_with(argument, "--openapi="))
1830     {
1831       std::string target = argument.substr(10);
1832 
1833       try
1834       {
1835         Json::Value openapi;
1836 
1837         {
1838           SQLiteDatabaseWrapper inMemoryDatabase;
1839           inMemoryDatabase.Open();
1840           MemoryStorageArea inMemoryStorage;
1841           ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
1842           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
1843           restApi.GenerateOpenApiDocumentation(openapi);
1844           context.Stop();
1845         }
1846 
1847         openapi["info"]["version"] = ORTHANC_VERSION;
1848         openapi["info"]["title"] = "Orthanc API";
1849         openapi["info"]["description"] =
1850           "This is the full documentation of the [REST API](https://book.orthanc-server.com/users/rest.html) "
1851           "of Orthanc.<p>This reference is automatically generated from the source code of Orthanc. A "
1852           "[shorter cheat sheet](https://book.orthanc-server.com/users/rest-cheatsheet.html) is part of "
1853           "the Orthanc Book.<p>An earlier, manually crafted version from August 2019, is [still available]"
1854           "(2019-08-orthanc-openapi.html), but is not up-to-date anymore ([source]"
1855           "(https://groups.google.com/g/orthanc-users/c/NUiJTEICSl8/m/xKeqMrbqAAAJ)).";
1856 
1857         Json::Value server = Json::objectValue;
1858         server["url"] = "https://demo.orthanc-server.com/";
1859         openapi["servers"].append(server);
1860 
1861         std::string s;
1862         Toolbox::WriteStyledJson(s, openapi);
1863 
1864         if (target == "-")
1865         {
1866           std::cout << s;   // Print to stdout
1867         }
1868         else
1869         {
1870           SystemToolbox::WriteFile(s, target);
1871         }
1872         return 0;
1873       }
1874       catch (OrthancException&)
1875       {
1876         LOG(ERROR) << "Cannot export OpenAPI documentation as file \"" << target << "\"";
1877         return -1;
1878       }
1879     }
1880     else if (boost::starts_with(argument, "--cheatsheet="))
1881     {
1882       std::string target = argument.substr(13);
1883 
1884       try
1885       {
1886         std::string cheatsheet;
1887 
1888         {
1889           SQLiteDatabaseWrapper inMemoryDatabase;
1890           inMemoryDatabase.Open();
1891           MemoryStorageArea inMemoryStorage;
1892           ServerContext context(inMemoryDatabase, inMemoryStorage, true /* unit testing */, 0 /* max completed jobs */);
1893           OrthancRestApi restApi(context, false /* no Orthanc Explorer */);
1894           restApi.GenerateReStructuredTextCheatSheet(cheatsheet, "https://api.orthanc-server.com/index.html");
1895           context.Stop();
1896         }
1897 
1898         if (target == "-")
1899         {
1900           std::cout << cheatsheet;   // Print to stdout
1901         }
1902         else
1903         {
1904           SystemToolbox::WriteFile(cheatsheet, target);
1905         }
1906         return 0;
1907       }
1908       catch (OrthancException&)
1909       {
1910         LOG(ERROR) << "Cannot export REST cheat sheet as file \"" << target << "\"";
1911         return -1;
1912       }
1913     }
1914     else
1915     {
1916       LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;
1917     }
1918   }
1919 
1920 
1921   /**
1922    * Launch Orthanc.
1923    **/
1924 
1925   {
1926     std::string version(ORTHANC_VERSION);
1927 
1928     if (std::string(ORTHANC_VERSION) == "mainline")
1929     {
1930       try
1931       {
1932         boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
1933         std::time_t creation = boost::filesystem::last_write_time(exe);
1934         boost::posix_time::ptime converted(boost::posix_time::from_time_t(creation));
1935         version += " (" + boost::posix_time::to_iso_string(converted) + ")";
1936       }
1937       catch (...)
1938       {
1939       }
1940     }
1941 
1942     LOG(WARNING) << "Orthanc version: " << version;
1943     assert(DisplayPerformanceWarning());
1944 
1945     std::string s = "Architecture: ";
1946     if (sizeof(void*) == 4)
1947     {
1948       s += "32-bit, ";
1949     }
1950     else if (sizeof(void*) == 8)
1951     {
1952       s += "64-bit, ";
1953     }
1954     else
1955     {
1956       s += "unsupported pointer size, ";
1957     }
1958 
1959     switch (Toolbox::DetectEndianness())
1960     {
1961       case Endianness_Little:
1962         s += "little endian";
1963         break;
1964 
1965       case Endianness_Big:
1966         s += "big endian";
1967         break;
1968 
1969       default:
1970         s += "unsupported endianness";
1971         break;
1972     }
1973 
1974     LOG(INFO) << s;
1975   }
1976 
1977   int status = 0;
1978   try
1979   {
1980     for (;;)
1981     {
1982       OrthancInitialize(configurationFile);
1983 
1984       bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
1985       if (restart)
1986       {
1987         OrthancFinalize();
1988         LOG(WARNING) << "Logging system is resetting";
1989         Logging::Reset();
1990       }
1991       else
1992       {
1993         break;
1994       }
1995     }
1996   }
1997   catch (const OrthancException& e)
1998   {
1999     LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")";
2000 #if defined(_WIN32)
2001     if (e.GetErrorCode() >= ErrorCode_START_PLUGINS)
2002     {
2003       status = static_cast<int>(ErrorCode_Plugin);
2004     }
2005     else
2006     {
2007       status = static_cast<int>(e.GetErrorCode());
2008     }
2009 
2010 #else
2011     status = -1;
2012 #endif
2013   }
2014   catch (const std::exception& e)
2015   {
2016     LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]";
2017     status = -1;
2018   }
2019   catch (const std::string& s)
2020   {
2021     LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]";
2022     status = -1;
2023   }
2024   catch (...)
2025   {
2026     LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any.";
2027     status = -1;
2028   }
2029 
2030   LOG(WARNING) << "Orthanc has stopped";
2031 
2032   OrthancFinalize();
2033 
2034   return status;
2035 }
2036