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 /* Test for parseMongoDBUrl */ 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 cfg = MongoClientSettings.init; 234 assert(parseMongoDBUrl(cfg, "mongodb://fred:foobar@localhost")); 235 assert(cfg.username == "fred"); 236 //assert(cfg.password == "foobar"); 237 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "foobar")); 238 assert(cfg.hosts.length == 1); 239 assert(cfg.database == ""); 240 assert(cfg.hosts[0].name == "localhost"); 241 assert(cfg.hosts[0].port == 27017); 242 243 cfg = MongoClientSettings.init; 244 assert(parseMongoDBUrl(cfg, "mongodb://fred:@localhost/baz")); 245 assert(cfg.username == "fred"); 246 //assert(cfg.password == ""); 247 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "")); 248 assert(cfg.database == "baz"); 249 assert(cfg.hosts.length == 1); 250 assert(cfg.hosts[0].name == "localhost"); 251 assert(cfg.hosts[0].port == 27017); 252 253 cfg = MongoClientSettings.init; 254 assert(parseMongoDBUrl(cfg, "mongodb://host1,host2,host3/?safe=true&w=2&wtimeoutMS=2000&ssl=true&sslverifycertificate=false")); 255 assert(cfg.username == ""); 256 //assert(cfg.password == ""); 257 assert(cfg.digest == ""); 258 assert(cfg.database == ""); 259 assert(cfg.hosts.length == 3); 260 assert(cfg.hosts[0].name == "host1"); 261 assert(cfg.hosts[0].port == 27017); 262 assert(cfg.hosts[1].name == "host2"); 263 assert(cfg.hosts[1].port == 27017); 264 assert(cfg.hosts[2].name == "host3"); 265 assert(cfg.hosts[2].port == 27017); 266 assert(cfg.safe == true); 267 assert(cfg.w == Bson(2L)); 268 assert(cfg.wTimeoutMS == 2000); 269 assert(cfg.ssl == true); 270 assert(cfg.sslverifycertificate == false); 271 272 cfg = MongoClientSettings.init; 273 assert(parseMongoDBUrl(cfg, 274 "mongodb://fred:flinstone@host1.example.com,host2.other.example.com:27108,host3:" 275 ~ "27019/mydb?journal=true;fsync=true;connectTimeoutms=1500;sockettimeoutMs=1000;w=majority")); 276 assert(cfg.username == "fred"); 277 //assert(cfg.password == "flinstone"); 278 assert(cfg.digest == MongoClientSettings.makeDigest("fred", "flinstone")); 279 assert(cfg.database == "mydb"); 280 assert(cfg.hosts.length == 3); 281 assert(cfg.hosts[0].name == "host1.example.com"); 282 assert(cfg.hosts[0].port == 27017); 283 assert(cfg.hosts[1].name == "host2.other.example.com"); 284 assert(cfg.hosts[1].port == 27108); 285 assert(cfg.hosts[2].name == "host3"); 286 assert(cfg.hosts[2].port == 27019); 287 assert(cfg.fsync == true); 288 assert(cfg.journal == true); 289 assert(cfg.connectTimeoutMS == 1500); 290 assert(cfg.socketTimeoutMS == 1000); 291 assert(cfg.w == Bson("majority")); 292 assert(cfg.safe == true); 293 294 // Invalid URLs - these should fail to parse 295 cfg = MongoClientSettings.init; 296 assert(! (parseMongoDBUrl(cfg, "localhost:27018"))); 297 assert(! (parseMongoDBUrl(cfg, "http://blah"))); 298 assert(! (parseMongoDBUrl(cfg, "mongodb://@localhost"))); 299 assert(! (parseMongoDBUrl(cfg, "mongodb://:thepass@localhost"))); 300 assert(! (parseMongoDBUrl(cfg, "mongodb://:badport/"))); 301 302 assert(parseMongoDBUrl(cfg, "mongodb://me:sl$ash/w0+rd@localhost")); 303 assert(cfg.digest == MongoClientSettings.makeDigest("me", "sl$ash/w0+rd")); 304 assert(cfg.hosts.length == 1); 305 assert(cfg.hosts[0].name == "localhost"); 306 assert(cfg.hosts[0].port == 27017); 307 assert(parseMongoDBUrl(cfg, "mongodb://me:sl$ash/w0+rd@localhost/mydb")); 308 assert(cfg.digest == MongoClientSettings.makeDigest("me", "sl$ash/w0+rd")); 309 assert(cfg.database == "mydb"); 310 assert(cfg.hosts.length == 1); 311 assert(cfg.hosts[0].name == "localhost"); 312 assert(cfg.hosts[0].port == 27017); 313 } 314 315 /** 316 * Describes a vibe.d supported authentication mechanism to use on client 317 * connection to a MongoDB server. 318 */ 319 enum MongoAuthMechanism 320 { 321 /** 322 * Use no auth mechanism. If a digest or ssl certificate is given this 323 * defaults to trying the recommend auth mechanisms depending on server 324 * version and input parameters. 325 */ 326 none, 327 328 /** 329 * Use SCRAM-SHA-1 as defined in [RFC 5802](http://tools.ietf.org/html/rfc5802) 330 * 331 * This is the default when a password is provided. In the future other 332 * scram algorithms may be implemented and selectable through these values. 333 * 334 * MongoDB: 3.0– 335 */ 336 scramSHA1, 337 338 /** 339 * Forces login through the legacy MONGODB-CR authentication mechanism. This 340 * mechanism is a nonce and MD5 based system. 341 * 342 * MongoDB: 1.4–4.0 (deprecated 3.0) 343 */ 344 mongoDBCR, 345 346 /** 347 * Use an X.509 certificate to authenticate. Only works if digest is set to 348 * null or empty string in the MongoClientSettings. 349 * 350 * MongoDB: 2.6– 351 */ 352 mongoDBX509 353 } 354 355 private MongoAuthMechanism parseAuthMechanism(string str) 356 @safe { 357 switch (str) { 358 case "SCRAM-SHA-1": return MongoAuthMechanism.scramSHA1; 359 case "MONGODB-CR": return MongoAuthMechanism.mongoDBCR; 360 case "MONGODB-X509": return MongoAuthMechanism.mongoDBX509; 361 default: throw new Exception("Auth mechanism \"" ~ str ~ "\" not supported"); 362 } 363 } 364 365 /** 366 * See_Also: $(LINK https://docs.mongodb.com/manual/reference/connection-string/#connections-connection-options) 367 */ 368 class MongoClientSettings 369 { 370 /// Gets the default port used for MongoDB connections 371 enum ushort defaultPort = 27017; 372 373 /** 374 * If set to non-empty string, use this username to try to authenticate with 375 * to the database. Only has an effect if digest or sslPEMKeyFile is set too 376 * 377 * Use $(LREF authenticatePassword) or $(LREF authenticateSSL) to 378 * automatically fill this. 379 */ 380 string username; 381 382 /** 383 * The password hashed as MongoDB digest as returned by $(LREF makeDigest). 384 * 385 * **DISCOURAGED** to fill this manually as future authentication mechanisms 386 * may use other digest algorithms. 387 * 388 * Use $(LREF authenticatePassword) to automatically fill this. 389 */ 390 string digest; 391 392 /** 393 * Amount of maximum simultaneous connections to have open at the same time. 394 * 395 * Every MongoDB call may allocate a new connection if no previous ones are 396 * available and there is no connection associated with the calling Fiber. 397 */ 398 uint maxConnections = uint.max; 399 400 /** 401 * MongoDB hosts to try to connect to. 402 * 403 * Bugs: currently only a connection to the first host is attempted, more 404 * hosts are simply ignored. 405 */ 406 MongoHost[] hosts; 407 408 /** 409 * Default auth database to operate on, otherwise operating on special 410 * "admin" database for all MongoDB authentication commands. 411 */ 412 string database; 413 414 deprecated("unused since at least before v3.6") QueryFlags defQueryFlags = QueryFlags.None; 415 416 /** 417 * Specifies the name of the replica set, if the mongod is a member of a 418 * replica set. 419 * 420 * Bugs: Not yet implemented 421 */ 422 string replicaSet; 423 424 /** 425 * Automatically check for errors when operating on collections and throw a 426 * $(REF MongoDBException, vibe,db,mongo,connection) in case of errors. 427 * 428 * Automatically set if either: 429 * * the "w" (write concern) parameter is set 430 * * the "wTimeoutMS" parameter is set 431 * * journal is true 432 */ 433 bool safe; 434 435 /** 436 * Requests acknowledgment that write operations have propagated to a 437 * specified number of mongod instances (number) or to mongod instances with 438 * specified tags (string) or "majority" for calculated majority. 439 * 440 * See_Also: write concern [w Option](https://docs.mongodb.com/manual/reference/write-concern/#wc-w). 441 */ 442 Bson w; // Either a number or the string 'majority' 443 444 /** 445 * Time limit for the w option to prevent write operations from blocking 446 * indefinitely. 447 * 448 * See_Also: $(LREF w) 449 */ 450 long wTimeoutMS; 451 452 // undocumented feature in no documentation of >=MongoDB 2.2 ?! 453 bool fsync; 454 455 /** 456 * Requests acknowledgment that write operations have been written to the 457 * [on-disk journal](https://docs.mongodb.com/manual/core/journaling/). 458 * 459 * See_Also: write concern [j Option](https://docs.mongodb.com/manual/reference/write-concern/#wc-j). 460 */ 461 bool journal; 462 463 /** 464 * The time to attempt a connection before timing out. 465 */ 466 Duration connectTimeout = 10.seconds; 467 468 /// ditto 469 long connectTimeoutMS() const @property 470 @safe { 471 return connectTimeout.total!"msecs"; 472 } 473 474 /// ditto 475 void connectTimeoutMS(long ms) @property 476 @safe { 477 connectTimeout = ms.msecs; 478 } 479 480 /** 481 * The time to attempt a send or receive on a socket before the attempt 482 * times out. 483 * 484 * Bugs: Not implemented for sending 485 */ 486 Duration socketTimeout = Duration.zero; 487 488 /// ditto 489 long socketTimeoutMS() const @property 490 @safe { 491 return socketTimeout.total!"msecs"; 492 } 493 494 /// ditto 495 void socketTimeoutMS(long ms) @property 496 @safe { 497 socketTimeout = ms.msecs; 498 } 499 500 /** 501 * Enables or disables TLS/SSL for the connection. 502 */ 503 bool ssl; 504 505 /** 506 * Can be set to false to disable TLS peer validation to allow self signed 507 * certificates. 508 * 509 * This mode is discouraged and should ONLY be used in development. 510 */ 511 bool sslverifycertificate = true; 512 513 /** 514 * Path to a certificate with private key and certificate chain to connect 515 * with. 516 */ 517 string sslPEMKeyFile; 518 519 /** 520 * Path to a certificate authority file for verifying the remote 521 * certificate. 522 */ 523 string sslCAFile; 524 525 /** 526 * Specify the database name associated with the user's credentials. If 527 * `authSource` is unspecified, `authSource` defaults to the `defaultauthdb` 528 * specified in the connection string. If `defaultauthdb` is unspecified, 529 * then `authSource` defaults to `admin`. 530 * 531 * The `PLAIN` (LDAP), `GSSAPI` (Kerberos), and `MONGODB-AWS` (IAM) 532 * authentication mechanisms require that `authSource` be set to `$external`, 533 * as these mechanisms delegate credential storage to external services. 534 * 535 * Ignored if no username is provided. 536 */ 537 string authSource; 538 539 /** 540 * Use the given authentication mechanism when connecting to the server. If 541 * unsupported by the server, throw a MongoAuthException. 542 * 543 * If set to none, but digest or sslPEMKeyFile are set, this automatically 544 * determines a suitable authentication mechanism based on server version. 545 */ 546 MongoAuthMechanism authMechanism; 547 548 /** 549 * Specify properties for the specified authMechanism as a comma-separated 550 * list of colon-separated key-value pairs. 551 * 552 * Currently none are used by the vibe.d Mongo driver. 553 */ 554 string[] authMechanismProperties; 555 556 /** 557 * Application name for the connection information when connected. 558 * 559 * The application name is printed to the mongod logs upon establishing the 560 * connection. It is also recorded in the slow query logs and profile 561 * collections. 562 */ 563 string appName; 564 565 /** 566 * Generates a digest string which can be used for authentication by setting 567 * the username and digest members. 568 * 569 * Use $(LREF authenticate) to automatically configure username and digest. 570 */ 571 static pure string makeDigest(string username, string password) 572 @safe { 573 return md5Of(username ~ ":mongo:" ~ password).toHexString().idup.toLower(); 574 } 575 576 /** 577 * Sets the username and the digest string in this MongoClientSettings 578 * instance. 579 */ 580 void authenticatePassword(string username, string password) 581 @safe { 582 this.username = username; 583 this.digest = MongoClientSettings.makeDigest(username, password); 584 } 585 586 /** 587 * Sets ssl, the username, the PEM key file and the trusted CA file in this 588 * MongoClientSettings instance. 589 * 590 * Params: 591 * username = The username as provided in the cert file like 592 * `"C=IS,ST=Reykjavik,L=Reykjavik,O=MongoDB,OU=Drivers,CN=client"`. 593 * 594 * The username can be blank if connecting to MongoDB 3.4 or above. 595 * 596 * sslPEMKeyFile = Path to a certificate with private key and certificate 597 * chain to connect with. 598 * 599 * sslCAFile = Optional path to a trusted certificate authority file for 600 * verifying the remote certificate. 601 */ 602 void authenticateSSL(string username, string sslPEMKeyFile, string sslCAFile = null) 603 @safe { 604 this.ssl = true; 605 this.digest = null; 606 this.username = username; 607 this.sslPEMKeyFile = sslPEMKeyFile; 608 this.sslCAFile = sslCAFile; 609 } 610 611 /** 612 * Resolves the database to run authentication commands on. 613 * (authSource if set, otherwise the URI's database if set, otherwise "admin") 614 */ 615 string getAuthDatabase() 616 @safe @nogc nothrow pure const return { 617 if (authSource.length) 618 return authSource; 619 else if (database.length) 620 return database; 621 else 622 return "admin"; 623 } 624 } 625 626 /// Describes a host we might be able to connect to 627 struct MongoHost 628 { 629 /// The host name or IP address of the remote MongoDB server. 630 string name; 631 /// The port of the MongoDB server. See `MongoClientSettings.defaultPort`. 632 ushort port; 633 }