mongo 2.24.1 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mongo_console +2 -2
  3. data/lib/mongo/address/validator.rb +1 -1
  4. data/lib/mongo/address.rb +1 -3
  5. data/lib/mongo/auth/aws/conversation.rb +0 -4
  6. data/lib/mongo/auth/aws/credentials_retriever.rb +3 -8
  7. data/lib/mongo/auth/base.rb +1 -1
  8. data/lib/mongo/auth/sasl_conversation_base.rb +1 -1
  9. data/lib/mongo/auth/scram_conversation_base.rb +1 -8
  10. data/lib/mongo/auth/stringprep.rb +1 -6
  11. data/lib/mongo/auth/user/view.rb +1 -1
  12. data/lib/mongo/bulk_write/result_combiner.rb +3 -3
  13. data/lib/mongo/bulk_write.rb +1 -1
  14. data/lib/mongo/client.rb +82 -12
  15. data/lib/mongo/cluster/reapers/cursor_reaper.rb +12 -5
  16. data/lib/mongo/cluster/sdam_flow.rb +4 -4
  17. data/lib/mongo/cluster/topology/base.rb +1 -1
  18. data/lib/mongo/cluster.rb +12 -28
  19. data/lib/mongo/collection/view/aggregation/behavior.rb +1 -1
  20. data/lib/mongo/collection/view/aggregation.rb +2 -4
  21. data/lib/mongo/collection/view/builder/aggregation.rb +3 -5
  22. data/lib/mongo/collection/view/builder/map_reduce.rb +1 -1
  23. data/lib/mongo/collection/view/change_stream.rb +14 -9
  24. data/lib/mongo/collection/view/map_reduce.rb +2 -11
  25. data/lib/mongo/collection/view/readable.rb +11 -7
  26. data/lib/mongo/collection/view/writable.rb +4 -4
  27. data/lib/mongo/collection/view.rb +8 -4
  28. data/lib/mongo/collection.rb +1 -1
  29. data/lib/mongo/crypt/auto_encrypter.rb +1 -1
  30. data/lib/mongo/crypt/binary.rb +7 -4
  31. data/lib/mongo/crypt/binding.rb +88 -10
  32. data/lib/mongo/crypt/context.rb +50 -19
  33. data/lib/mongo/crypt/encryption_io.rb +11 -5
  34. data/lib/mongo/crypt/handle.rb +17 -5
  35. data/lib/mongo/crypt/kms/credentials.rb +70 -31
  36. data/lib/mongo/crypt/kms/kmip/master_document.rb +30 -1
  37. data/lib/mongo/crypt/kms/master_key_document.rb +11 -6
  38. data/lib/mongo/crypt/kms.rb +12 -1
  39. data/lib/mongo/cursor.rb +8 -2
  40. data/lib/mongo/database/cursor_command_view.rb +95 -0
  41. data/lib/mongo/database.rb +144 -18
  42. data/lib/mongo/error/invalid_uri.rb +5 -1
  43. data/lib/mongo/error/operation_failure.rb +2 -2
  44. data/lib/mongo/error/parser.rb +3 -3
  45. data/lib/mongo/error/sdam_error_detection.rb +2 -3
  46. data/lib/mongo/grid/fs_bucket.rb +27 -21
  47. data/lib/mongo/grid/stream/write.rb +2 -2
  48. data/lib/mongo/index/view.rb +13 -7
  49. data/lib/mongo/monitoring/event/cmap/connection_check_out_failed.rb +1 -0
  50. data/lib/mongo/monitoring/event/cmap/connection_check_out_started.rb +1 -0
  51. data/lib/mongo/monitoring/event/cmap/connection_checked_in.rb +1 -0
  52. data/lib/mongo/monitoring/event/cmap/connection_checked_out.rb +1 -0
  53. data/lib/mongo/monitoring/event/cmap/connection_closed.rb +1 -0
  54. data/lib/mongo/monitoring/event/cmap/connection_created.rb +1 -0
  55. data/lib/mongo/monitoring/event/cmap/connection_ready.rb +1 -0
  56. data/lib/mongo/monitoring/event/cmap/pool_cleared.rb +1 -0
  57. data/lib/mongo/monitoring/event/cmap/pool_closed.rb +1 -0
  58. data/lib/mongo/monitoring/event/cmap/pool_created.rb +1 -0
  59. data/lib/mongo/monitoring/event/cmap/pool_ready.rb +1 -0
  60. data/lib/mongo/monitoring/event/command_failed.rb +1 -0
  61. data/lib/mongo/monitoring/event/command_started.rb +1 -0
  62. data/lib/mongo/monitoring/event/command_succeeded.rb +1 -0
  63. data/lib/mongo/monitoring/event/secure.rb +12 -17
  64. data/lib/mongo/monitoring/event/server_closed.rb +1 -0
  65. data/lib/mongo/monitoring/event/server_description_changed.rb +1 -0
  66. data/lib/mongo/monitoring/event/server_heartbeat_failed.rb +1 -0
  67. data/lib/mongo/monitoring/event/server_heartbeat_started.rb +1 -0
  68. data/lib/mongo/monitoring/event/server_heartbeat_succeeded.rb +1 -0
  69. data/lib/mongo/monitoring/event/server_opening.rb +1 -0
  70. data/lib/mongo/monitoring/event/topology_changed.rb +1 -0
  71. data/lib/mongo/monitoring/event/topology_closed.rb +1 -0
  72. data/lib/mongo/monitoring/event/topology_opening.rb +1 -0
  73. data/lib/mongo/operation/create/op_msg.rb +1 -0
  74. data/lib/mongo/operation/create_index/op_msg.rb +2 -1
  75. data/lib/mongo/operation/cursor_command/op_msg.rb +37 -0
  76. data/lib/mongo/operation/cursor_command/result.rb +60 -0
  77. data/lib/mongo/operation/cursor_command.rb +33 -0
  78. data/lib/mongo/operation/delete/op_msg.rb +1 -0
  79. data/lib/mongo/operation/drop/op_msg.rb +1 -0
  80. data/lib/mongo/operation/drop_database/op_msg.rb +1 -0
  81. data/lib/mongo/operation/drop_index/op_msg.rb +1 -0
  82. data/lib/mongo/operation/find/builder/command.rb +1 -1
  83. data/lib/mongo/operation/insert/bulk_result.rb +2 -4
  84. data/lib/mongo/operation/insert/op_msg.rb +1 -0
  85. data/lib/mongo/operation/result.rb +12 -25
  86. data/lib/mongo/operation/shared/causal_consistency_supported.rb +4 -2
  87. data/lib/mongo/operation/shared/executable.rb +1 -9
  88. data/lib/mongo/operation/shared/result/aggregatable.rb +2 -2
  89. data/lib/mongo/operation/shared/sessions_supported.rb +3 -5
  90. data/lib/mongo/operation/update/bulk_result.rb +1 -1
  91. data/lib/mongo/operation/update/op_msg.rb +1 -0
  92. data/lib/mongo/operation/write_command/op_msg.rb +2 -0
  93. data/lib/mongo/operation.rb +1 -0
  94. data/lib/mongo/options/redacted.rb +2 -2
  95. data/lib/mongo/protocol/compressed.rb +15 -11
  96. data/lib/mongo/protocol/message.rb +16 -16
  97. data/lib/mongo/protocol/registry.rb +1 -1
  98. data/lib/mongo/protocol/serializers.rb +1 -3
  99. data/lib/mongo/query_cache.rb +3 -5
  100. data/lib/mongo/server/connection.rb +3 -1
  101. data/lib/mongo/server/connection_common.rb +9 -1
  102. data/lib/mongo/server/connection_pool/generation_manager.rb +1 -1
  103. data/lib/mongo/server/connection_pool.rb +24 -10
  104. data/lib/mongo/server/description/features.rb +1 -1
  105. data/lib/mongo/server/description.rb +1 -1
  106. data/lib/mongo/server/monitor/connection.rb +3 -2
  107. data/lib/mongo/server/monitor.rb +133 -30
  108. data/lib/mongo/server/pending_connection.rb +1 -0
  109. data/lib/mongo/server/push_monitor.rb +16 -2
  110. data/lib/mongo/server.rb +27 -7
  111. data/lib/mongo/server_selector/base.rb +3 -6
  112. data/lib/mongo/server_selector/secondary_preferred.rb +4 -1
  113. data/lib/mongo/session.rb +22 -23
  114. data/lib/mongo/socket/ssl.rb +3 -3
  115. data/lib/mongo/socket.rb +1 -7
  116. data/lib/mongo/srv/monitor.rb +11 -2
  117. data/lib/mongo/srv/resolver.rb +1 -1
  118. data/lib/mongo/srv/result.rb +5 -3
  119. data/lib/mongo/timeout.rb +10 -15
  120. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +16 -2
  121. data/lib/mongo/uri/options_mapper.rb +21 -9
  122. data/lib/mongo/uri/srv_protocol.rb +4 -4
  123. data/lib/mongo/uri.rb +56 -8
  124. data/lib/mongo/utils.rb +1 -1
  125. data/lib/mongo/version.rb +1 -1
  126. metadata +6 -4
  127. data/lib/mongo/auth/stringprep/unicode_normalize/normalize.rb +0 -173
  128. data/lib/mongo/auth/stringprep/unicode_normalize/tables.rb +0 -1164
@@ -239,7 +239,7 @@ module Mongo
239
239
 
240
240
  session.unpin unless session.in_transaction?
241
241
 
242
- if server = session.pinned_server
242
+ if (server = session.pinned_server)
243
243
  # Here we assume that a mongos stays in the topology indefinitely.
244
244
  # This will no longer be the case once SRV polling is implemented.
245
245
 
@@ -398,9 +398,7 @@ module Mongo
398
398
  # @api private
399
399
  def candidates(cluster, deprioritized = [])
400
400
  servers = cluster.servers.reject { |s| deprioritized.include?(s) }
401
- if cluster.single?
402
- servers
403
- elsif cluster.sharded?
401
+ if cluster.single? || cluster.sharded?
404
402
  servers
405
403
  elsif cluster.replica_set?
406
404
  select_in_replica_set(servers)
@@ -689,8 +687,7 @@ module Mongo
689
687
  msg = ''
690
688
  dead_monitors = []
691
689
  cluster.servers_list.each do |server|
692
- thread = server.monitor.instance_variable_get(:@thread)
693
- dead_monitors << server if thread.nil? || !thread.alive?
690
+ dead_monitors << server unless server.monitor&.running?
694
691
  end
695
692
  if dead_monitors.any?
696
693
  msg += ". The following servers have dead monitor threads: #{dead_monitors.map(&:summary).join(', ')}"
@@ -101,7 +101,10 @@ module Mongo
101
101
  #
102
102
  # @since 2.0.0
103
103
  def select_in_replica_set(candidates)
104
- near_servers(secondaries(candidates)) + primary(candidates)
104
+ eligible = secondaries(candidates)
105
+ return near_servers(eligible) unless eligible.empty?
106
+
107
+ primary(candidates)
105
108
  end
106
109
 
107
110
  def max_staleness_allowed?
data/lib/mongo/session.rb CHANGED
@@ -459,13 +459,9 @@ module Mongo
459
459
  @inside_with_transaction = true
460
460
  @with_transaction_timeout_ms = options&.dig(:timeout_ms) || @options[:default_timeout_ms] || @client.timeout_ms
461
461
  @with_transaction_deadline = calculate_with_transaction_deadline(options)
462
- deadline = if @with_transaction_deadline
463
- # CSOT enabled, so we have a customer defined deadline.
464
- @with_transaction_deadline
465
- else
466
- # CSOT not enabled, so we use the default deadline, 120 seconds.
467
- Utils.monotonic_time + 120
468
- end
462
+ # When CSOT is enabled we have a customer defined deadline; otherwise
463
+ # fall back to the default deadline, 120 seconds.
464
+ deadline = @with_transaction_deadline || (Utils.monotonic_time + 120)
469
465
  transaction_in_progress = false
470
466
  transaction_attempt = 0
471
467
  last_error = nil
@@ -1054,7 +1050,7 @@ module Mongo
1054
1050
  # even if transaction's read concern is not set.
1055
1051
  # Read concern here is the one sent to the server and may
1056
1052
  # include afterClusterTime.
1057
- if rc = c[:readConcern]
1053
+ if (rc = c[:readConcern])
1058
1054
  rc = rc.dup
1059
1055
  rc.delete(:level)
1060
1056
  end
@@ -1201,7 +1197,7 @@ module Mongo
1201
1197
  def process(result)
1202
1198
  unless implicit?
1203
1199
  set_operation_time(result)
1204
- if cluster_time_doc = result.cluster_time
1200
+ if (cluster_time_doc = result.cluster_time)
1205
1201
  advance_cluster_time(cluster_time_doc)
1206
1202
  end
1207
1203
  end
@@ -1312,8 +1308,6 @@ module Mongo
1312
1308
  @inside_with_transaction
1313
1309
  end
1314
1310
 
1315
- private
1316
-
1317
1311
  # Get the read concern the session will use when starting a transaction.
1318
1312
  #
1319
1313
  # This is a driver style hash with underscore keys.
@@ -1324,11 +1318,24 @@ module Mongo
1324
1318
  # @return [ Hash ] The read concern used for starting transactions.
1325
1319
  #
1326
1320
  # @since 2.9.0
1321
+ # @api private
1327
1322
  def txn_read_concern
1328
1323
  # Read concern is inherited from client but not db or collection.
1329
1324
  txn_options[:read_concern] || @client.read_concern
1330
1325
  end
1331
1326
 
1327
+ # Returns causal consistency document if the last operation time is
1328
+ # known and causal consistency is enabled, otherwise returns nil.
1329
+ #
1330
+ # @api private
1331
+ def causal_consistency_doc
1332
+ return unless operation_time && causal_consistency?
1333
+
1334
+ { afterClusterTime: operation_time }
1335
+ end
1336
+
1337
+ private
1338
+
1332
1339
  def within_states?(*states)
1333
1340
  states.include?(@state)
1334
1341
  end
@@ -1346,14 +1353,6 @@ module Mongo
1346
1353
  (@client.write_concern && @client.write_concern.options)
1347
1354
  end
1348
1355
 
1349
- # Returns causal consistency document if the last operation time is
1350
- # known and causal consistency is enabled, otherwise returns nil.
1351
- def causal_consistency_doc
1352
- return unless operation_time && causal_consistency?
1353
-
1354
- { afterClusterTime: operation_time }
1355
- end
1356
-
1357
1356
  def causal_consistency?
1358
1357
  @causal_consistency ||= if @options.key?(:causal_consistency)
1359
1358
  !!@options[:causal_consistency]
@@ -1391,9 +1390,9 @@ module Mongo
1391
1390
  raise Mongo::Error::InvalidTransactionOperation,
1392
1391
  'timeoutMS cannot be overridden inside a withTransaction callback'
1393
1392
  end
1394
- elsif timeout_ms = opts[:timeout_ms]
1393
+ elsif (timeout_ms = opts[:timeout_ms])
1395
1394
  result[:operation_timeout_ms] = timeout_ms
1396
- elsif default_timeout_ms = options[:default_timeout_ms]
1395
+ elsif (default_timeout_ms = options[:default_timeout_ms])
1397
1396
  result[:operation_timeout_ms] = default_timeout_ms
1398
1397
  end
1399
1398
  end
@@ -1407,9 +1406,9 @@ module Mongo
1407
1406
  Utils.monotonic_time + (timeout / 1000.0)
1408
1407
  end
1409
1408
  }
1410
- if timeout_ms = opts&.dig(:timeout_ms)
1409
+ if (timeout_ms = opts&.dig(:timeout_ms))
1411
1410
  calc.call(timeout_ms)
1412
- elsif default_timeout_ms = @options[:default_timeout_ms]
1411
+ elsif (default_timeout_ms = @options[:default_timeout_ms])
1413
1412
  calc.call(default_timeout_ms)
1414
1413
  elsif @client.timeout_ms
1415
1414
  calc.call(@client.timeout_ms)
@@ -359,7 +359,7 @@ module Mongo
359
359
  if options[:ssl_cert]
360
360
  cert_text = File.read(options[:ssl_cert])
361
361
  cert_object = nil
362
- elsif cert_text = options[:ssl_cert_string]
362
+ elsif (cert_text = options[:ssl_cert_string])
363
363
  cert_object = nil
364
364
  else
365
365
  cert_object = options[:ssl_cert_object]
@@ -421,13 +421,13 @@ module Mongo
421
421
  # https://github.com/jruby/jruby-openssl/issues/176
422
422
  if BSON::Environment.jruby?
423
423
  [ OpenSSL::PKey::RSA, OpenSSL::PKey::DSA ].each do |cls|
424
- return cls.send(:new, *args)
424
+ return cls.new(*args)
425
425
  rescue OpenSSL::PKey::PKeyError
426
426
  # ignore
427
427
  end
428
428
  # Neither RSA nor DSA worked, fall through to trying PKey
429
429
  end
430
- OpenSSL::PKey.send(:read, *args)
430
+ OpenSSL::PKey.read(*args)
431
431
  end
432
432
 
433
433
  def set_cert_verification(context, options)
data/lib/mongo/socket.rb CHANGED
@@ -547,10 +547,6 @@ module Mongo
547
547
  !rv.nil?
548
548
  end
549
549
 
550
- def unix_socket?(sock)
551
- defined?(UNIXSocket) && sock.is_a?(UNIXSocket)
552
- end
553
-
554
550
  DEFAULT_TCP_KEEPINTVL = 10
555
551
 
556
552
  DEFAULT_TCP_KEEPCNT = 9
@@ -590,9 +586,7 @@ module Mongo
590
586
  yield
591
587
  rescue Errno::ETIMEDOUT => e
592
588
  raise Error::SocketTimeoutError, "#{e.class}: #{e} (for #{human_address})"
593
- rescue IOError, SystemCallError, ::SocketError => e
594
- raise Error::SocketError, "#{e.class}: #{e} (for #{human_address})"
595
- rescue OpenSSL::SSL::SSLError => e
589
+ rescue IOError, SystemCallError, ::SocketError, OpenSSL::SSL::SSLError => e
596
590
  raise Error::SocketError, "#{e.class}: #{e} (for #{human_address})"
597
591
  end
598
592
 
@@ -42,10 +42,13 @@ module Mongo
42
42
  # Options to pass through to Resolv::DNS constructor for SRV lookups.
43
43
  def initialize(cluster, **opts)
44
44
  @cluster = cluster
45
- raise ArgumentError, 'SRV URI is required' unless @srv_uri = opts.delete(:srv_uri)
45
+ raise ArgumentError, 'SRV URI is required' unless (@srv_uri = opts.delete(:srv_uri))
46
46
 
47
47
  @options = opts.freeze
48
- @resolver = Srv::Resolver.new(**opts)
48
+ # Per the polling-srv-records spec, mismatched-domain records and empty
49
+ # results MUST be logged and ignored, not raised. Force the polling
50
+ # resolver into log-and-continue mode regardless of caller options.
51
+ @resolver = Srv::Resolver.new(**opts, raise_on_invalid: false)
49
52
  @last_result = @srv_uri.srv_result
50
53
  @stop_semaphore = Semaphore.new
51
54
  end
@@ -82,6 +85,12 @@ module Mongo
82
85
  rescue Resolv::ResolvError => e
83
86
  log_warn("SRV monitor: unable to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
84
87
  return
88
+ rescue Mongo::Error => e
89
+ # Defense in depth: the polling resolver is configured to log-and-continue
90
+ # for mismatched-domain and empty-result cases, but if any Mongo::Error
91
+ # leaks out we MUST NOT let it terminate the SRV monitor thread.
92
+ log_warn("SRV monitor: error resolving hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
93
+ return
85
94
  end
86
95
 
87
96
  if last_result.empty?
@@ -142,7 +142,7 @@ module Mongo
142
142
  #
143
143
  # @return [ Boolean ] Whether an error should be raised.
144
144
  def raise_on_invalid?
145
- @raise_on_invalid ||= @options[:raise_on_invalid] || true
145
+ @options.fetch(:raise_on_invalid, true)
146
146
  end
147
147
  end
148
148
  end
@@ -123,9 +123,11 @@ module Mongo
123
123
  raise Error::MismatchedDomain.new(format(MISMATCHED_DOMAINNAME, record_host, srv_host_domain))
124
124
  end
125
125
 
126
- unless (record_host_parts.size > srv_host_domain.size) && (srv_host_domain == record_host_parts[-srv_host_domain.size..-1])
127
- raise Error::MismatchedDomain.new(format(MISMATCHED_DOMAINNAME, record_host, srv_host_domain))
128
- end
126
+ record_matches_domain = (record_host_parts.size > srv_host_domain.size) &&
127
+ (srv_host_domain == record_host_parts[-srv_host_domain.size..-1])
128
+ return if record_matches_domain
129
+
130
+ raise Error::MismatchedDomain.new(format(MISMATCHED_DOMAINNAME, record_host, srv_host_domain))
129
131
  end
130
132
  end
131
133
  end
data/lib/mongo/timeout.rb CHANGED
@@ -18,8 +18,7 @@ module Mongo
18
18
  # @api private
19
19
  module Timeout
20
20
  # A wrapper around Ruby core's Timeout::timeout method that provides
21
- # a standardized API for Ruby versions older and newer than 2.4.0,
22
- # which is when the third argument was introduced.
21
+ # a standardized API for calling it.
23
22
  #
24
23
  # @param [ Numeric ] sec The number of seconds before timeout.
25
24
  # @param [ Class ] klass The exception class to raise on timeout, optional.
@@ -28,19 +27,15 @@ module Mongo
28
27
  # on timeout, optional. When no error message is provided, the default
29
28
  # error message for the exception class is used.
30
29
  def timeout(sec, klass = nil, message = nil, &block)
31
- if message && RUBY_VERSION < '2.94.0'
32
- begin
33
- ::Timeout.timeout(sec, &block)
34
- rescue ::Timeout::Error
35
- raise klass, message
36
- end
37
- else
38
- # Jruby Timeout::timeout method does not support passing nil arguments.
39
- # Remove the nil arguments before passing them along to the core
40
- # Timeout::timeout method.
41
- optional_args = [ klass, message ].compact
42
- ::Timeout.timeout(sec, *optional_args, &block)
43
- end
30
+ # provide a default error class if the message is present; otherwise,
31
+ # the message will be interpreted as the error class, if klass is nil
32
+ klass ||= ::Timeout::Error if message
33
+
34
+ # Jruby Timeout::timeout method does not support passing nil arguments.
35
+ # Remove the nil arguments before passing them along to the core
36
+ # Timeout::timeout method.
37
+ optional_args = [ klass, message ].compact
38
+ ::Timeout.timeout(sec, *optional_args, &block)
44
39
  end
45
40
  module_function :timeout
46
41
  end
@@ -206,9 +206,23 @@ module Mongo
206
206
  # @param _context [ OpenTelemetry::Context ] the context (unused).
207
207
  # @param span [ OpenTelemetry::Trace::Span ] the current span.
208
208
  def process_cursor_context(result, _cursor_id, _context, span)
209
- return unless result.has_cursor_id? && result.cursor_id.positive?
209
+ cursor_id = normalize_cursor_id(result.cursor_id)
210
+ return unless cursor_id.positive?
210
211
 
211
- span.set_attribute('db.mongodb.cursor_id', result.cursor_id)
212
+ span.set_attribute('db.mongodb.cursor_id', cursor_id)
213
+ end
214
+
215
+ # Normalizes a cursor id to a plain Integer.
216
+ #
217
+ # OP_MSG replies deserialized in :bson mode expose the cursor id as a
218
+ # BSON::Int64, which does not implement Numeric#positive? and is not a
219
+ # valid OpenTelemetry attribute type.
220
+ #
221
+ # @param cursor_id [ Integer | BSON::Int64 ] the raw cursor id.
222
+ #
223
+ # @return [ Integer ] the cursor id as an Integer.
224
+ def normalize_cursor_id(cursor_id)
225
+ cursor_id.is_a?(BSON::Int64) ? cursor_id.value : cursor_id
212
226
  end
213
227
 
214
228
  # Records error status code if the command failed.
@@ -87,7 +87,7 @@ module Mongo
87
87
  strategy = URI_OPTION_MAP[key.downcase]
88
88
  if strategy.nil?
89
89
  log_warn("Unsupported URI option '#{key}' on URI '#{@string}'. It will be ignored.")
90
- return
90
+ next
91
91
  end
92
92
 
93
93
  group = strategy[:group]
@@ -125,7 +125,7 @@ module Mongo
125
125
  end
126
126
  next if v.nil?
127
127
 
128
- if type = spec[:type]
128
+ if (type = spec[:type])
129
129
  v = send("revert_#{type}", v)
130
130
  end
131
131
  canonical_key = URI_OPTION_CANONICAL_NAMES[uri_key]
@@ -163,7 +163,7 @@ module Mongo
163
163
  end
164
164
  next if v.nil?
165
165
 
166
- if type = spec[:type]
166
+ if (type = spec[:type])
167
167
  v = send("stringify_#{type}", v)
168
168
  end
169
169
  canonical_key = URI_OPTION_CANONICAL_NAMES[uri_key]
@@ -354,13 +354,25 @@ module Mongo
354
354
 
355
355
  # Converts the value into a boolean and returns it wrapped in an array.
356
356
  #
357
- # @param [ String ] name Name of the URI option being processed.
357
+ # tls and ssl are the only repeated_bool options. An invalid value is
358
+ # preserved as a nil element rather than warned-and-dropped, so that
359
+ # URI#validate_uri_options! can detect it and raise instead of silently
360
+ # falling back to a plaintext connection (RUBY-3832).
361
+ #
362
+ # @param [ String ] _name Name of the URI option being processed.
358
363
  # @param [ String ] value URI option value.
359
364
  #
360
- # @return [ Array<true | false> | nil ] The boolean value parsed and wraped
361
- # in an array.
362
- def convert_repeated_bool(name, value)
363
- [ convert_bool(name, value) ]
365
+ # @return [ Array<true | false | nil> ] The boolean value parsed and
366
+ # wrapped in an array.
367
+ def convert_repeated_bool(_name, value)
368
+ case value
369
+ when true, 'true', 'TRUE'
370
+ [ true ]
371
+ when false, 'false', 'FALSE'
372
+ [ false ]
373
+ else
374
+ [ nil ]
375
+ end
364
376
  end
365
377
 
366
378
  # Reverts a repeated boolean type.
@@ -379,7 +391,7 @@ module Mongo
379
391
  # @return [ Array<true | false> | nil ] The string.
380
392
  def stringify_repeated_bool(value)
381
393
  rep = revert_repeated_bool(value)
382
- if rep&.is_a?(Array)
394
+ if rep.is_a?(Array)
383
395
  rep.join(',')
384
396
  else
385
397
  rep
@@ -117,14 +117,14 @@ module Mongo
117
117
  raise Error::InvalidURI.new(@string, details, FORMAT)
118
118
  end
119
119
 
120
- # Gets the SRV resolver.
121
- # If domain verification fails or no SRV records are found,
122
- # an error must not be raised per the spec; instead, a warning is logged.
120
+ # Gets the SRV resolver used for initial URI parsing.
121
+ # Per the Initial DNS Seedlist Discovery spec, the driver MUST raise an
122
+ # error if domain verification fails or no SRV records are found, so
123
+ # raise_on_invalid is left at its default of true.
123
124
  #
124
125
  # @return [ Mongo::Srv::Resolver ]
125
126
  def resolver
126
127
  @resolver ||= Srv::Resolver.new(
127
- raise_on_invalid: false,
128
128
  resolv_options: options[:resolv_options],
129
129
  timeout: options[:connect_timeout]
130
130
  )
data/lib/mongo/uri.rb CHANGED
@@ -141,6 +141,19 @@ module Mongo
141
141
  # @since 2.5.0
142
142
  SCHEME_DELIM = '://'
143
143
 
144
+ # Placeholder used in place of cleartext credentials when a URI is
145
+ # rendered for display, logging, or error reporting.
146
+ CREDENTIALS_PLACEHOLDER = '<credentials>'
147
+
148
+ # Pattern matching the userinfo portion of a MongoDB connection string.
149
+ # Anchors at the start and is bounded to the authority component (stops at
150
+ # the first '/', '?', or '#'), but matches greedily up to the last '@' in
151
+ # that component so passwords containing an unescaped '@' are still fully
152
+ # redacted. Case-insensitive so an unusual scheme like 'MongoDB://' is
153
+ # redacted too — the parser will reject it, and the redactor must not be
154
+ # the thing that leaks the credentials in the resulting error.
155
+ USERINFO_REDACTION_REGEX = %r{\A(mongodb(?:\+srv)?://)[^/?#]*@}i.freeze
156
+
144
157
  # Error details for an invalid options format.
145
158
  #
146
159
  # @since 2.1.0
@@ -215,6 +228,24 @@ module Mongo
215
228
  # @since 2.1.0
216
229
  REPEATABLE_OPTIONS = %i[tag_sets ssl]
217
230
 
231
+ # Replace the userinfo portion of a MongoDB connection string with a
232
+ # placeholder so the result can safely be logged, displayed, or embedded
233
+ # in an exception message.
234
+ #
235
+ # The input is matched as a string, not parsed, so this is safe to call
236
+ # on malformed URIs (which is exactly when {InvalidURI} is raised).
237
+ #
238
+ # @param [ String ] string The raw URI string.
239
+ #
240
+ # @return [ String ] The URI with any userinfo replaced by
241
+ # {CREDENTIALS_PLACEHOLDER}, or the input unchanged if it is not a
242
+ # string or has no userinfo.
243
+ def self.redact(string)
244
+ return string unless string.is_a?(String)
245
+
246
+ string.sub(USERINFO_REDACTION_REGEX, "\\1#{CREDENTIALS_PLACEHOLDER}@")
247
+ end
248
+
218
249
  # Get either a URI object or a SRVProtocol URI object.
219
250
  #
220
251
  # @example Get the uri object.
@@ -320,20 +351,33 @@ module Mongo
320
351
  @database || Database::ADMIN
321
352
  end
322
353
 
323
- # Get the uri as a string.
354
+ # Get the uri as a string with any credentials redacted.
355
+ #
356
+ # Credentials are replaced with {CREDENTIALS_PLACEHOLDER} so the result is
357
+ # safe to log or display. Use {#credentials} to recover the original user
358
+ # and password.
324
359
  #
325
360
  # @example Get the uri as a string.
326
361
  # uri.to_s
327
362
  #
328
- # @return [ String ] The uri string.
363
+ # @return [ String ] The redacted uri string.
329
364
  def to_s
330
365
  reconstruct_uri
331
366
  end
332
367
 
368
+ # Return a redacted, human-readable representation of the URI. The
369
+ # default {Object#inspect} would dump {@string} and {@password} as
370
+ # instance variables, leaking credentials.
371
+ #
372
+ # @return [ String ] The redacted inspect string.
373
+ def inspect
374
+ "#<#{self.class.name}: #{reconstruct_uri}>"
375
+ end
376
+
333
377
  private
334
378
 
335
- # Reconstruct the URI from its parts. Invalid options are dropped and options
336
- # are converted to camelCase.
379
+ # Reconstruct the URI from its parts with credentials redacted. Invalid
380
+ # options are dropped and options are converted to camelCase.
337
381
  #
338
382
  # @return [ String ] the uri.
339
383
  def reconstruct_uri
@@ -349,9 +393,7 @@ module Mongo
349
393
  end.compact.join('&')
350
394
 
351
395
  uri = "#{scheme}#{SCHEME_DELIM}"
352
- uri += @user.to_s if @user
353
- uri += "#{AUTH_USER_PWD_DELIM}#{@password}" if @password
354
- uri += '@' if @user || @password
396
+ uri += "#{CREDENTIALS_PLACEHOLDER}@" if @user || @password
355
397
  uri += @query_hostname || servers
356
398
  uri += '/' if @database || !options.empty?
357
399
  uri += @database.to_s if @database
@@ -418,7 +460,7 @@ module Mongo
418
460
  end
419
461
 
420
462
  def parse_user!(string)
421
- if string && user = string.partition(AUTH_USER_PWD_DELIM)[0]
463
+ if string && (user = string.partition(AUTH_USER_PWD_DELIM)[0])
422
464
  raise_invalid_error!(UNESCAPED_USER_PWD) if UNSAFE.match?(user)
423
465
  user_decoded = decode(user)
424
466
  raise_invalid_error!(UNESCAPED_USER_PWD) if user_decoded =~ PERCENT_CHAR && encode(user_decoded) != user
@@ -461,6 +503,12 @@ module Mongo
461
503
  # 'tls' and 'ssl'. In order to fulfill this, we parse the values of each instance into an
462
504
  # array; assuming all values in the array are the same, we replace the array with that value.
463
505
  unless uri_options[:ssl].nil? || uri_options[:ssl].empty?
506
+ # A nil element means an invalid value (e.g. tls=yes) was supplied.
507
+ # Refuse it rather than silently connecting over plaintext (RUBY-3832).
508
+ if uri_options[:ssl].include?(nil)
509
+ raise_invalid_error_no_fmt!("invalid boolean value for 'tls' or 'ssl'; only 'true' and 'false' are accepted")
510
+ end
511
+
464
512
  unless uri_options[:ssl].uniq.length == 1
465
513
  raise_invalid_error_no_fmt!("all instances of 'tls' and 'ssl' must have the same value")
466
514
  end
data/lib/mongo/utils.rb CHANGED
@@ -73,7 +73,7 @@ module Mongo
73
73
  # @note server_api must have symbol keys or be a BSON::Document.
74
74
  module_function def transform_server_api(server_api)
75
75
  {}.tap do |doc|
76
- if version = server_api[:version]
76
+ if (version = server_api[:version])
77
77
  doc['apiVersion'] = version
78
78
  end
79
79
  doc['apiStrict'] = server_api[:strict] unless server_api[:strict].nil?
data/lib/mongo/version.rb CHANGED
@@ -5,5 +5,5 @@ module Mongo
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '2.24.1'
8
+ VERSION = '2.25.0'
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.24.1
4
+ version: 2.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -90,8 +90,6 @@ files:
90
90
  - lib/mongo/auth/stringprep.rb
91
91
  - lib/mongo/auth/stringprep/profiles/sasl.rb
92
92
  - lib/mongo/auth/stringprep/tables.rb
93
- - lib/mongo/auth/stringprep/unicode_normalize/normalize.rb
94
- - lib/mongo/auth/stringprep/unicode_normalize/tables.rb
95
93
  - lib/mongo/auth/user.rb
96
94
  - lib/mongo/auth/user/view.rb
97
95
  - lib/mongo/auth/x509.rb
@@ -191,6 +189,7 @@ files:
191
189
  - lib/mongo/cursor/nontailable.rb
192
190
  - lib/mongo/cursor_host.rb
193
191
  - lib/mongo/database.rb
192
+ - lib/mongo/database/cursor_command_view.rb
194
193
  - lib/mongo/database/view.rb
195
194
  - lib/mongo/dbref.rb
196
195
  - lib/mongo/deprecations.rb
@@ -369,6 +368,9 @@ files:
369
368
  - lib/mongo/operation/create_search_indexes/op_msg.rb
370
369
  - lib/mongo/operation/create_user.rb
371
370
  - lib/mongo/operation/create_user/op_msg.rb
371
+ - lib/mongo/operation/cursor_command.rb
372
+ - lib/mongo/operation/cursor_command/op_msg.rb
373
+ - lib/mongo/operation/cursor_command/result.rb
372
374
  - lib/mongo/operation/delete.rb
373
375
  - lib/mongo/operation/delete/bulk_result.rb
374
376
  - lib/mongo/operation/delete/op_msg.rb
@@ -559,7 +561,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
559
561
  - !ruby/object:Gem::Version
560
562
  version: '0'
561
563
  requirements: []
562
- rubygems_version: 4.0.12
564
+ rubygems_version: 4.0.17
563
565
  specification_version: 4
564
566
  summary: Ruby driver for MongoDB
565
567
  test_files: []