1 /** 2 MongoDB client connection settings. 3 4 Copyright: © 2012-2016 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module vibe.db.mongo.settings; 9 10 import vibe.core.log; 11 import vibe.data.bson; 12 deprecated import vibe.db.mongo.flags : QueryFlags; 13 import vibe.inet.webform; 14 15 import core.time; 16 import std.conv : to; 17 import std.digest : toHexString; 18 import std.digest.md : md5Of; 19 import std.algorithm : splitter, startsWith; 20 import std.string : icmp, indexOf, toLower; 21 22 23 /** 24 * Parses the given string as a mongodb URL. The URL must be in the form documented at 25 * $(LINK http://www.mongodb.org/display/DOCS/Connections) which is: 26 * 27 * mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] 28 * 29 * Returns: true if the URL was successfully parsed. False if the URL can not be parsed. 30 * 31 * If the URL is successfully parsed the MongoClientSettings instance will contain the parsed config. 32 * If the URL is not successfully parsed the information in the MongoClientSettings instance may be 33 * incomplete and should not be used. 34 */ 35 bool parseMongoDBUrl(out MongoClientSettings cfg, string url) 36 @safe { 37 import std.exception : enforce; 38 39 cfg = new MongoClientSettings(); 40 41 string tmpUrl = url[0..$]; // Slice of the URL (not a copy) 42 43 if( !startsWith(tmpUrl, "mongodb://") ) 44 { 45 return false; 46 } 47 48 // Reslice to get rid of 'mongodb://' 49 tmpUrl = tmpUrl[10..$]; 50 51 auto authIndex = tmpUrl.indexOf('@'); 52 sizediff_t hostIndex = 0; // Start of the host portion of the URL. 53 54 // Parse out the username and optional password. 55 if( authIndex != -1 ) 56 { 57 // Set the host start to after the '@' 58 hostIndex = authIndex + 1; 59 string password; 60 61 auto colonIndex = tmpUrl[0..authIndex].indexOf(':'); 62 if(colonIndex != -1) 63 { 64 cfg.username = tmpUrl[0..colonIndex]; 65 password = tmpUrl[colonIndex + 1 .. authIndex]; 66 } else { 67 cfg.username = tmpUrl[0..authIndex]; 68 } 69 70 // Make sure the username is not empty. If it is then the parse failed. 71 if(cfg.username.length == 0) 72 { 73 return false; 74 } 75 76 cfg.digest = MongoClientSettings.makeDigest(cfg.username, password); 77 } 78 79 auto slashIndex = tmpUrl[hostIndex..$].indexOf("/"); 80 if( slashIndex == -1 ) slashIndex = tmpUrl.length; 81 else slashIndex += hostIndex; 82 83 // Parse the hosts section. 84 try 85 { 86 foreach(entry; splitter(tmpUrl[hostIndex..slashIndex], ",")) 87 { 88 auto hostPort = splitter(entry, ":"); 89 string host = hostPort.front; 90 hostPort.popFront(); 91 ushort port = MongoClientSettings.defaultPort; 92 if (!hostPort.empty) { 93 port = to!ushort(hostPort.front); 94 hostPort.popFront(); 95 } 96 enforce(hostPort.empty, "Host specifications are expected to be of the form \"HOST:PORT,HOST:PORT,...\"."); 97 cfg.hosts ~= MongoHost(host, port); 98 } 99 } catch (Exception e) { 100 return false; // Probably failed converting the port to ushort. 101 } 102 103 // If we couldn't parse a host we failed. 104 if(cfg.hosts.length == 0) 105 { 106 return false; 107 } 108 109 if(slashIndex == tmpUrl.length) 110 { 111 // We're done parsing. 112 return true; 113 } 114 115 auto queryIndex = tmpUrl[slashIndex..$].indexOf("?"); 116 if(queryIndex == -1){ 117 // No query string. Remaining string is the database 118 queryIndex = tmpUrl.length; 119 } else { 120 queryIndex += slashIndex; 121 } 122 123 cfg.database = tmpUrl[slashIndex+1..queryIndex]; 124 if(queryIndex != tmpUrl.length) 125 { 126 FormFields options; 127 parseURLEncodedForm(tmpUrl[queryIndex+1 .. $], options); 128 foreach (option, value; options.byKeyValue) { 129 bool setBool(ref bool dst) 130 { 131 try { 132 dst = to!bool(value); 133 return true; 134 } catch( Exception e ){ 135 logError("Value for '%s' must be 'true' or 'false' but was '%s'.", option, value); 136 return false; 137 } 138 } 139 140 bool setLong(ref long dst) 141 { 142 try { 143 dst = to!long(value); 144 return true; 145 } catch( Exception e ){ 146 logError("Value for '%s' must be an integer but was '%s'.", option, value); 147 return false; 148 } 149 } 150 151 bool setMsecs(ref Duration dst) 152 { 153 try { 154 dst = to!long(value).msecs; 155 return true; 156 } catch( Exception e ){ 157 logError("Value for '%s' must be an integer but was '%s'.", option, value); 158 return false; 159 } 160 } 161 162 void warnNotImplemented() 163 { 164 logDiagnostic("MongoDB option %s not yet implemented.", option); 165 } 166 167 switch( option.toLower() ){ 168 import std.string : split; 169 170 default: logWarn("Unknown MongoDB option %s", option); break; 171 case "appname": cfg.appName = value; break; 172 case "replicaset": cfg.replicaSet = value; warnNotImplemented(); break; 173 case "safe": setBool(cfg.safe); break; 174 case "fsync": setBool(cfg.fsync); break; 175 case "journal": setBool(cfg.journal); break; 176 case "connecttimeoutms": setMsecs(cfg.connectTimeout); break; 177 case "sockettimeoutms": setMsecs(cfg.socketTimeout); break; 178 case "tls": setBool(cfg.ssl); break; 179 case "ssl": setBool(cfg.ssl); break; 180 case "sslverifycertificate": setBool(cfg.sslverifycertificate); break; 181 case "authmechanism": cfg.authMechanism = parseAuthMechanism(value); break; 182 case "authmechanismproperties": cfg.authMechanismProperties = value.split(","); warnNotImplemented(); break; 183 case "authsource": cfg.authSource = value; break; 184 case "wtimeoutms": setLong(cfg.wTimeoutMS); break; 185 case "w": 186 try { 187 if(icmp(value, "majority") == 0){ 188 cfg.w = Bson("majority"); 189 } else { 190 cfg.w = Bson(to!long(value)); 191 } 192 } catch (Exception e) { 193 logError("Invalid w value: [%s] Should be an integer number or 'majority'", value); 194 } 195 break; 196 } 197 } 198 199 /* Some m_settings imply safe. If they are set, set safe to true regardless 200 * of what it was set to in the URL string 201 */ 202 if( (cfg.w != Bson.init) || (cfg.wTimeoutMS != long.init) || 203 cfg.journal || cfg.fsync ) 204 { 205 cfg.safe = true; 206 } 207 } 208 209 return true; 210 } 211 212 /// parseMongoDBUrl parses minimal localhost URL with all defaults 213 unittest 214 { 215 MongoClientSettings cfg; 216 217 assert(parseMongoDBUrl(cfg, "mongodb://localhost")); 218 assert(cfg.hosts.length == 1); 219 assert(cfg.database == ""); 220 assert(cfg.hosts[0].name == "localhost"); 221 assert(cfg.hosts[0].port == 27017); 222 assert(cfg.replicaSet == ""); 223 assert(cfg.safe == false); 224 assert(cfg.w == Bson.init); 225 assert(cfg.wTimeoutMS == long.init); 226 assert(cfg.fsync == false); 227 assert(cfg.journal == false); 228 assert(cfg.connectTimeoutMS == 10_000); 229 assert(cfg.socketTimeoutMS == long.init); 230 assert(cfg.ssl == bool.init); 231 assert(cfg.sslverifycertificate == true); 232 } 233 234 /// parseMongoDBUrl parses URL with username and password 235 unittest 236 { 237 MongoClientSettings cfg; 238 239 assert(parseMongoDBUrl(cfg, "mongodb://fred:foobar@localhost")); 240 assert(cfg.username == "fred"); 241 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "foobar")); 242 assert(cfg.hosts.length == 1); 243 assert(cfg.database == ""); 244 assert(cfg.hosts[0].name == "localhost"); 245 assert(cfg.hosts[0].port == 27017); 246 } 247 248 /// parseMongoDBUrl parses URL with empty password and database 249 unittest 250 { 251 MongoClientSettings cfg; 252 253 assert(parseMongoDBUrl(cfg, "mongodb://fred:@localhost/baz")); 254 assert(cfg.username == "fred"); 255 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "")); 256 assert(cfg.database == "baz"); 257 assert(cfg.hosts.length == 1); 258 assert(cfg.hosts[0].name == "localhost"); 259 assert(cfg.hosts[0].port == 27017); 260 } 261 262 /// parseMongoDBUrl parses multi-host URL with safe, w, wtimeoutMS, ssl options 263 unittest 264 { 265 MongoClientSettings cfg; 266 267 assert(parseMongoDBUrl(cfg, "mongodb://host1,host2,host3/?safe=true&w=2&wtimeoutMS=2000&ssl=true&sslverifycertificate=false")); 268 assert(cfg.username == ""); 269 assert(cfg.digest == ""); 270 assert(cfg.database == ""); 271 assert(cfg.hosts.length == 3); 272 assert(cfg.hosts[0].name == "host1"); 273 assert(cfg.hosts[0].port == 27017); 274 assert(cfg.hosts[1].name == "host2"); 275 assert(cfg.hosts[1].port == 27017); 276 assert(cfg.hosts[2].name == "host3"); 277 assert(cfg.hosts[2].port == 27017); 278 assert(cfg.safe == true); 279 assert(cfg.w == Bson(2L)); 280 assert(cfg.wTimeoutMS == 2000); 281 assert(cfg.ssl == true); 282 assert(cfg.sslverifycertificate == false); 283 } 284 285 /// parseMongoDBUrl parses full URL with credentials, multi-host with ports, database, and all options 286 unittest 287 { 288 MongoClientSettings cfg; 289 290 assert(parseMongoDBUrl(cfg, 291 "mongodb://fred:flinstone@host1.example.com,host2.other.example.com:27108,host3:" 292 ~ "27019/mydb?journal=true;fsync=true;connectTimeoutms=1500;sockettimeoutMs=1000;w=majority")); 293 assert(cfg.username == "fred"); 294 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "flinstone")); 295 assert(cfg.database == "mydb"); 296 assert(cfg.hosts.length == 3); 297 assert(cfg.hosts[0].name == "host1.example.com"); 298 assert(cfg.hosts[0].port == 27017); 299 assert(cfg.hosts[1].name == "host2.other.example.com"); 300 assert(cfg.hosts[1].port == 27108); 301 assert(cfg.hosts[2].name == "host3"); 302 assert(cfg.hosts[2].port == 27019); 303 assert(cfg.fsync == true); 304 assert(cfg.journal == true); 305 assert(cfg.connectTimeoutMS == 1500); 306 assert(cfg.socketTimeoutMS == 1000); 307 assert(cfg.w == Bson("majority")); 308 assert(cfg.safe == true); 309 } 310 311 /// parseMongoDBUrl returns false for invalid URLs 312 unittest 313 { 314 MongoClientSettings cfg; 315 316 assert(!(parseMongoDBUrl(cfg, "localhost:27018"))); 317 assert(!(parseMongoDBUrl(cfg, "http://blah"))); 318 assert(!(parseMongoDBUrl(cfg, "mongodb://@localhost"))); 319 assert(!(parseMongoDBUrl(cfg, "mongodb://:thepass@localhost"))); 320 assert(!(parseMongoDBUrl(cfg, "mongodb://:badport/"))); 321 } 322 323 /// parseMongoDBUrl parses URL with special characters in password 324 unittest 325 { 326 MongoClientSettings cfg; 327 328 assert(parseMongoDBUrl(cfg, "mongodb://me:sl$ash/w0+rd@localhost")); 329 assert(cfg.digest == MongoClientSettings.makeDigest("me", "sl$ash/w0+rd")); 330 assert(cfg.hosts.length == 1); 331 assert(cfg.hosts[0].name == "localhost"); 332 assert(cfg.hosts[0].port == 27017); 333 } 334 335 /// parseMongoDBUrl parses URL with special characters in password and database 336 unittest 337 { 338 MongoClientSettings cfg; 339 340 assert(parseMongoDBUrl(cfg, "mongodb://me:sl$ash/w0+rd@localhost/mydb")); 341 assert(cfg.digest == MongoClientSettings.makeDigest("me", "sl$ash/w0+rd")); 342 assert(cfg.database == "mydb"); 343 assert(cfg.hosts.length == 1); 344 assert(cfg.hosts[0].name == "localhost"); 345 assert(cfg.hosts[0].port == 27017); 346 } 347 348 /// parseMongoDBUrl parses authMechanism=SCRAM-SHA-1 349 unittest 350 { 351 MongoClientSettings cfg; 352 353 assert(parseMongoDBUrl(cfg, "mongodb://user:pass@localhost/?authMechanism=SCRAM-SHA-1")); 354 assert(cfg.authMechanism == MongoAuthMechanism.scramSHA1); 355 } 356 357 /// parseMongoDBUrl parses authMechanism=MONGODB-CR 358 unittest 359 { 360 MongoClientSettings cfg; 361 362 assert(parseMongoDBUrl(cfg, "mongodb://user:pass@localhost/?authMechanism=MONGODB-CR")); 363 assert(cfg.authMechanism == MongoAuthMechanism.mongoDBCR); 364 } 365 366 /// parseMongoDBUrl parses authMechanism=MONGODB-X509 367 unittest 368 { 369 MongoClientSettings cfg; 370 371 assert(parseMongoDBUrl(cfg, "mongodb://user:pass@localhost/?authMechanism=MONGODB-X509")); 372 assert(cfg.authMechanism == MongoAuthMechanism.mongoDBX509); 373 } 374 375 /// parseMongoDBUrl throws on invalid authMechanism 376 unittest 377 { 378 import std.exception : assertThrown; 379 380 MongoClientSettings cfg; 381 382 assertThrown!Exception(parseMongoDBUrl(cfg, "mongodb://user:pass@localhost/?authMechanism=INVALID")); 383 } 384 385 /// parseMongoDBUrl parses authSource overriding database for getAuthDatabase 386 unittest 387 { 388 MongoClientSettings cfg; 389 390 assert(parseMongoDBUrl(cfg, "mongodb://user:pass@localhost/mydb?authSource=admin")); 391 assert(cfg.authSource == "admin"); 392 assert(cfg.getAuthDatabase() == "admin"); 393 } 394 395 /// parseMongoDBUrl parses appName option 396 unittest 397 { 398 MongoClientSettings cfg; 399 400 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?appName=myApp")); 401 assert(cfg.appName == "myApp"); 402 } 403 404 /// parseMongoDBUrl parses replicaSet option 405 unittest 406 { 407 MongoClientSettings cfg; 408 409 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?replicaSet=rs0")); 410 assert(cfg.replicaSet == "rs0"); 411 } 412 413 /// parseMongoDBUrl parses tls=true as ssl alias 414 unittest 415 { 416 MongoClientSettings cfg; 417 418 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?tls=true")); 419 assert(cfg.ssl == true); 420 } 421 422 /// parseMongoDBUrl parses tls=false as ssl alias 423 unittest 424 { 425 MongoClientSettings cfg; 426 427 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?tls=false")); 428 assert(cfg.ssl == false); 429 } 430 431 /// parseMongoDBUrl parses connectTimeoutMS 432 unittest 433 { 434 import core.time : msecs; 435 436 MongoClientSettings cfg; 437 438 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?connectTimeoutMS=5000")); 439 assert(cfg.connectTimeout == 5000.msecs); 440 assert(cfg.connectTimeoutMS == 5000); 441 } 442 443 /// parseMongoDBUrl parses socketTimeoutMS 444 unittest 445 { 446 import core.time : msecs; 447 448 MongoClientSettings cfg; 449 450 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?socketTimeoutMS=3000")); 451 assert(cfg.socketTimeout == 3000.msecs); 452 assert(cfg.socketTimeoutMS == 3000); 453 } 454 455 /// parseMongoDBUrl parses w=1 as integer write concern 456 unittest 457 { 458 MongoClientSettings cfg; 459 460 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?w=1")); 461 assert(cfg.w == Bson(1L)); 462 } 463 464 /// parseMongoDBUrl parses w=majority as string write concern 465 unittest 466 { 467 MongoClientSettings cfg; 468 469 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?w=majority")); 470 assert(cfg.w == Bson("majority")); 471 } 472 473 /// parseMongoDBUrl sets safe=true when journal=true 474 unittest 475 { 476 MongoClientSettings cfg; 477 478 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?journal=true")); 479 assert(cfg.journal == true); 480 assert(cfg.safe == true); 481 } 482 483 /// parseMongoDBUrl sets safe=true when fsync=true 484 unittest 485 { 486 MongoClientSettings cfg; 487 488 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?fsync=true")); 489 assert(cfg.fsync == true); 490 assert(cfg.safe == true); 491 } 492 493 /// parseMongoDBUrl parses sslverifycertificate=false 494 unittest 495 { 496 MongoClientSettings cfg; 497 498 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?sslverifycertificate=false")); 499 assert(cfg.sslverifycertificate == false); 500 } 501 502 /// parseMongoDBUrl parses multiple combined options 503 unittest 504 { 505 MongoClientSettings cfg; 506 507 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?appName=test&replicaSet=rs1&ssl=true&authSource=admin")); 508 assert(cfg.appName == "test"); 509 assert(cfg.replicaSet == "rs1"); 510 assert(cfg.ssl == true); 511 assert(cfg.authSource == "admin"); 512 } 513 514 /// parseMongoDBUrl parses URL with database and no options 515 unittest 516 { 517 MongoClientSettings cfg; 518 519 assert(parseMongoDBUrl(cfg, "mongodb://localhost/mydb")); 520 assert(cfg.database == "mydb"); 521 assert(cfg.hosts[0].name == "localhost"); 522 assert(cfg.hosts[0].port == 27017); 523 } 524 525 /// parseMongoDBUrl parses URL with database and trailing empty query string 526 unittest 527 { 528 MongoClientSettings cfg; 529 530 assert(parseMongoDBUrl(cfg, "mongodb://localhost/mydb?")); 531 assert(cfg.database == "mydb"); 532 } 533 534 /// parseMongoDBUrl parses URL with no database but with options 535 unittest 536 { 537 MongoClientSettings cfg; 538 539 assert(parseMongoDBUrl(cfg, "mongodb://localhost/?safe=true")); 540 assert(cfg.database == ""); 541 assert(cfg.safe == true); 542 } 543 544 /// parseMongoDBUrl parses explicit non-default port 545 unittest 546 { 547 MongoClientSettings cfg; 548 549 assert(parseMongoDBUrl(cfg, "mongodb://localhost:27018")); 550 assert(cfg.hosts[0].port == 27018); 551 } 552 553 /// parseMongoDBUrl parses minimum valid port 1 554 unittest 555 { 556 MongoClientSettings cfg; 557 558 assert(parseMongoDBUrl(cfg, "mongodb://localhost:1")); 559 assert(cfg.hosts[0].port == 1); 560 } 561 562 /// parseMongoDBUrl parses maximum valid port 65535 563 unittest 564 { 565 MongoClientSettings cfg; 566 567 assert(parseMongoDBUrl(cfg, "mongodb://localhost:65535")); 568 assert(cfg.hosts[0].port == 65535); 569 } 570 571 /// parseMongoDBUrl parses port 0 572 unittest 573 { 574 MongoClientSettings cfg; 575 576 assert(parseMongoDBUrl(cfg, "mongodb://localhost:0")); 577 assert(cfg.hosts[0].port == 0); 578 } 579 580 /// parseMongoDBUrl returns false for port exceeding ushort range 581 unittest 582 { 583 MongoClientSettings cfg; 584 585 assert(!parseMongoDBUrl(cfg, "mongodb://localhost:65536")); 586 } 587 588 /// parseMongoDBUrl returns false for non-numeric port 589 unittest 590 { 591 MongoClientSettings cfg; 592 593 assert(!parseMongoDBUrl(cfg, "mongodb://localhost:abc")); 594 } 595 596 /// getAuthDatabase returns authSource when set 597 unittest 598 { 599 auto cfg = new MongoClientSettings(); 600 cfg.authSource = "external"; 601 cfg.database = "mydb"; 602 assert(cfg.getAuthDatabase() == "external"); 603 } 604 605 /// getAuthDatabase returns database when authSource is empty 606 unittest 607 { 608 auto cfg = new MongoClientSettings(); 609 cfg.database = "mydb"; 610 assert(cfg.getAuthDatabase() == "mydb"); 611 } 612 613 /// getAuthDatabase returns "admin" when both authSource and database are empty 614 unittest 615 { 616 auto cfg = new MongoClientSettings(); 617 assert(cfg.getAuthDatabase() == "admin"); 618 } 619 620 /// makeDigest produces deterministic output for same inputs 621 unittest 622 { 623 assert(MongoClientSettings.makeDigest("user", "pass") == 624 MongoClientSettings.makeDigest("user", "pass")); 625 } 626 627 /// makeDigest produces different output for different passwords 628 unittest 629 { 630 assert(MongoClientSettings.makeDigest("user", "pass1") != 631 MongoClientSettings.makeDigest("user", "pass2")); 632 } 633 634 /// makeDigest produces different output for different usernames 635 unittest 636 { 637 assert(MongoClientSettings.makeDigest("user1", "pass") != 638 MongoClientSettings.makeDigest("user2", "pass")); 639 } 640 641 /// connectTimeoutMS defaults to 10000 and round-trips through Duration 642 unittest 643 { 644 import core.time : msecs, seconds; 645 646 auto cfg = new MongoClientSettings(); 647 648 assert(cfg.connectTimeoutMS == 10_000); 649 assert(cfg.connectTimeout == 10.seconds); 650 651 cfg.connectTimeoutMS = 2500; 652 assert(cfg.connectTimeout == 2500.msecs); 653 assert(cfg.connectTimeoutMS == 2500); 654 655 cfg.connectTimeout = 7.seconds; 656 assert(cfg.connectTimeoutMS == 7000); 657 } 658 659 /// socketTimeoutMS defaults to 0 and round-trips through Duration 660 unittest 661 { 662 import core.time : msecs; 663 664 auto cfg = new MongoClientSettings(); 665 666 assert(cfg.socketTimeoutMS == 0); 667 668 cfg.socketTimeoutMS = 5000; 669 assert(cfg.socketTimeout == 5000.msecs); 670 assert(cfg.socketTimeoutMS == 5000); 671 } 672 673 /// authenticatePassword sets username and digest 674 unittest 675 { 676 auto cfg = new MongoClientSettings(); 677 678 cfg.authenticatePassword("fred", "secret"); 679 assert(cfg.username == "fred"); 680 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "secret")); 681 } 682 683 /// authenticateSSL sets ssl, username, PEM key file, and CA file 684 unittest 685 { 686 auto cfg = new MongoClientSettings(); 687 688 cfg.authenticateSSL("CN=client", "/path/to/cert.pem", "/path/to/ca.pem"); 689 assert(cfg.ssl == true); 690 assert(cfg.username == "CN=client"); 691 assert(cfg.digest is null); 692 assert(cfg.sslPEMKeyFile == "/path/to/cert.pem"); 693 assert(cfg.sslCAFile == "/path/to/ca.pem"); 694 } 695 696 /// authenticateSSL without CA file sets sslCAFile to null 697 unittest 698 { 699 auto cfg = new MongoClientSettings(); 700 701 cfg.authenticateSSL("CN=client2", "/path/to/cert2.pem"); 702 assert(cfg.sslCAFile is null); 703 } 704 705 /** 706 * Describes a vibe.d supported authentication mechanism to use on client 707 * connection to a MongoDB server. 708 */ 709 enum MongoAuthMechanism 710 { 711 /** 712 * Use no auth mechanism. If a digest or ssl certificate is given this 713 * defaults to trying the recommend auth mechanisms depending on server 714 * version and input parameters. 715 */ 716 none, 717 718 /** 719 * Use SCRAM-SHA-1 as defined in [RFC 5802](http://tools.ietf.org/html/rfc5802) 720 * 721 * This is the default when a password is provided. In the future other 722 * scram algorithms may be implemented and selectable through these values. 723 * 724 * MongoDB: 3.0– 725 */ 726 scramSHA1, 727 728 /** 729 * Forces login through the legacy MONGODB-CR authentication mechanism. This 730 * mechanism is a nonce and MD5 based system. 731 * 732 * MongoDB: 1.4–4.0 (deprecated 3.0) 733 */ 734 mongoDBCR, 735 736 /** 737 * Use an X.509 certificate to authenticate. Only works if digest is set to 738 * null or empty string in the MongoClientSettings. 739 * 740 * MongoDB: 2.6– 741 */ 742 mongoDBX509 743 } 744 745 private MongoAuthMechanism parseAuthMechanism(string str) 746 @safe { 747 switch (str) { 748 case "SCRAM-SHA-1": return MongoAuthMechanism.scramSHA1; 749 case "MONGODB-CR": return MongoAuthMechanism.mongoDBCR; 750 case "MONGODB-X509": return MongoAuthMechanism.mongoDBX509; 751 default: throw new Exception("Auth mechanism \"" ~ str ~ "\" not supported"); 752 } 753 } 754 755 /** 756 * See_Also: $(LINK https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options) 757 */ 758 class MongoClientSettings 759 { 760 /// Gets the default port used for MongoDB connections 761 enum ushort defaultPort = 27017; 762 763 /** 764 * If set to non-empty string, use this username to try to authenticate with 765 * to the database. Only has an effect if digest or sslPEMKeyFile is set too 766 * 767 * Use $(LREF authenticatePassword) or $(LREF authenticateSSL) to 768 * automatically fill this. 769 */ 770 string username; 771 772 /** 773 * The password hashed as MongoDB digest as returned by $(LREF makeDigest). 774 * 775 * **DISCOURAGED** to fill this manually as future authentication mechanisms 776 * may use other digest algorithms. 777 * 778 * Use $(LREF authenticatePassword) to automatically fill this. 779 */ 780 string digest; 781 782 /** 783 * Amount of maximum simultaneous connections to have open at the same time. 784 * 785 * Every MongoDB call may allocate a new connection if no previous ones are 786 * available and there is no connection associated with the calling Fiber. 787 */ 788 uint maxConnections = uint.max; 789 790 /** 791 * MongoDB hosts to try to connect to. 792 * 793 * Bugs: currently only a connection to the first host is attempted, more 794 * hosts are simply ignored. 795 */ 796 MongoHost[] hosts; 797 798 /** 799 * Default auth database to operate on, otherwise operating on special 800 * "admin" database for all MongoDB authentication commands. 801 */ 802 string database; 803 804 /** 805 * Specifies the name of the replica set, if the mongod is a member of a 806 * replica set. 807 * 808 * Bugs: Not yet implemented 809 */ 810 string replicaSet; 811 812 /** 813 * Automatically check for errors when operating on collections and throw a 814 * $(REF MongoDBException, vibe,db,mongo,connection) in case of errors. 815 * 816 * Automatically set if either: 817 * * the "w" (write concern) parameter is set 818 * * the "wTimeoutMS" parameter is set 819 * * journal is true 820 */ 821 bool safe; 822 823 /** 824 * Requests acknowledgment that write operations have propagated to a 825 * specified number of mongod instances (number) or to mongod instances with 826 * specified tags (string) or "majority" for calculated majority. 827 * 828 * See_Also: write concern [w Option](https://docs.mongodb.com/manual/reference/write-concern/#wc-w). 829 */ 830 Bson w; // Either a number or the string 'majority' 831 832 /** 833 * Time limit for the w option to prevent write operations from blocking 834 * indefinitely. 835 * 836 * See_Also: $(LREF w) 837 */ 838 long wTimeoutMS; 839 840 // undocumented feature in no documentation of >=MongoDB 2.2 ?! 841 bool fsync; 842 843 /** 844 * Requests acknowledgment that write operations have been written to the 845 * [on-disk journal](https://docs.mongodb.com/manual/core/journaling/). 846 * 847 * See_Also: write concern [j Option](https://docs.mongodb.com/manual/reference/write-concern/#wc-j). 848 */ 849 bool journal; 850 851 /** 852 * The time to attempt a connection before timing out. 853 */ 854 Duration connectTimeout = 10.seconds; 855 856 /// ditto 857 long connectTimeoutMS() const @property 858 @safe { 859 return connectTimeout.total!"msecs"; 860 } 861 862 /// ditto 863 void connectTimeoutMS(long ms) @property 864 @safe { 865 connectTimeout = ms.msecs; 866 } 867 868 /** 869 * The time to attempt a send or receive on a socket before the attempt 870 * times out. 871 * 872 * Bugs: Not implemented for sending 873 */ 874 Duration socketTimeout = Duration.zero; 875 876 /// ditto 877 long socketTimeoutMS() const @property 878 @safe { 879 return socketTimeout.total!"msecs"; 880 } 881 882 /// ditto 883 void socketTimeoutMS(long ms) @property 884 @safe { 885 socketTimeout = ms.msecs; 886 } 887 888 /** 889 * Enables or disables TLS/SSL for the connection. 890 */ 891 bool ssl; 892 893 /** 894 * Can be set to false to disable TLS peer validation to allow self signed 895 * certificates. 896 * 897 * This mode is discouraged and should ONLY be used in development. 898 */ 899 bool sslverifycertificate = true; 900 901 /** 902 * Path to a certificate with private key and certificate chain to connect 903 * with. 904 */ 905 string sslPEMKeyFile; 906 907 /** 908 * Path to a certificate authority file for verifying the remote 909 * certificate. 910 */ 911 string sslCAFile; 912 913 /** 914 * Specify the database name associated with the user's credentials. If 915 * `authSource` is unspecified, `authSource` defaults to the `defaultauthdb` 916 * specified in the connection string. If `defaultauthdb` is unspecified, 917 * then `authSource` defaults to `admin`. 918 * 919 * The `PLAIN` (LDAP), `GSSAPI` (Kerberos), and `MONGODB-AWS` (IAM) 920 * authentication mechanisms require that `authSource` be set to `$external`, 921 * as these mechanisms delegate credential storage to external services. 922 * 923 * Ignored if no username is provided. 924 */ 925 string authSource; 926 927 /** 928 * Use the given authentication mechanism when connecting to the server. If 929 * unsupported by the server, throw a MongoAuthException. 930 * 931 * If set to none, but digest or sslPEMKeyFile are set, this automatically 932 * determines a suitable authentication mechanism based on server version. 933 */ 934 MongoAuthMechanism authMechanism; 935 936 /** 937 * Specify properties for the specified authMechanism as a comma-separated 938 * list of colon-separated key-value pairs. 939 * 940 * Currently none are used by the vibe.d Mongo driver. 941 */ 942 string[] authMechanismProperties; 943 944 /** 945 * Application name for the connection information when connected. 946 * 947 * The application name is printed to the mongod logs upon establishing the 948 * connection. It is also recorded in the slow query logs and profile 949 * collections. 950 */ 951 string appName; 952 953 /** 954 * Generates a digest string which can be used for authentication by setting 955 * the username and digest members. 956 * 957 * Use $(LREF authenticate) to automatically configure username and digest. 958 */ 959 static pure string makeDigest(string username, string password) 960 @safe { 961 return md5Of(username ~ ":mongo:" ~ password).toHexString().idup.toLower(); 962 } 963 964 /** 965 * Sets the username and the digest string in this MongoClientSettings 966 * instance. 967 */ 968 void authenticatePassword(string username, string password) 969 @safe { 970 this.username = username; 971 this.digest = MongoClientSettings.makeDigest(username, password); 972 } 973 974 /** 975 * Sets ssl, the username, the PEM key file and the trusted CA file in this 976 * MongoClientSettings instance. 977 * 978 * Params: 979 * username = The username as provided in the cert file like 980 * `"C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client"`. 981 * 982 * The username can be blank if connecting to MongoDB 3.4 or above. 983 * 984 * sslPEMKeyFile = Path to a certificate with private key and certificate 985 * chain to connect with. 986 * 987 * sslCAFile = Optional path to a trusted certificate authority file for 988 * verifying the remote certificate. 989 */ 990 void authenticateSSL(string username, string sslPEMKeyFile, string sslCAFile = null) 991 @safe { 992 this.ssl = true; 993 this.digest = null; 994 this.username = username; 995 this.sslPEMKeyFile = sslPEMKeyFile; 996 this.sslCAFile = sslCAFile; 997 } 998 999 /** 1000 * Resolves the database to run authentication commands on. 1001 * (authSource if set, otherwise the URI's database if set, otherwise "admin") 1002 */ 1003 string getAuthDatabase() 1004 @safe @nogc nothrow pure const return { 1005 if (authSource.length) 1006 return authSource; 1007 else if (database.length) 1008 return database; 1009 else 1010 return "admin"; 1011 } 1012 } 1013 1014 /// Describes a host we might be able to connect to 1015 struct MongoHost 1016 { 1017 /// The host name or IP address of the remote MongoDB server. 1018 string name; 1019 /// The port of the MongoDB server. See `MongoClientSettings.defaultPort`. 1020 ushort port; 1021 }