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