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