Skip to content

Commit 82bca96

Browse files
committed
CXX-2 Implement support for connecting via mondodb:// URLs
1 parent 9f5a88d commit 82bca96

File tree

7 files changed

+352
-9
lines changed

7 files changed

+352
-9
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ install:
5050
libboost-program-options1.49-dev
5151
libboost-filesystem1.49-dev
5252
libboost-thread1.49-dev
53+
libboost-regex1.49-dev
5354

5455
# Install MongoDB Enterprise and let smoke drive
5556
- sudo apt-get install mongodb-enterprise-server

SConstruct

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ def printLocalInfo():
262262

263263
printLocalInfo()
264264

265-
boostLibs = [ "thread" , "system" ]
265+
boostLibs = [ "regex", "thread" , "system" ]
266266

267267
linux64 = False
268268
force32 = has_option( "force32" )

src/mongo/SConscript

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ unittests = [
4040
'bson/bson_validate_test',
4141
'bson/bsonobjbuilder_test',
4242
'bson/util/bson_extract_test',
43+
'client/connection_string_test',
4344
'client/dbclient_rs_test',
4445
'client/index_spec_test',
4546
'client/replica_set_monitor_test',
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/* Copyright 2014 MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
#include "mongo/unittest/unittest.h"
17+
18+
#include "mongo/client/dbclientinterface.h"
19+
20+
namespace {
21+
using mongo::ConnectionString;
22+
23+
struct URLTestCase {
24+
std::string URL;
25+
std::string uname;
26+
std::string password;
27+
mongo::ConnectionString::ConnectionType type;
28+
std::string setname;
29+
size_t numservers;
30+
size_t numOptions;
31+
std::string database;
32+
};
33+
34+
const mongo::ConnectionString::ConnectionType kMaster = mongo::ConnectionString::MASTER;
35+
const mongo::ConnectionString::ConnectionType kSet = mongo::ConnectionString::SET;
36+
37+
const URLTestCase cases[] = {
38+
39+
{ "mongodb://user:pwd@127.0.0.1", "user", "pwd", kMaster, "", 1, 0, "" },
40+
41+
{ "mongodb://user@127.0.0.1", "user", "", kMaster, "", 1, 0, "" },
42+
43+
{ "mongodb://127.0.0.1/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
44+
45+
{ "mongodb://user:pwd@127.0.0.1:1234", "user", "pwd", kMaster, "", 1, 0, "" },
46+
47+
{ "mongodb://user@127.0.0.1:1234", "user", "", kMaster, "", 1, 0, "" },
48+
49+
{ "mongodb://127.0.0.1:1234/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
50+
51+
{ "mongodb://user:pwd@127.0.0.1,127.0.0.2/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
52+
53+
{ "mongodb://user@127.0.0.1,127.0.0.2/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
54+
55+
{ "mongodb://127.0.0.1,127.0.0.2/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName" },
56+
57+
{ "mongodb://user:pwd@127.0.0.1:1234,127.0.0.2:1234/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
58+
59+
{ "mongodb://user@127.0.0.1:1234,127.0.0.2:1234/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
60+
61+
{ "mongodb://127.0.0.1:1234,127.0.0.1:1234/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName" },
62+
63+
{ "mongodb://user:pwd@[::1]", "user", "pwd", kMaster, "", 1, 0, "" },
64+
65+
{ "mongodb://user@[::1]", "user", "", kMaster, "", 1, 0, "" },
66+
67+
{ "mongodb://[::1]/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
68+
69+
{ "mongodb://user:pwd@[::1]:1234", "user", "pwd", kMaster, "", 1, 0, "" },
70+
71+
{ "mongodb://user@[::1]:1234", "user", "", kMaster, "", 1, 0, "" },
72+
73+
{ "mongodb://[::1]:1234/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
74+
75+
{ "mongodb://user:pwd@[::1],127.0.0.2/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
76+
77+
{ "mongodb://user@[::1],127.0.0.2/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
78+
79+
{ "mongodb://[::1],127.0.0.2/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName" },
80+
81+
{ "mongodb://user:pwd@[::1]:1234,127.0.0.2:1234/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
82+
83+
{ "mongodb://user@[::1]:1234,127.0.0.2:1234/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
84+
85+
{ "mongodb://[::1]:1234,[::1]:1234/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName" },
86+
87+
{ "mongodb://user:pwd@[::1]", "user", "pwd", kMaster, "", 1, 0, "" },
88+
89+
{ "mongodb://user@[::1]", "user", "", kMaster, "", 1, 0, "" },
90+
91+
{ "mongodb://[::1]/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
92+
93+
{ "mongodb://user:pwd@[::1]:1234", "user", "pwd", kMaster, "", 1, 0, "" },
94+
95+
{ "mongodb://user@[::1]:1234", "user", "", kMaster, "", 1, 0, "" },
96+
97+
{ "mongodb://[::1]:1234/dbName?foo=a&c=b", "", "", kMaster, "", 1, 2, "dbName" },
98+
99+
{ "mongodb://user:pwd@[::1],127.0.0.2/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
100+
101+
{ "mongodb://user@[::1],127.0.0.2/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
102+
103+
{ "mongodb://[::1],127.0.0.2/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName" },
104+
105+
{ "mongodb://user:pwd@[::1]:1234,127.0.0.2:1234/?replicaSet=replName", "user", "pwd", kSet, "replName", 2, 1, "" },
106+
107+
{ "mongodb://user@[::1]:1234,127.0.0.2:1234/?replicaSet=replName", "user", "", kSet, "replName", 2, 1, "" },
108+
109+
{ "mongodb://[::1]:1234,[::1]:1234/dbName?foo=a&c=b&replicaSet=replName", "", "", kSet, "replName", 2, 3, "dbName"},
110+
};
111+
112+
113+
TEST(ConnectionString, GoodTrickyURLs) {
114+
115+
const size_t numCases = sizeof(cases) / sizeof(cases[0]);
116+
117+
for (size_t i = 0; i != numCases; ++i) {
118+
const URLTestCase testCase = cases[i];
119+
std::cout << "Testing URL: " << testCase.URL << '\n';
120+
std::string errMsg;
121+
const ConnectionString result = ConnectionString::parse(testCase.URL, errMsg);
122+
ASSERT_TRUE(result.isValid());
123+
ASSERT_TRUE(errMsg.empty());
124+
ASSERT_EQ(testCase.uname, result.getUser());
125+
ASSERT_EQ(testCase.password, result.getPassword());
126+
ASSERT_EQ(testCase.type, result.type());
127+
ASSERT_EQ(testCase.setname, result.getSetName());
128+
ASSERT_EQ(testCase.numservers, result.getServers().size());
129+
BSONObj options = result.getOptions();
130+
std::set<std::string> fieldNames;
131+
options.getFieldNames(fieldNames);
132+
ASSERT_EQ(testCase.numOptions, fieldNames.size());
133+
ASSERT_EQ(testCase.database, result.getDatabase());
134+
}
135+
}
136+
137+
} // namespace

src/mongo/client/dbclient.cpp

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
#include "mongo/util/net/ssl_manager.h"
4242
#include "mongo/util/password_digest.h"
4343

44+
#include <boost/algorithm/string/classification.hpp>
45+
#include <boost/algorithm/string/predicate.hpp>
46+
#include <boost/algorithm/string/split.hpp>
47+
#include <boost/regex.hpp>
48+
4449
#include <algorithm>
4550
#include <cstdlib>
4651

@@ -62,6 +67,26 @@ namespace mongo {
6267
const int defaultMaxMessageSizeBytes = defaultMaxBsonObjectSize * 2;
6368
const int defaultMaxWriteBatchSize = 1000;
6469

70+
namespace {
71+
const char kMongoDBURL[] =
72+
// scheme: non-capturing
73+
"mongodb://"
74+
75+
// credentials: two inner captures for user and password
76+
"(?:([^:]+)(?::([^@]+))?@)?"
77+
78+
// servers: grabs all host:port or UNIX socket names
79+
"((?:(?:[^\\/]+|/.+.sock?),?)+)"
80+
81+
// database: matches anything but the chars that cannot
82+
// be part of a MongoDB database name.
83+
"(?:/([^/\\.\\ \"*<>:\\|\\?]*))?"
84+
85+
// options
86+
"(?:\\?(?:(.+=.+)&?)+)*";
87+
88+
} // namespace
89+
6590
void ConnectionString::_fillServers( string s ) {
6691

6792
//
@@ -111,6 +136,30 @@ namespace mongo {
111136
_string = ss.str();
112137
}
113138

139+
BSONObj ConnectionString::_makeAuthObjFromOptions() const {
140+
BSONObjBuilder bob;
141+
142+
// Add the username and optional password
143+
invariant(!_user.empty());
144+
bob.append("user", _user);
145+
if (!_password.empty())
146+
bob.append("pwd", _password);
147+
148+
BSONElement elt = _options.getField("authSource");
149+
if (!elt.eoo())
150+
bob.appendAs(elt, "db");
151+
152+
elt = _options.getField("authMechanism");
153+
if (!elt.eoo())
154+
bob.appendAs(elt, "mechanism");
155+
156+
elt = _options.getField("gssapiServiceName");
157+
if (!elt.eoo())
158+
bob.appendAs(elt, "serviceName");
159+
160+
return bob.obj();
161+
}
162+
114163
boost::mutex ConnectionString::_connectHookMutex;
115164
ConnectionString::ConnectionHook* ConnectionString::_connectHook = NULL;
116165

@@ -125,6 +174,17 @@ namespace mongo {
125174
delete c;
126175
return 0;
127176
}
177+
178+
if (!_user.empty()) {
179+
try {
180+
c->auth(_makeAuthObjFromOptions());
181+
}
182+
catch(...) {
183+
delete c;
184+
throw;
185+
}
186+
}
187+
128188
LOG(1) << "connected connection!" << endl;
129189
return c;
130190
}
@@ -138,6 +198,17 @@ namespace mongo {
138198
errmsg += toString();
139199
return 0;
140200
}
201+
202+
if (!_user.empty()) {
203+
try {
204+
set->auth(_makeAuthObjFromOptions());
205+
}
206+
catch(...) {
207+
delete set;
208+
throw;
209+
}
210+
}
211+
141212
return set;
142213
}
143214

@@ -192,26 +263,107 @@ namespace mongo {
192263
verify( false );
193264
}
194265

195-
ConnectionString ConnectionString::parse( const string& host , string& errmsg ) {
266+
ConnectionString ConnectionString::parse( const string& url , string& errmsg ) {
196267

197-
string::size_type i = host.find( '/' );
268+
if ( boost::algorithm::starts_with( url, "mongodb://" ) )
269+
return _parseURL( url, errmsg );
270+
271+
string::size_type i = url.find( '/' );
198272
if ( i != string::npos && i != 0) {
199273
// replica set
200-
return ConnectionString( SET , host.substr( i + 1 ) , host.substr( 0 , i ) );
274+
return ConnectionString( SET , url.substr( i + 1 ) , url.substr( 0 , i ) );
201275
}
202276

203-
int numCommas = str::count( host , ',' );
277+
int numCommas = str::count( url , ',' );
204278

205279
if( numCommas == 0 )
206-
return ConnectionString( HostAndPort( host ) );
280+
return ConnectionString( HostAndPort( url ) );
207281

208282
if ( numCommas == 1 )
209-
return ConnectionString( PAIR , host );
283+
return ConnectionString( PAIR , url );
210284

211-
errmsg = (string)"invalid hostname [" + host + "]";
285+
errmsg = (string)"invalid hostname [" + url + "]";
212286
return ConnectionString(); // INVALID
213287
}
214288

289+
ConnectionString ConnectionString::_parseURL( const string& url, string& errmsg ) {
290+
291+
const boost::regex mongoUrlRe(kMongoDBURL);
292+
293+
boost::smatch matches;
294+
if (!boost::regex_match(url, matches, mongoUrlRe)) {
295+
errmsg = "Failed to parse mongodb:// URL: " + url;
296+
return ConnectionString();
297+
}
298+
299+
// We have 5 top level captures, plus the whole input.
300+
invariant(matches.size() == 6);
301+
302+
if (!matches[3].matched) {
303+
errmsg = "No server(s) specified";
304+
return ConnectionString();
305+
}
306+
307+
std::map<std::string, std::string> options;
308+
309+
if (matches[5].matched) {
310+
const std::string optionsMatch = matches[5].str();
311+
312+
std::vector< boost::iterator_range<std::string::const_iterator> > optionsTokens;
313+
boost::algorithm::split(
314+
optionsTokens, optionsMatch, boost::algorithm::is_any_of("=&"));
315+
316+
invariant(optionsTokens.size() % 2 == 0);
317+
318+
for (size_t i = 0; i != optionsTokens.size(); i = i + 2)
319+
options[std::string(optionsTokens[i].begin(), optionsTokens[i].end())] =
320+
std::string(optionsTokens[i + 1].begin(), optionsTokens[i + 1].end());
321+
}
322+
323+
// Validate options against global driver state, and transform
324+
// or append relevant options to the auth struct.
325+
326+
std::map<std::string, std::string>::const_iterator optIter;
327+
328+
// If a replica set option was specified, store it in the 'setName' field.
329+
bool haveSetName;
330+
std::string setName;
331+
if ((haveSetName = ((optIter = options.find("replicaSet")) != options.end())))
332+
setName = optIter->second;
333+
334+
// If an SSL option was specified that conflicts with the global setting, error out.
335+
// The driver doesn't offer per connection settings.
336+
if ((optIter = options.find("ssl")) != options.end()) {
337+
if (optIter->second != (client::Options::current().SSLEnabled() ? "true" : "false")) {
338+
errmsg = "Cannot override global driver SSL state in connection URL";
339+
return ConnectionString();
340+
}
341+
}
342+
343+
// Add all remaining options into the bson object
344+
BSONObjBuilder optionsBob;
345+
for (optIter = options.begin(); optIter != options.end(); ++optIter)
346+
optionsBob.append(optIter->first, optIter->second);
347+
348+
std::string servers = matches[3].str();
349+
const bool direct = !haveSetName && (servers.find(',') == std::string::npos);
350+
351+
if (!direct && setName.empty()) {
352+
errmsg = "Cannot list multiple servers in URL without 'replicaSet' option";
353+
return ConnectionString();
354+
}
355+
356+
return ConnectionString(
357+
direct ? MASTER : SET,
358+
matches[1].str(),
359+
matches[2].str(),
360+
servers,
361+
matches[4].str(),
362+
setName,
363+
optionsBob.obj());
364+
365+
}
366+
215367
string ConnectionString::typeToString( ConnectionType type ) {
216368
switch ( type ) {
217369
case INVALID:

0 commit comments

Comments
 (0)