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 Affero General Public License
9  * as published by the Free Software Foundation, either version 3 of
10  * the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program. If not, see <http://www.gnu.org/licenses/>.
19  **/
20 
21 
22 #include "MySQLDatabase.h"
23 
24 #include "../Common/ImplicitTransaction.h"
25 #include "../Common/Integer64Value.h"
26 #include "../Common/RetryDatabaseFactory.h"
27 #include "MySQLResult.h"
28 #include "MySQLStatement.h"
29 #include "MySQLTransaction.h"
30 
31 #include <Compatibility.h>  // For std::unique_ptr<>
32 #include <Logging.h>
33 #include <OrthancException.h>
34 #include <Toolbox.h>
35 
36 #include <errmsg.h>
37 #include <mysqld_error.h>
38 
39 #include <memory>
40 #include <boost/thread.hpp>
41 
42 namespace OrthancDatabases
43 {
Close()44   void MySQLDatabase::Close()
45   {
46     if (mysql_ != NULL)
47     {
48       LOG(INFO) << "Closing connection to MySQL database";
49       mysql_close(mysql_);
50       mysql_ = NULL;
51     }
52   }
53 
54 
ThrowException()55   void MySQLDatabase::ThrowException()
56   {
57     LogError();
58 
59     unsigned int error = mysql_errno(mysql_);
60     if (error == CR_SERVER_GONE_ERROR ||
61         error == CR_SERVER_LOST ||
62         error == ER_QUERY_INTERRUPTED)
63     {
64       throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable);
65     }
66     else if (error == CR_COMMANDS_OUT_OF_SYNC)
67     {
68 #if !defined(MARIADB_VERSION_ID)
69       LOG(ERROR) << "TODO - This error seems to be related to the use of libmysqlclient: Try to switch to mariadb-connector";
70 #endif
71 
72       throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable);
73     }
74     else if (error == ER_LOCK_DEADLOCK)
75     {
76 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 9, 2)
77       throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseCannotSerialize);
78 #else
79       throw Orthanc::OrthancException(Orthanc::ErrorCode_Database, "Collision between multiple writers");
80 #endif
81     }
82     else
83     {
84       throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
85     }
86   }
87 
88 
CheckErrorCode(int code)89   void MySQLDatabase::CheckErrorCode(int code)
90   {
91     if (code == 0)
92     {
93       return;
94     }
95     else
96     {
97       ThrowException();
98     }
99   }
100 
101 
MySQLDatabase(const MySQLParameters & parameters)102   MySQLDatabase::MySQLDatabase(const MySQLParameters& parameters) :
103     parameters_(parameters),
104     mysql_(NULL)
105   {
106   }
107 
108 
~MySQLDatabase()109   MySQLDatabase::~MySQLDatabase()
110   {
111     try
112     {
113       Close();
114     }
115     catch (Orthanc::OrthancException&)
116     {
117       // Ignore possible exceptions due to connection loss
118     }
119   }
120 
121 
LogError()122   void MySQLDatabase::LogError()
123   {
124     if (mysql_ != NULL)
125     {
126       LOG(ERROR) << "MySQL error (" << mysql_errno(mysql_)
127                  << "," << mysql_sqlstate(mysql_)
128                  << "): " << mysql_error(mysql_);
129     }
130   }
131 
132 
GetObject()133   MYSQL* MySQLDatabase::GetObject()
134   {
135     if (mysql_ == NULL)
136     {
137       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
138     }
139     else
140     {
141       return mysql_;
142     }
143   }
144 
145 
OpenInternal(const char * db)146   void MySQLDatabase::OpenInternal(const char* db)
147   {
148     if (mysql_ != NULL)
149     {
150       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
151     }
152 
153     mysql_ = mysql_init(NULL);
154     if (mysql_ == NULL)
155     {
156       LOG(ERROR) << "Cannot initialize the MySQL connector";
157       throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
158     }
159 
160     if (parameters_.GetUnixSocket().empty())
161     {
162       // Fallback to TCP connection if no UNIX socket is provided
163       unsigned int protocol = MYSQL_PROTOCOL_TCP;
164       mysql_options(mysql_, MYSQL_OPT_PROTOCOL, (unsigned int *) &protocol);
165     }
166 
167     if (parameters_.IsSsl())
168     {
169       if (parameters_.IsVerifyServerCertificates())
170       {
171 #if (MYSQL_VERSION_ID > 50110 && MYSQL_VERSION_ID < 80000)  // Removed in MySQL client 8.0
172         my_bool verifyCert = 1;
173         mysql_options(mysql_, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, (void *) &verifyCert);
174 #endif
175 
176         mysql_options(mysql_, MYSQL_OPT_SSL_CA, (void *)(parameters_.GetSslCaCertificates()));
177       }
178 
179 #if (MYSQL_VERSION_ID > 50110 && MYSQL_VERSION_ID < 80000)  // Removed in MySQL client 8.0
180       my_bool enforceTls = 1;
181       mysql_options(mysql_, MYSQL_OPT_SSL_ENFORCE, (void *) &enforceTls);
182 #endif
183     }
184 
185     const char* socket = (parameters_.GetUnixSocket().empty() ? NULL :
186                           parameters_.GetUnixSocket().c_str());
187 
188     if (mysql_real_connect(mysql_,
189                            parameters_.GetHost().c_str(),
190                            parameters_.GetUsername().c_str(),
191                            parameters_.GetPassword().c_str(), db,
192                            parameters_.GetPort(), socket, 0) == 0)
193     {
194       LogError();
195       Close();
196       throw Orthanc::OrthancException(Orthanc::ErrorCode_DatabaseUnavailable);
197     }
198     else
199     {
200       LOG(INFO) << "Successful connection to MySQL database";
201     }
202 
203     if (mysql_set_character_set(mysql_, "utf8mb4") != 0)
204     {
205       LOG(ERROR) << "Cannot set the character set to UTF8";
206       Close();
207       throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
208     }
209   }
210 
211 
Open()212   void MySQLDatabase::Open()
213   {
214     if (parameters_.GetDatabase().empty())
215     {
216       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
217     }
218     else
219     {
220       OpenInternal(parameters_.GetDatabase().c_str());
221     }
222   }
223 
224 
ClearDatabase(const MySQLParameters & parameters)225   void MySQLDatabase::ClearDatabase(const MySQLParameters& parameters)
226   {
227     MySQLDatabase db(parameters);
228     db.OpenRoot();
229 
230     const std::string& database = parameters.GetDatabase();
231 
232     {
233       MySQLTransaction t(db, TransactionType_ReadWrite);
234 
235       if (!db.DoesDatabaseExist(t, database))
236       {
237         LOG(ERROR) << "Inexistent database, please create it first: " << database;
238         throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
239       }
240 
241       db.ExecuteMultiLines("DROP DATABASE " + database, false);
242       db.ExecuteMultiLines("CREATE DATABASE " + database, false);
243       t.Commit();
244     }
245   }
246 
247 
248   namespace
249   {
250     class ResultWrapper : public boost::noncopyable
251     {
252     private:
253       MYSQL_RES *result_;
254 
255     public:
ResultWrapper(MySQLDatabase & mysql,const std::string & sql)256       ResultWrapper(MySQLDatabase& mysql,
257                     const std::string& sql) :
258         result_(NULL)
259       {
260         if (mysql_real_query(mysql.GetObject(), sql.c_str(), sql.size()))
261         {
262           mysql.LogError();
263           throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
264         }
265 
266         result_ = mysql_use_result(mysql.GetObject());
267         if (result_ == NULL)
268         {
269           mysql.LogError();
270           throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
271         }
272       }
273 
~ResultWrapper()274       ~ResultWrapper()
275       {
276         if (result_ != NULL)
277         {
278           mysql_free_result(result_);
279           result_ = NULL;
280         }
281       }
282 
GetObject()283       MYSQL_RES *GetObject()
284       {
285         return result_;
286       }
287     };
288   }
289 
290 
LookupGlobalStringVariable(std::string & value,const std::string & variable)291   bool MySQLDatabase::LookupGlobalStringVariable(std::string& value,
292                                                  const std::string& variable)
293   {
294     ResultWrapper result(*this, "SELECT @@global." + variable);
295 
296     MYSQL_ROW row = mysql_fetch_row(result.GetObject());
297     if (mysql_errno(mysql_) == 0 &&
298         row &&
299         row[0])
300     {
301       value = std::string(row[0]);
302       return true;
303     }
304     else
305     {
306       return false;
307     }
308   }
309 
310 
LookupGlobalIntegerVariable(int64_t & value,const std::string & variable)311   bool MySQLDatabase::LookupGlobalIntegerVariable(int64_t& value,
312                                                   const std::string& variable)
313   {
314     std::string s;
315 
316     if (LookupGlobalStringVariable(s, variable))
317     {
318       try
319       {
320         value = boost::lexical_cast<int64_t>(s);
321         return true;
322       }
323       catch (boost::bad_lexical_cast&)
324       {
325         throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
326       }
327     }
328     else
329     {
330       return false;
331     }
332   }
333 
334 
RunAdvisoryLockStatement(Query & query,const std::string & lock)335   bool MySQLDatabase::RunAdvisoryLockStatement(Query& query,
336                                                const std::string& lock)
337   {
338     const std::string& dbName = parameters_.GetDatabase();
339 
340     // Prepend the name of the lock by the database name. This allows
341     // to create a namespace for advisory locks:
342     // https://groups.google.com/d/msg/orthanc-users/yV3LSTh_TjI/MQIcvnMlAQAJ
343     std::string prefix;
344     prefix.reserve(dbName.size());
345     for (size_t i = 0; i < dbName.size(); i++)
346     {
347       if (isalnum(dbName[i]) ||
348           dbName[i] == '$' ||
349           dbName[i] == '_')
350       {
351         prefix.push_back(dbName[i]);
352       }
353     }
354 
355     query.SetType("lock", ValueType_Utf8String);
356 
357     Dictionary args;
358     args.SetUtf8Value("lock", prefix + "." + lock);
359 
360     bool success;
361 
362     {
363       MySQLStatement statement(*this, query);
364 
365       MySQLTransaction t(*this, TransactionType_ReadWrite);
366       std::unique_ptr<IResult> result(t.Execute(statement, args));
367 
368       success = (!result->IsDone() &&
369                  result->GetField(0).GetType() == ValueType_Integer64 &&
370                  dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() == 1);
371 
372       t.Commit();
373     }
374 
375     return success;
376   }
377 
378 
AcquireAdvisoryLock(const std::string & lock)379   bool MySQLDatabase::AcquireAdvisoryLock(const std::string& lock)
380   {
381     Query query("SELECT GET_LOCK(${lock}, 0)", false);
382     return RunAdvisoryLockStatement(query, lock);
383   }
384 
385 
ReleaseAdvisoryLock(const std::string & lock)386   bool MySQLDatabase::ReleaseAdvisoryLock(const std::string& lock)
387   {
388     Query query("SELECT RELEASE_LOCK(${lock})", false);
389     return RunAdvisoryLockStatement(query, lock);
390   }
391 
392 
AdvisoryLock(const std::string & lock)393   void MySQLDatabase::AdvisoryLock(const std::string& lock)
394   {
395     if (!AcquireAdvisoryLock(lock))
396     {
397       LOG(ERROR) << "The MySQL database is locked by another instance of Orthanc";
398       throw Orthanc::OrthancException(Orthanc::ErrorCode_Database);
399     }
400   }
401 
402 
DoesTableExist(MySQLTransaction & transaction,const std::string & name)403   bool MySQLDatabase::DoesTableExist(MySQLTransaction& transaction,
404                                      const std::string& name)
405   {
406     if (mysql_ == NULL)
407     {
408       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
409     }
410 
411     if (!IsValidDatabaseIdentifier(name))
412     {
413       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
414     }
415 
416     Query query("SELECT COUNT(*) FROM information_schema.TABLES WHERE "
417                 "(TABLE_SCHEMA = ${database}) AND (TABLE_NAME = ${table})", true);
418     query.SetType("database", ValueType_Utf8String);
419     query.SetType("table", ValueType_Utf8String);
420 
421     MySQLStatement statement(*this, query);
422 
423     Dictionary args;
424     args.SetUtf8Value("database", parameters_.GetDatabase());
425     args.SetUtf8Value("table", name);
426 
427     std::unique_ptr<IResult> result(statement.Execute(transaction, args));
428     return (!result->IsDone() &&
429             result->GetFieldsCount() == 1 &&
430             result->GetField(0).GetType() == ValueType_Integer64 &&
431             dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() == 1);
432   }
433 
434 
DoesDatabaseExist(MySQLTransaction & transaction,const std::string & name)435   bool MySQLDatabase::DoesDatabaseExist(MySQLTransaction& transaction,
436                                         const std::string& name)
437   {
438     if (mysql_ == NULL)
439     {
440       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
441     }
442 
443     if (!IsValidDatabaseIdentifier(name))
444     {
445       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
446     }
447 
448     Query query("SELECT COUNT(*) FROM information_schema.SCHEMATA "
449                 "WHERE SCHEMA_NAME = ${database}", true);
450     query.SetType("database", ValueType_Utf8String);
451 
452     MySQLStatement statement(*this, query);
453 
454     Dictionary args;
455     args.SetUtf8Value("database", name);
456 
457     std::unique_ptr<IResult> result(statement.Execute(transaction, args));
458     return (!result->IsDone() &&
459             result->GetFieldsCount() == 1 &&
460             result->GetField(0).GetType() == ValueType_Integer64 &&
461             dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() == 1);
462   }
463 
464 
DoesTriggerExist(MySQLTransaction & transaction,const std::string & name)465   bool MySQLDatabase::DoesTriggerExist(MySQLTransaction& transaction,
466                                        const std::string& name)
467   {
468     if (mysql_ == NULL)
469     {
470       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
471     }
472 
473     if (!IsValidDatabaseIdentifier(name))
474     {
475       throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
476     }
477 
478     Query query("SELECT COUNT(*) FROM information_schema.TRIGGERS "
479                 "WHERE TRIGGER_NAME = ${trigger}", true);
480     query.SetType("trigger", ValueType_Utf8String);
481 
482     MySQLStatement statement(*this, query);
483 
484     Dictionary args;
485     args.SetUtf8Value("trigger", name);
486 
487     std::unique_ptr<IResult> result(statement.Execute(transaction, args));
488     return (!result->IsDone() &&
489             result->GetFieldsCount() == 1 &&
490             result->GetField(0).GetType() == ValueType_Integer64 &&
491             dynamic_cast<const Integer64Value&>(result->GetField(0)).GetValue() != 0);
492   }
493 
494 
ExecuteMultiLines(const std::string & sql,bool arobaseSeparator)495   void MySQLDatabase::ExecuteMultiLines(const std::string& sql,
496                                         bool arobaseSeparator)
497   {
498     if (mysql_ == NULL)
499     {
500       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
501     }
502 
503     // This emulates the behavior of "CLIENT_MULTI_STATEMENTS" in
504     // "mysql_real_connect()", avoiding to implement a loop over
505     // "mysql_query()"
506     std::vector<std::string> commands;
507     Orthanc::Toolbox::TokenizeString(commands, sql, ';');
508 
509     for (size_t i = 0; i < commands.size(); i++)
510     {
511       std::string s = Orthanc::Toolbox::StripSpaces(commands[i]);
512 
513       if (!s.empty())
514       {
515         if (arobaseSeparator)
516         {
517           // Replace the escape character "@" by a semicolon
518           std::replace(s.begin(), s.end(), '@', ';');
519         }
520 
521         LOG(TRACE) << "MySQL: " << s;
522         CheckErrorCode(mysql_query(mysql_, s.c_str()));
523       }
524     }
525   }
526 
527 
Compile(const Query & query)528   IPrecompiledStatement* MySQLDatabase::Compile(const Query& query)
529   {
530     if (mysql_ == NULL)
531     {
532       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
533     }
534 
535     return new MySQLStatement(*this, query);
536   }
537 
538 
539 
540   namespace
541   {
542     class MySQLImplicitTransaction : public ImplicitTransaction
543     {
544     private:
545       MySQLDatabase& db_;
546 
547     protected:
ExecuteInternal(IPrecompiledStatement & statement,const Dictionary & parameters)548       virtual IResult* ExecuteInternal(IPrecompiledStatement& statement,
549                                        const Dictionary& parameters) ORTHANC_OVERRIDE
550       {
551         return dynamic_cast<MySQLStatement&>(statement).Execute(*this, parameters);
552       }
553 
ExecuteWithoutResultInternal(IPrecompiledStatement & statement,const Dictionary & parameters)554       virtual void ExecuteWithoutResultInternal(IPrecompiledStatement& statement,
555                                                 const Dictionary& parameters) ORTHANC_OVERRIDE
556       {
557         dynamic_cast<MySQLStatement&>(statement).ExecuteWithoutResult(*this, parameters);
558       }
559 
560     public:
MySQLImplicitTransaction(MySQLDatabase & db)561       explicit MySQLImplicitTransaction(MySQLDatabase& db) :
562         db_(db)
563       {
564       }
565 
DoesTableExist(const std::string & name)566       virtual bool DoesTableExist(const std::string& name) ORTHANC_OVERRIDE
567       {
568         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "An explicit transaction is needed");
569       }
570 
DoesTriggerExist(const std::string & name)571       virtual bool DoesTriggerExist(const std::string& name) ORTHANC_OVERRIDE
572       {
573         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "An explicit transaction is needed");
574       }
575 
ExecuteMultiLines(const std::string & query)576       virtual void ExecuteMultiLines(const std::string& query) ORTHANC_OVERRIDE
577       {
578         db_.ExecuteMultiLines(query, false /* don't deal with arobases */);
579       }
580     };
581   }
582 
583 
CreateTransaction(TransactionType type)584   ITransaction* MySQLDatabase::CreateTransaction(TransactionType type)
585   {
586     if (mysql_ == NULL)
587     {
588       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
589     }
590 
591     switch (type)
592     {
593       case TransactionType_Implicit:
594         return new MySQLImplicitTransaction(*this);
595 
596       case TransactionType_ReadOnly:
597       case TransactionType_ReadWrite:
598         return new MySQLTransaction(*this, type);
599 
600       default:
601         throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
602     }
603   }
604 
605 
GlobalFinalization()606   void MySQLDatabase::GlobalFinalization()
607   {
608     mysql_library_end();
609   }
610 
611 
IsValidDatabaseIdentifier(const std::string & s)612   bool MySQLDatabase::IsValidDatabaseIdentifier(const std::string& s)
613   {
614     for (size_t i = 0; i < s.length(); i++)
615     {
616       // https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
617       if (!isalnum(s[i]) &&
618           s[i] != '$' &&
619           s[i] != '_')
620       {
621         return false;
622       }
623     }
624 
625     return true;
626   }
627 
628 
629   MySQLDatabase::TransientAdvisoryLock::
TransientAdvisoryLock(MySQLDatabase & database,const std::string & lock)630   TransientAdvisoryLock(MySQLDatabase&  database,
631                         const std::string& lock) :
632     database_(database),
633     lock_(lock)
634   {
635     bool locked = true;
636 
637     for (unsigned int i = 0; i < 10; i++)
638     {
639       if (database_.AcquireAdvisoryLock(lock_))
640       {
641         locked = false;
642         break;
643       }
644       else
645       {
646         boost::this_thread::sleep(boost::posix_time::milliseconds(500));
647       }
648     }
649 
650     if (locked)
651     {
652       LOG(ERROR) << "Cannot acquire a transient advisory lock";
653       throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
654     }
655   }
656 
657 
~TransientAdvisoryLock()658   MySQLDatabase::TransientAdvisoryLock::~TransientAdvisoryLock()
659   {
660     database_.ReleaseAdvisoryLock(lock_);
661   }
662 
663 
664   class MySQLDatabase::Factory : public RetryDatabaseFactory
665   {
666   private:
667     MySQLParameters  parameters_;
668 
669   protected:
TryOpen()670     virtual IDatabase* TryOpen()
671     {
672       std::unique_ptr<MySQLDatabase> db(new MySQLDatabase(parameters_));
673       db->Open();
674       db->ExecuteMultiLines("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE", false);
675       return db.release();
676     }
677 
678   public:
Factory(const MySQLParameters & parameters)679     explicit Factory(const MySQLParameters& parameters) :
680       RetryDatabaseFactory(parameters.GetMaxConnectionRetries(),
681                            parameters.GetConnectionRetryInterval()),
682       parameters_(parameters)
683     {
684     }
685   };
686 
687 
CreateDatabaseFactory(const MySQLParameters & parameters)688   IDatabaseFactory* MySQLDatabase::CreateDatabaseFactory(const MySQLParameters& parameters)
689   {
690     return new Factory(parameters);
691   }
692 
693 
CreateDatabaseConnection(const MySQLParameters & parameters)694   MySQLDatabase* MySQLDatabase::CreateDatabaseConnection(const MySQLParameters& parameters)
695   {
696     Factory factory(parameters);
697     return dynamic_cast<MySQLDatabase*>(factory.Open());
698   }
699 }
700