mongo 1.9.2 → 1.10.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE +1 -1
  5. data/README.md +94 -334
  6. data/Rakefile +6 -4
  7. data/VERSION +1 -1
  8. data/bin/mongo_console +13 -6
  9. data/lib/mongo.rb +22 -27
  10. data/lib/mongo/bulk_write_collection_view.rb +352 -0
  11. data/lib/mongo/collection.rb +128 -188
  12. data/lib/mongo/collection_writer.rb +348 -0
  13. data/lib/mongo/connection.rb +19 -0
  14. data/lib/mongo/{util → connection}/node.rb +15 -1
  15. data/lib/mongo/{util → connection}/pool.rb +34 -19
  16. data/lib/mongo/{util → connection}/pool_manager.rb +8 -2
  17. data/lib/mongo/{util → connection}/sharding_pool_manager.rb +1 -1
  18. data/lib/mongo/connection/socket.rb +18 -0
  19. data/lib/mongo/{util → connection/socket}/socket_util.rb +5 -2
  20. data/lib/mongo/{util → connection/socket}/ssl_socket.rb +3 -4
  21. data/lib/mongo/{util → connection/socket}/tcp_socket.rb +25 -15
  22. data/lib/mongo/{util → connection/socket}/unix_socket.rb +6 -4
  23. data/lib/mongo/cursor.rb +113 -47
  24. data/lib/mongo/db.rb +203 -131
  25. data/lib/mongo/{exceptions.rb → exception.rb} +7 -1
  26. data/lib/mongo/functional.rb +19 -0
  27. data/lib/mongo/functional/authentication.rb +303 -0
  28. data/lib/mongo/{util → functional}/logging.rb +1 -1
  29. data/lib/mongo/{util → functional}/read_preference.rb +49 -1
  30. data/lib/mongo/{util → functional}/uri_parser.rb +81 -69
  31. data/lib/mongo/{util → functional}/write_concern.rb +2 -1
  32. data/{test/unit/pool_test.rb → lib/mongo/gridfs.rb} +5 -10
  33. data/lib/mongo/gridfs/grid.rb +1 -3
  34. data/lib/mongo/gridfs/grid_ext.rb +1 -1
  35. data/lib/mongo/gridfs/grid_file_system.rb +1 -1
  36. data/lib/mongo/gridfs/grid_io.rb +1 -1
  37. data/lib/mongo/legacy.rb +63 -8
  38. data/lib/mongo/mongo_client.rb +128 -154
  39. data/lib/mongo/mongo_replica_set_client.rb +17 -11
  40. data/lib/mongo/mongo_sharded_client.rb +2 -1
  41. data/lib/mongo/networking.rb +19 -10
  42. data/lib/mongo/utils.rb +19 -0
  43. data/lib/mongo/{util → utils}/conversions.rb +1 -1
  44. data/lib/mongo/{util → utils}/core_ext.rb +1 -1
  45. data/lib/mongo/{util → utils}/server_version.rb +1 -1
  46. data/lib/mongo/{util → utils}/support.rb +10 -57
  47. data/lib/mongo/{util → utils}/thread_local_variable_manager.rb +1 -1
  48. data/test/functional/authentication_test.rb +8 -21
  49. data/test/functional/bulk_write_collection_view_test.rb +782 -0
  50. data/test/functional/{connection_test.rb → client_test.rb} +153 -78
  51. data/test/functional/collection_test.rb +343 -97
  52. data/test/functional/collection_writer_test.rb +83 -0
  53. data/test/functional/conversions_test.rb +1 -3
  54. data/test/functional/cursor_fail_test.rb +3 -3
  55. data/test/functional/cursor_message_test.rb +3 -3
  56. data/test/functional/cursor_test.rb +38 -3
  57. data/test/functional/db_api_test.rb +5 -5
  58. data/test/functional/db_connection_test.rb +2 -2
  59. data/test/functional/db_test.rb +35 -11
  60. data/test/functional/grid_file_system_test.rb +2 -2
  61. data/test/functional/grid_io_test.rb +2 -2
  62. data/test/functional/grid_test.rb +2 -2
  63. data/test/functional/pool_test.rb +2 -3
  64. data/test/functional/safe_test.rb +5 -5
  65. data/test/functional/ssl_test.rb +22 -102
  66. data/test/functional/support_test.rb +1 -1
  67. data/test/functional/timeout_test.rb +6 -22
  68. data/test/functional/uri_test.rb +113 -12
  69. data/test/functional/write_concern_test.rb +6 -6
  70. data/test/helpers/general.rb +50 -0
  71. data/test/helpers/test_unit.rb +309 -0
  72. data/test/replica_set/authentication_test.rb +8 -23
  73. data/test/replica_set/basic_test.rb +41 -14
  74. data/test/replica_set/client_test.rb +179 -117
  75. data/test/replica_set/complex_connect_test.rb +6 -7
  76. data/test/replica_set/connection_test.rb +46 -38
  77. data/test/replica_set/count_test.rb +2 -2
  78. data/test/replica_set/cursor_test.rb +8 -8
  79. data/test/replica_set/insert_test.rb +64 -2
  80. data/test/replica_set/max_values_test.rb +59 -10
  81. data/test/replica_set/pinning_test.rb +2 -2
  82. data/test/replica_set/query_test.rb +2 -2
  83. data/test/replica_set/read_preference_test.rb +6 -6
  84. data/test/replica_set/refresh_test.rb +7 -7
  85. data/test/replica_set/replication_ack_test.rb +5 -5
  86. data/test/replica_set/ssl_test.rb +24 -106
  87. data/test/sharded_cluster/basic_test.rb +43 -15
  88. data/test/shared/authentication/basic_auth_shared.rb +215 -0
  89. data/test/shared/authentication/sasl_plain_shared.rb +96 -0
  90. data/test/shared/ssl_shared.rb +173 -0
  91. data/test/test_helper.rb +31 -199
  92. data/test/threading/basic_test.rb +29 -3
  93. data/test/tools/mongo_config.rb +45 -20
  94. data/test/tools/mongo_config_test.rb +1 -1
  95. data/test/unit/client_test.rb +136 -57
  96. data/test/unit/collection_test.rb +31 -55
  97. data/test/unit/connection_test.rb +135 -72
  98. data/test/unit/cursor_test.rb +2 -2
  99. data/test/unit/db_test.rb +19 -15
  100. data/test/unit/grid_test.rb +2 -2
  101. data/test/unit/mongo_sharded_client_test.rb +17 -15
  102. data/test/unit/node_test.rb +2 -2
  103. data/test/unit/pool_manager_test.rb +7 -5
  104. data/test/unit/read_pref_test.rb +82 -2
  105. data/test/unit/read_test.rb +14 -14
  106. data/test/unit/safe_test.rb +9 -9
  107. data/test/unit/sharding_pool_manager_test.rb +11 -5
  108. data/test/unit/write_concern_test.rb +9 -9
  109. metadata +71 -56
  110. metadata.gz.sig +0 -0
  111. data/test/functional/threading_test.rb +0 -109
  112. data/test/shared/authentication.rb +0 -121
  113. data/test/unit/util_test.rb +0 -69
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013 10gen Inc.
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -65,6 +65,9 @@ module Mongo
65
65
  # Raised when a database operation fails.
66
66
  class OperationFailure < MongoDBError; end
67
67
 
68
+ # Raised when a database operation exceeds maximum specified time.
69
+ class ExecutionTimeout < OperationFailure; end
70
+
68
71
  # Raised when a socket read operation times out.
69
72
  class OperationTimeout < SocketError; end
70
73
 
@@ -76,4 +79,7 @@ module Mongo
76
79
 
77
80
  # Raised when the client supplies an invalid value to sort by.
78
81
  class InvalidSortValueError < MongoRubyError; end
82
+
83
+ # Raised for bulk write errors.
84
+ class BulkWriteError < OperationFailure; end
79
85
  end
@@ -0,0 +1,19 @@
1
+ # Copyright (C) 2009-2013 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
+ require 'mongo/functional/authentication'
16
+ require 'mongo/functional/logging'
17
+ require 'mongo/functional/read_preference'
18
+ require 'mongo/functional/write_concern'
19
+ require 'mongo/functional/uri_parser'
@@ -0,0 +1,303 @@
1
+ # Copyright (C) 2009-2013 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
+ require 'digest/md5'
16
+
17
+ module Mongo
18
+ module Authentication
19
+
20
+ DEFAULT_MECHANISM = 'MONGODB-CR'
21
+ MECHANISMS = ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN']
22
+
23
+ # authentication module methods
24
+ class << self
25
+ # Helper to validate an authentication mechanism and optionally
26
+ # raise an error if invalid.
27
+ #
28
+ # @param mechanism [String] [description]
29
+ # @param raise_error [Boolean] [description]
30
+ #
31
+ # @raise [ArgumentError] if raise_error and not a valid auth mechanism.
32
+ # @return [Boolean] returns the validation result.
33
+ def validate_mechanism(mechanism, raise_error=false)
34
+ return true if MECHANISMS.include?(mechanism.upcase)
35
+ if raise_error
36
+ raise ArgumentError,
37
+ "Invalid authentication mechanism provided. Must be one of " +
38
+ "#{Mongo::Authentication::MECHANISMS.join(', ')}."
39
+ end
40
+ false
41
+ end
42
+
43
+
44
+ # Helper to validate and normalize credential sets.
45
+ #
46
+ # @param auth [Hash] A hash containing the credential set.
47
+ #
48
+ # @raise [MongoArgumentError] if the credential set is invalid.
49
+ # @return [Hash] The validated credential set.
50
+ def validate_credentials(auth)
51
+ # set the default auth mechanism if not defined
52
+ auth[:mechanism] ||= DEFAULT_MECHANISM
53
+
54
+ # set the default auth source if not defined
55
+ auth[:source] = auth[:source] || auth[:db_name] || 'admin'
56
+
57
+ if (auth[:mechanism] == 'MONGODB-CR' || auth[:mechanism] == 'PLAIN') && !auth[:password]
58
+ raise MongoArgumentError,
59
+ "When using the authentication mechanism #{auth[:mechanism]} " +
60
+ "both username and password are required."
61
+ end
62
+ auth
63
+ end
64
+
65
+ # Generate an MD5 for authentication.
66
+ #
67
+ # @param username [String] The username.
68
+ # @param password [String] The user's password.
69
+ # @param nonce [String] The nonce value.
70
+ #
71
+ # @return [String] MD5 key for db authentication.
72
+ def auth_key(username, password, nonce)
73
+ Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
74
+ end
75
+
76
+ # Return a hashed password for auth.
77
+ #
78
+ # @param username [String] The username.
79
+ # @param password [String] The users's password.
80
+ #
81
+ # @return [String] The hashed password value.
82
+ def hash_password(username, password)
83
+ Digest::MD5.hexdigest("#{username}:mongo:#{password}")
84
+ end
85
+ end
86
+
87
+ # Saves a cache of authentication credentials to the current
88
+ # client instance. This method is called automatically by DB#authenticate.
89
+ #
90
+ # @param db_name [String] The current database name.
91
+ # @param username [String] The current username.
92
+ # @param password [String] (nil) The users's password (not required for
93
+ # all authentication mechanisms).
94
+ # @param source [String] (nil) The authentication source database
95
+ # (if different than the current database).
96
+ # @param mechanism [String] (nil) The authentication mechanism being used
97
+ # (default: 'MONGODB-CR').
98
+ #
99
+ # @raise [MongoArgumentError] Raised if the database has already been used
100
+ # for authentication. A log out is required before additional auths can
101
+ # be issued against a given database.
102
+ # @raise [AuthenticationError] Raised if authentication fails.
103
+ # @return [Hash] a hash representing the authentication just added.
104
+ def add_auth(db_name, username, password=nil, source=nil, mechanism=nil)
105
+ auth = Authentication.validate_credentials({
106
+ :db_name => db_name,
107
+ :username => username,
108
+ :password => password,
109
+ :source => source,
110
+ :mechanism => mechanism
111
+ })
112
+
113
+ if @auths.any? {|a| a[:source] == auth[:source]}
114
+ raise MongoArgumentError,
115
+ "Another user has already authenticated to the database " +
116
+ "'#{auth[:source]}' and multiple authentications are not " +
117
+ "permitted. Please logout first."
118
+ end
119
+
120
+ begin
121
+ socket = self.checkout_reader(:mode => :primary_preferred)
122
+ self.issue_authentication(auth, :socket => socket)
123
+ ensure
124
+ socket.checkin if socket
125
+ end
126
+
127
+ @auths << auth
128
+ auth
129
+ end
130
+
131
+ # Remove a saved authentication for this connection.
132
+ #
133
+ # @param db_name [String] The database name.
134
+ #
135
+ # @return [Boolean] The result of the operation.
136
+ def remove_auth(db_name)
137
+ return false unless @auths
138
+ @auths.reject! { |a| a[:source] == db_name } ? true : false
139
+ end
140
+
141
+ # Remove all authentication information stored in this connection.
142
+ #
143
+ # @return [Boolean] result of the operation.
144
+ def clear_auths
145
+ @auths = Set.new
146
+ true
147
+ end
148
+
149
+ # Method to handle and issue logout commands.
150
+ #
151
+ # @note This method should not be called directly. Use DB#logout.
152
+ #
153
+ # @param db_name [String] The database name.
154
+ # @param opts [Hash] Hash of optional settings and configuration values.
155
+ #
156
+ # @option opts [Socket] socket (nil) Optional socket instance to use.
157
+ #
158
+ # @raise [MongoDBError] Raised if the logout operation fails.
159
+ # @return [Boolean] The result of the logout operation.
160
+ def issue_logout(db_name, opts={})
161
+ doc = db(db_name).command({:logout => 1}, :socket => opts[:socket])
162
+ unless Support.ok?(doc)
163
+ raise MongoDBError, "Error logging out on DB #{db_name}."
164
+ end
165
+ true # somewhat pointless, but here to preserve the existing API
166
+ end
167
+
168
+ # Method to handle and issue authentication commands.
169
+ #
170
+ # @note This method should not be called directly. Use DB#authenticate.
171
+ #
172
+ # @param auth [Hash] The authentication credentials to be used.
173
+ # @param opts [Hash] Hash of optional settings and configuration values.
174
+ #
175
+ # @option opts [Socket] socket (nil) Optional socket instance to use.
176
+ #
177
+ # @raise [AuthenticationError] Raised if the authentication fails.
178
+ # @return [Boolean] Result of the authentication operation.
179
+ def issue_authentication(auth, opts={})
180
+ result = case auth[:mechanism]
181
+ when 'MONGODB-CR'
182
+ issue_cr(auth, opts)
183
+ when 'MONGODB-X509'
184
+ issue_x509(auth, opts)
185
+ when 'PLAIN'
186
+ issue_plain(auth, opts)
187
+ when 'GSSAPI'
188
+ issue_gssapi(auth, opts)
189
+ end
190
+
191
+ unless Support.ok?(result)
192
+ raise AuthenticationError,
193
+ "Failed to authenticate user '#{auth[:username]}' " +
194
+ "on db '#{auth[:source]}'."
195
+ end
196
+
197
+ true
198
+ end
199
+
200
+ private
201
+
202
+ # Handles issuing authentication commands for the MONGODB-CR auth mechanism.
203
+ #
204
+ # @param auth [Hash] The authentication credentials to be used.
205
+ # @param opts [Hash] Hash of optional settings and configuration values.
206
+ #
207
+ # @option opts [Socket] socket (nil) Optional socket instance to use.
208
+ #
209
+ # @return [Boolean] Result of the authentication operation.
210
+ #
211
+ # @private
212
+ def issue_cr(auth, opts={})
213
+ database = db(auth[:source])
214
+ nonce = get_nonce(database, opts)
215
+
216
+ # build auth command document
217
+ cmd = BSON::OrderedHash.new
218
+ cmd['authenticate'] = 1
219
+ cmd['user'] = auth[:username]
220
+ cmd['nonce'] = nonce
221
+ cmd['key'] = Authentication.auth_key(auth[:username],
222
+ auth[:password],
223
+ nonce)
224
+
225
+ database.command(cmd, :check_response => false,
226
+ :socket => opts[:socket])
227
+ end
228
+
229
+ # Handles issuing authentication commands for the MONGODB-X509 auth mechanism.
230
+ #
231
+ # @param auth [Hash] The authentication credentials to be used.
232
+ # @param opts [Hash] Hash of optional settings and configuration values.
233
+ #
234
+ # @private
235
+ def issue_x509(auth, opts={})
236
+ database = db('$external')
237
+
238
+ cmd = BSON::OrderedHash.new
239
+ cmd[:authenticate] = 1
240
+ cmd[:mechanism] = auth[:mechanism]
241
+ cmd[:user] = auth[:username]
242
+
243
+ database.command(cmd, :check_response => false,
244
+ :socket => opts[:socket])
245
+ end
246
+
247
+ # Handles issuing authentication commands for the PLAIN auth mechanism.
248
+ #
249
+ # @param auth [Hash] The authentication credentials to be used.
250
+ # @param opts [Hash] Hash of optional settings and configuration values.
251
+ #
252
+ # @option opts [Socket] socket (nil) Optional socket instance to use.
253
+ #
254
+ # @return [Boolean] Result of the authentication operation.
255
+ #
256
+ # @private
257
+ def issue_plain(auth, opts={})
258
+ database = db(auth[:source])
259
+ payload = "\x00#{auth[:username]}\x00#{auth[:password]}"
260
+
261
+ cmd = BSON::OrderedHash.new
262
+ cmd[:saslStart] = 1
263
+ cmd[:mechanism] = auth[:mechanism]
264
+ cmd[:payload] = BSON::Binary.new(payload)
265
+ cmd[:autoAuthorize] = 1
266
+
267
+ database.command(cmd, :check_response => false,
268
+ :socket => opts[:socket])
269
+ end
270
+
271
+ # Handles issuing authentication commands for the GSSAPI auth mechanism.
272
+ #
273
+ # @param auth [Hash] The authentication credentials to be used.
274
+ # @param opts [Hash] Hash of optional settings and configuration values.
275
+ #
276
+ # @private
277
+ def issue_gssapi(auth, opts={})
278
+ raise NotImplementedError,
279
+ "The #{auth[:mechanism]} authentication mechanism is not yet supported."
280
+ end
281
+
282
+ # Helper to fetch a nonce value from a given database instance.
283
+ #
284
+ # @param db [Mongo::DB] The DB instance to use for issue the nonce command.
285
+ # @param opts [Hash] Hash of optional settings and configuration values.
286
+ #
287
+ # @option opts [Socket] socket (nil) Optional socket instance to use.
288
+ #
289
+ # @raise [MongoDBError] Raised if there is an error executing the command.
290
+ # @return [String] Returns the nonce value.
291
+ #
292
+ # @private
293
+ def get_nonce(database, opts={})
294
+ doc = database.command({:getnonce => 1}, :check_response => false,
295
+ :socket => opts[:socket])
296
+ unless Support.ok?(doc)
297
+ raise MongoDBError, "Error retrieving nonce: #{doc}"
298
+ end
299
+ doc['nonce']
300
+ end
301
+
302
+ end
303
+ end
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013 10gen Inc.
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013 10gen Inc.
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -30,6 +30,24 @@ module Mongo
30
30
  :nearest => 'nearest'
31
31
  }
32
32
 
33
+ # Commands that may be sent to replica-set secondaries, depending on
34
+ # read preference and tags. All other commands are always run on the primary.
35
+ SECONDARY_OK_COMMANDS = [
36
+ 'group',
37
+ 'aggregate',
38
+ 'collstats',
39
+ 'dbstats',
40
+ 'count',
41
+ 'distinct',
42
+ 'geonear',
43
+ 'geosearch',
44
+ 'geowalk',
45
+ 'mapreduce',
46
+ 'replsetgetstatus',
47
+ 'ismaster',
48
+ 'parallelcollectionscan'
49
+ ]
50
+
33
51
  def self.mongos(mode, tag_sets)
34
52
  if mode != :secondary_preferred || !tag_sets.empty?
35
53
  mongos_read_preference = BSON::OrderedHash[:mode => MONGOS_MODES[mode]]
@@ -47,6 +65,36 @@ module Mongo
47
65
  end
48
66
  end
49
67
 
68
+ # Returns true if it's ok to run the command on a secondary
69
+ def self.secondary_ok?(selector)
70
+ command = selector.keys.first.to_s.downcase
71
+
72
+ if command == 'mapreduce'
73
+ out = selector.select { |k, v| k.to_s.downcase == 'out' }.first.last
74
+ # the server only looks at the first key in the out object
75
+ return out.respond_to?(:keys) && out.keys.first.to_s.downcase == 'inline'
76
+ elsif command == 'aggregate'
77
+ return selector['pipeline'].none? { |op| op.key?('$out') || op.key?(:$out) }
78
+ end
79
+ SECONDARY_OK_COMMANDS.member?(command)
80
+ end
81
+
82
+ # Returns true if the command should be rerouted to the primary.
83
+ def self.reroute_cmd_primary?(read_pref, selector)
84
+ return false if read_pref == :primary
85
+ !secondary_ok?(selector)
86
+ end
87
+
88
+ # Given a command and read preference, possibly reroute to primary.
89
+ def self.cmd_read_pref(read_pref, selector)
90
+ ReadPreference::validate(read_pref)
91
+ if reroute_cmd_primary?(read_pref, selector)
92
+ warn "Database command '#{selector.keys.first}' rerouted to primary node"
93
+ read_pref = :primary
94
+ end
95
+ read_pref
96
+ end
97
+
50
98
  def read_preference
51
99
  {
52
100
  :mode => @read,
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2013 10gen Inc.
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -18,11 +18,9 @@ require 'uri'
18
18
  module Mongo
19
19
  class URIParser
20
20
 
21
- USER_REGEX = /(.+)/
22
- PASS_REGEX = /([^@,]+)/
23
- AUTH_REGEX = /(#{USER_REGEX}:#{PASS_REGEX}@)?/
21
+ AUTH_REGEX = /((.+)@)?/
24
22
 
25
- HOST_REGEX = /([-.\w]+)/
23
+ HOST_REGEX = /([-.\w]+)|(\[[^\]]+\])/
26
24
  PORT_REGEX = /(?::(\w+))?/
27
25
  NODE_REGEX = /((#{HOST_REGEX}#{PORT_REGEX},?)+)/
28
26
 
@@ -34,14 +32,16 @@ module Mongo
34
32
  SPEC_ATTRS = [:nodes, :auths]
35
33
 
36
34
  READ_PREFERENCES = {
37
- "primary" => :primary,
38
- "primarypreferred" => :primary_preferred,
39
- "secondary" => :secondary,
40
- "secondarypreferred" => :secondary_preferred,
41
- "nearest" => :nearest
35
+ 'primary' => :primary,
36
+ 'primarypreferred' => :primary_preferred,
37
+ 'secondary' => :secondary,
38
+ 'secondarypreferred' => :secondary_preferred,
39
+ 'nearest' => :nearest
42
40
  }
43
41
 
44
42
  OPT_ATTRS = [
43
+ :authmechanism,
44
+ :authsource,
45
45
  :connect,
46
46
  :connecttimeoutms,
47
47
  :fsync,
@@ -59,6 +59,8 @@ module Mongo
59
59
  ]
60
60
 
61
61
  OPT_VALID = {
62
+ :authmechanism => lambda { |arg| Mongo::Authentication.validate_mechanism(arg) },
63
+ :authsource => lambda { |arg| arg.length > 0 },
62
64
  :connect => lambda { |arg| [ 'direct', 'replicaset', 'true', 'false', true, false ].include?(arg) },
63
65
  :connecttimeoutms => lambda { |arg| arg =~ /^\d+$/ },
64
66
  :fsync => lambda { |arg| ['true', 'false'].include?(arg) },
@@ -76,6 +78,8 @@ module Mongo
76
78
  }
77
79
 
78
80
  OPT_ERR = {
81
+ :authmechanism => "must be one of #{Mongo::Authentication::MECHANISMS.join(', ')}",
82
+ :authsource => "must be a string containing the name of the database being used for authentication",
79
83
  :connect => "must be 'direct', 'replicaset', 'true', or 'false'",
80
84
  :connecttimeoutms => "must be an integer specifying milliseconds",
81
85
  :fsync => "must be 'true' or 'false'",
@@ -85,7 +89,7 @@ module Mongo
85
89
  :replicaset => "must be a string containing the name of the replica set to connect to",
86
90
  :safe => "must be 'true' or 'false'",
87
91
  :slaveok => "must be 'true' or 'false'",
88
- :sockettimeoutms => "must be an integer specifying milliseconds",
92
+ :settimeoutms => "must be an integer specifying milliseconds",
89
93
  :ssl => "must be 'true' or 'false'",
90
94
  :w => "must be an integer indicating number of nodes to replicate to or a string " +
91
95
  "specifying that replication is required to the majority or nodes with a " +
@@ -95,6 +99,8 @@ module Mongo
95
99
  }
96
100
 
97
101
  OPT_CONV = {
102
+ :authmechanism => lambda { |arg| arg.upcase },
103
+ :authsource => lambda { |arg| arg },
98
104
  :connect => lambda { |arg| arg == 'false' ? false : arg }, # convert 'false' to FalseClass
99
105
  :connecttimeoutms => lambda { |arg| arg.to_f / 1000 }, # stored as seconds
100
106
  :fsync => lambda { |arg| arg == 'true' ? true : false },
@@ -112,8 +118,11 @@ module Mongo
112
118
  }
113
119
 
114
120
  attr_reader :auths,
121
+ :authmechanism,
122
+ :authsource,
115
123
  :connect,
116
124
  :connecttimeoutms,
125
+ :db_name,
117
126
  :fsync,
118
127
  :journal,
119
128
  :nodes,
@@ -134,9 +143,8 @@ module Mongo
134
143
  # @note Passwords can contain any character except for ','
135
144
  #
136
145
  # @param [String] uri The MongoDB URI string.
137
- # @param [Hash,nil] extra_opts Extra options. Will override anything already specified in the URI.
138
- #
139
- # @core connections
146
+ # @param [Hash,nil] extra_opts Extra options. Will override anything
147
+ # already specified in the URI.
140
148
  def initialize(uri)
141
149
  if uri.start_with?('mongodb://')
142
150
  uri = uri[10..-1]
@@ -145,8 +153,8 @@ module Mongo
145
153
  end
146
154
 
147
155
  hosts, opts = uri.split('?')
148
- parse_hosts(hosts)
149
156
  parse_options(opts)
157
+ parse_hosts(hosts)
150
158
  validate_connect
151
159
  end
152
160
 
@@ -155,7 +163,7 @@ module Mongo
155
163
  # @note Don't confuse this with attribute getter method #connect.
156
164
  #
157
165
  # @return [MongoClient,MongoReplicaSetClient]
158
- def connection(extra_opts, legacy = false, sharded = false)
166
+ def connection(extra_opts={}, legacy = false, sharded = false)
159
167
  opts = connection_options.merge!(extra_opts)
160
168
  if(legacy)
161
169
  if replicaset?
@@ -218,26 +226,15 @@ module Mongo
218
226
  end
219
227
  opts[:wtimeout] = @wtimeoutms
220
228
 
221
- opts[:w] = 1 if @safe
222
- opts[:w] = @w if @w
223
- opts[:j] = @journal
229
+ opts[:w] = 1 if @safe
230
+ opts[:w] = @w if @w
231
+ opts[:j] = @journal
224
232
  opts[:fsync] = @fsync
225
233
 
226
- if @connecttimeoutms
227
- opts[:connect_timeout] = @connecttimeoutms
228
- end
229
-
230
- if @sockettimeoutms
231
- opts[:op_timeout] = @sockettimeoutms
232
- end
233
-
234
- if @pool_size
235
- opts[:pool_size] = @pool_size
236
- end
237
-
238
- if @readpreference
239
- opts[:read] = @readpreference
240
- end
234
+ opts[:connect_timeout] = @connecttimeoutms if @connecttimeoutms
235
+ opts[:op_timeout] = @sockettimeoutms if @sockettimeoutms
236
+ opts[:pool_size] = @pool_size if @pool_size
237
+ opts[:read] = @readpreference if @readpreference
241
238
 
242
239
  if @slaveok && !@readpreference
243
240
  unless replicaset?
@@ -247,14 +244,13 @@ module Mongo
247
244
  end
248
245
  end
249
246
 
250
- opts[:ssl] = @ssl
251
- opts[:auths] = auths
252
-
253
247
  if replicaset.is_a?(String)
254
248
  opts[:name] = replicaset
255
249
  end
256
250
 
257
- opts[:default_db] = @db
251
+ opts[:db_name] = @db_name if @db_name
252
+ opts[:auths] = @auths if @auths
253
+ opts[:ssl] = @ssl
258
254
  opts[:connect] = connect?
259
255
 
260
256
  opts
@@ -266,47 +262,63 @@ module Mongo
266
262
 
267
263
  private
268
264
 
269
- def parse_hosts(uri_without_proto)
265
+ def parse_hosts(uri_without_protocol)
270
266
  @nodes = []
271
- @auths = []
267
+ @auths = Set.new
272
268
 
273
- matches = MONGODB_URI_MATCHER.match(uri_without_proto)
274
-
275
- if !matches
276
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
269
+ unless matches = MONGODB_URI_MATCHER.match(uri_without_protocol)
270
+ raise MongoArgumentError,
271
+ "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
277
272
  end
278
273
 
279
- uname = matches[2]
280
- pwd = matches[3]
281
- hosturis = matches[4].split(',')
282
- @db = matches[8]
283
-
284
- hosturis.each do |hosturi|
285
- # If port is present, use it, otherwise use default port
286
- host, port = hosturi.split(':') + [MongoClient::DEFAULT_PORT]
274
+ user_info = matches[2].split(':') if matches[2]
275
+ host_info = matches[3].split(',')
276
+ @db_name = matches[8]
287
277
 
288
- if !(port.to_s =~ /^\d+$/)
289
- raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
278
+ host_info.each do |host|
279
+ if host[0,1] == '['
280
+ host, port = host.split(']:') << MongoClient::DEFAULT_PORT
281
+ host = host.end_with?(']') ? host[1...-1] : host[1..-1]
282
+ else
283
+ host, port = host.split(':') << MongoClient::DEFAULT_PORT
290
284
  end
291
-
292
- port = port.to_i
293
- @nodes << [host, port]
285
+ unless port.to_s =~ /^\d+$/
286
+ raise MongoArgumentError,
287
+ "Invalid port #{port}; port must be specified as digits."
288
+ end
289
+ @nodes << [host, port.to_i]
294
290
  end
295
291
 
296
292
  if @nodes.empty?
297
- raise MongoArgumentError, "No nodes specified. Please ensure that you've provided at least one node."
293
+ raise MongoArgumentError,
294
+ "No nodes specified. Please ensure that you've provided at " +
295
+ "least one node."
296
+ end
297
+
298
+ # no user info to parse, exit here
299
+ return unless user_info
300
+
301
+ # check for url encoding for username and password
302
+ username, password = user_info
303
+ if user_info.size > 2 ||
304
+ (username && username.include?('@')) ||
305
+ (password && password.include?('@'))
306
+
307
+ raise MongoArgumentError,
308
+ "The characters ':' and '@' in a username or password " +
309
+ "must be escaped (RFC 2396)."
298
310
  end
299
311
 
300
- if uname && pwd && @db
301
- auths << {
302
- :db_name => @db,
303
- :username => URI.unescape(uname),
304
- :password => URI.unescape(pwd)
305
- }
306
- elsif uname || pwd
307
- raise MongoArgumentError, 'MongoDB URI must include username, ' +
308
- 'password, and db if username and ' +
309
- 'password are specified.'
312
+ # if username exists, proceed adding to auth set
313
+ unless username.nil? || username.empty?
314
+ auth = Authentication.validate_credentials({
315
+ :db_name => @db_name,
316
+ :username => URI.unescape(username),
317
+ :password => password ? URI.unescape(password) : nil,
318
+ :source => @authsource,
319
+ :mechanism => @authmechanism
320
+ })
321
+ @auths << auth
310
322
  end
311
323
  end
312
324
 
@@ -321,7 +333,7 @@ module Mongo
321
333
  return if string_opts.empty?
322
334
 
323
335
  if string_opts.include?(';') and string_opts.include?('&')
324
- raise MongoArgumentError, "must not mix URL separators ; and &"
336
+ raise MongoArgumentError, 'must not mix URL separators ; and &'
325
337
  end
326
338
 
327
339
  opts = CGI.parse(string_opts).inject({}) do |memo, (key, value)|