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 }