mongo 2.13.1 → 2.14.0.rc1

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 (161) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +1 -4
  4. data/lib/mongo.rb +9 -0
  5. data/lib/mongo/address/ipv4.rb +1 -1
  6. data/lib/mongo/address/ipv6.rb +1 -1
  7. data/lib/mongo/bulk_write.rb +17 -0
  8. data/lib/mongo/caching_cursor.rb +74 -0
  9. data/lib/mongo/client.rb +47 -8
  10. data/lib/mongo/cluster.rb +3 -3
  11. data/lib/mongo/cluster/topology/single.rb +1 -1
  12. data/lib/mongo/collection.rb +26 -0
  13. data/lib/mongo/collection/view.rb +24 -20
  14. data/lib/mongo/collection/view/aggregation.rb +25 -4
  15. data/lib/mongo/collection/view/builder/find_command.rb +38 -18
  16. data/lib/mongo/collection/view/explainable.rb +27 -8
  17. data/lib/mongo/collection/view/iterable.rb +72 -12
  18. data/lib/mongo/collection/view/readable.rb +12 -2
  19. data/lib/mongo/collection/view/writable.rb +15 -1
  20. data/lib/mongo/crypt/encryption_io.rb +6 -6
  21. data/lib/mongo/cursor.rb +1 -0
  22. data/lib/mongo/database.rb +6 -0
  23. data/lib/mongo/error.rb +2 -0
  24. data/lib/mongo/error/invalid_read_concern.rb +28 -0
  25. data/lib/mongo/error/server_certificate_revoked.rb +22 -0
  26. data/lib/mongo/error/unsupported_option.rb +14 -12
  27. data/lib/mongo/lint.rb +2 -1
  28. data/lib/mongo/logger.rb +3 -3
  29. data/lib/mongo/operation.rb +2 -0
  30. data/lib/mongo/operation/aggregate/result.rb +9 -8
  31. data/lib/mongo/operation/collections_info/result.rb +2 -0
  32. data/lib/mongo/operation/delete/bulk_result.rb +2 -0
  33. data/lib/mongo/operation/delete/result.rb +3 -0
  34. data/lib/mongo/operation/explain/command.rb +4 -0
  35. data/lib/mongo/operation/explain/legacy.rb +4 -0
  36. data/lib/mongo/operation/explain/op_msg.rb +6 -0
  37. data/lib/mongo/operation/explain/result.rb +3 -0
  38. data/lib/mongo/operation/find/legacy/result.rb +2 -0
  39. data/lib/mongo/operation/find/result.rb +3 -0
  40. data/lib/mongo/operation/get_more/result.rb +3 -0
  41. data/lib/mongo/operation/indexes/result.rb +5 -0
  42. data/lib/mongo/operation/insert/bulk_result.rb +5 -0
  43. data/lib/mongo/operation/insert/result.rb +5 -0
  44. data/lib/mongo/operation/list_collections/result.rb +5 -0
  45. data/lib/mongo/operation/map_reduce/result.rb +10 -0
  46. data/lib/mongo/operation/parallel_scan/result.rb +4 -0
  47. data/lib/mongo/operation/result.rb +35 -6
  48. data/lib/mongo/operation/shared/bypass_document_validation.rb +1 -0
  49. data/lib/mongo/operation/shared/causal_consistency_supported.rb +1 -0
  50. data/lib/mongo/operation/shared/collections_info_or_list_collections.rb +2 -0
  51. data/lib/mongo/operation/shared/executable.rb +1 -0
  52. data/lib/mongo/operation/shared/idable.rb +2 -1
  53. data/lib/mongo/operation/shared/limited.rb +1 -0
  54. data/lib/mongo/operation/shared/object_id_generator.rb +1 -0
  55. data/lib/mongo/operation/shared/result/aggregatable.rb +1 -0
  56. data/lib/mongo/operation/shared/sessions_supported.rb +1 -0
  57. data/lib/mongo/operation/shared/specifiable.rb +1 -0
  58. data/lib/mongo/operation/shared/write.rb +1 -0
  59. data/lib/mongo/operation/shared/write_concern_supported.rb +1 -0
  60. data/lib/mongo/operation/update/legacy/result.rb +7 -0
  61. data/lib/mongo/operation/update/result.rb +8 -0
  62. data/lib/mongo/operation/users_info/result.rb +3 -0
  63. data/lib/mongo/query_cache.rb +242 -0
  64. data/lib/mongo/retryable.rb +8 -1
  65. data/lib/mongo/server.rb +5 -1
  66. data/lib/mongo/server/connection_common.rb +2 -2
  67. data/lib/mongo/server/connection_pool.rb +3 -0
  68. data/lib/mongo/server/monitor.rb +1 -1
  69. data/lib/mongo/server/monitor/connection.rb +3 -3
  70. data/lib/mongo/server/pending_connection.rb +2 -2
  71. data/lib/mongo/server/push_monitor.rb +1 -1
  72. data/lib/mongo/server_selector/base.rb +5 -1
  73. data/lib/mongo/session.rb +3 -0
  74. data/lib/mongo/socket.rb +6 -4
  75. data/lib/mongo/socket/ocsp_cache.rb +97 -0
  76. data/lib/mongo/socket/ocsp_verifier.rb +368 -0
  77. data/lib/mongo/socket/ssl.rb +45 -24
  78. data/lib/mongo/srv/monitor.rb +7 -13
  79. data/lib/mongo/srv/resolver.rb +14 -10
  80. data/lib/mongo/timeout.rb +2 -0
  81. data/lib/mongo/uri.rb +21 -390
  82. data/lib/mongo/uri/options_mapper.rb +582 -0
  83. data/lib/mongo/uri/srv_protocol.rb +3 -2
  84. data/lib/mongo/utils.rb +12 -1
  85. data/lib/mongo/version.rb +1 -1
  86. data/spec/NOTES.aws-auth.md +12 -7
  87. data/spec/README.md +56 -1
  88. data/spec/integration/bulk_write_spec.rb +48 -0
  89. data/spec/integration/client_authentication_options_spec.rb +55 -28
  90. data/spec/integration/connection_pool_populator_spec.rb +3 -1
  91. data/spec/integration/cursor_reaping_spec.rb +53 -17
  92. data/spec/integration/ocsp_connectivity_spec.rb +26 -0
  93. data/spec/integration/ocsp_verifier_cache_spec.rb +188 -0
  94. data/spec/integration/ocsp_verifier_spec.rb +334 -0
  95. data/spec/integration/query_cache_spec.rb +1045 -0
  96. data/spec/integration/query_cache_transactions_spec.rb +179 -0
  97. data/spec/integration/retryable_writes/retryable_writes_40_and_newer_spec.rb +1 -0
  98. data/spec/integration/retryable_writes/shared/performs_legacy_retries.rb +2 -0
  99. data/spec/integration/sdam_error_handling_spec.rb +68 -0
  100. data/spec/integration/server_selection_spec.rb +36 -0
  101. data/spec/integration/srv_monitoring_spec.rb +38 -3
  102. data/spec/integration/srv_spec.rb +56 -0
  103. data/spec/lite_spec_helper.rb +3 -1
  104. data/spec/mongo/address_spec.rb +1 -1
  105. data/spec/mongo/caching_cursor_spec.rb +70 -0
  106. data/spec/mongo/client_construction_spec.rb +54 -1
  107. data/spec/mongo/client_spec.rb +40 -0
  108. data/spec/mongo/cluster/topology/single_spec.rb +14 -5
  109. data/spec/mongo/cluster_spec.rb +3 -0
  110. data/spec/mongo/collection/view/explainable_spec.rb +87 -4
  111. data/spec/mongo/collection/view/map_reduce_spec.rb +2 -0
  112. data/spec/mongo/collection_spec.rb +60 -0
  113. data/spec/mongo/crypt/auto_decryption_context_spec.rb +1 -1
  114. data/spec/mongo/crypt/auto_encryption_context_spec.rb +1 -1
  115. data/spec/mongo/crypt/explicit_decryption_context_spec.rb +1 -1
  116. data/spec/mongo/crypt/explicit_encryption_context_spec.rb +1 -1
  117. data/spec/mongo/database_spec.rb +44 -0
  118. data/spec/mongo/error/no_server_available_spec.rb +1 -1
  119. data/spec/mongo/logger_spec.rb +13 -11
  120. data/spec/mongo/query_cache_spec.rb +279 -0
  121. data/spec/mongo/server/connection_pool_spec.rb +7 -3
  122. data/spec/mongo/server/connection_spec.rb +14 -7
  123. data/spec/mongo/socket/ssl_spec.rb +1 -1
  124. data/spec/mongo/socket_spec.rb +1 -1
  125. data/spec/mongo/uri/srv_protocol_spec.rb +64 -33
  126. data/spec/mongo/uri_option_parsing_spec.rb +11 -11
  127. data/spec/mongo/uri_spec.rb +68 -41
  128. data/spec/mongo/utils_spec.rb +39 -0
  129. data/spec/runners/auth.rb +3 -0
  130. data/spec/runners/connection_string.rb +35 -124
  131. data/spec/spec_tests/cmap_spec.rb +7 -3
  132. data/spec/spec_tests/data/change_streams/change-streams-errors.yml +0 -1
  133. data/spec/spec_tests/data/change_streams/change-streams.yml +0 -1
  134. data/spec/spec_tests/data/cmap/pool-checkout-connection.yml +6 -2
  135. data/spec/spec_tests/data/cmap/pool-create-min-size.yml +3 -0
  136. data/spec/spec_tests/data/connection_string/valid-warnings.yml +24 -0
  137. data/spec/spec_tests/data/sdam_monitoring/discovered_standalone.yml +1 -3
  138. data/spec/spec_tests/data/sdam_monitoring/standalone.yml +2 -2
  139. data/spec/spec_tests/data/sdam_monitoring/standalone_repeated.yml +2 -2
  140. data/spec/spec_tests/data/sdam_monitoring/standalone_suppress_equal_description_changes.yml +2 -2
  141. data/spec/spec_tests/data/sdam_monitoring/standalone_to_rs_with_me_mismatch.yml +2 -2
  142. data/spec/spec_tests/data/uri_options/auth-options.yml +25 -0
  143. data/spec/spec_tests/data/uri_options/compression-options.yml +6 -3
  144. data/spec/spec_tests/data/uri_options/read-preference-options.yml +24 -0
  145. data/spec/spec_tests/data/uri_options/ruby-connection-options.yml +1 -0
  146. data/spec/spec_tests/data/uri_options/tls-options.yml +160 -4
  147. data/spec/spec_tests/dns_seedlist_discovery_spec.rb +9 -1
  148. data/spec/spec_tests/uri_options_spec.rb +31 -33
  149. data/spec/support/certificates/atlas-ocsp-ca.crt +28 -0
  150. data/spec/support/certificates/atlas-ocsp.crt +41 -0
  151. data/spec/support/client_registry_macros.rb +11 -2
  152. data/spec/support/common_shortcuts.rb +45 -0
  153. data/spec/support/constraints.rb +23 -0
  154. data/spec/support/lite_constraints.rb +24 -0
  155. data/spec/support/matchers.rb +16 -0
  156. data/spec/support/ocsp +1 -0
  157. data/spec/support/session_registry.rb +52 -0
  158. data/spec/support/spec_config.rb +22 -0
  159. data/spec/support/utils.rb +19 -1
  160. metadata +38 -3
  161. metadata.gz.sig +0 -0
@@ -59,7 +59,14 @@ module Mongo
59
59
  def read_with_retry_cursor(session, server_selector, view, &block)
60
60
  read_with_retry(session, server_selector) do |server|
61
61
  result = yield server
62
- Cursor.new(view, result, server, session: session)
62
+
63
+ # RUBY-2367: This will be updated to allow the query cache to
64
+ # cache cursors with multi-batch results.
65
+ if QueryCache.enabled? && !view.collection.system_collection?
66
+ CachingCursor.new(view, result, server, session: session)
67
+ else
68
+ Cursor.new(view, result, server, session: session)
69
+ end
63
70
  end
64
71
  end
65
72
 
@@ -311,7 +311,7 @@ module Mongo
311
311
  #
312
312
  # @since 2.0.0
313
313
  def inspect
314
- "#<Mongo::Server:0x#{object_id} address=#{address.host}:#{address.port}>"
314
+ "#<Mongo::Server:0x#{object_id} address=#{address.host}:#{address.port} #{status}>"
315
315
  end
316
316
 
317
317
  # @return [ String ] String representing server status (e.g. PRIMARY).
@@ -352,6 +352,10 @@ module Mongo
352
352
  status += " replica_set=#{replica_set_name}"
353
353
  end
354
354
 
355
+ unless monitor&.running?
356
+ status += " NO-MONITORING"
357
+ end
358
+
355
359
  if @pool
356
360
  status += " pool=#{@pool.summary}"
357
361
  end
@@ -105,8 +105,8 @@ module Mongo
105
105
  options.select { |k, v| k.to_s.start_with?('ssl') }
106
106
  else
107
107
  # Due to the way options are propagated from the client, if we
108
- # decide that we don't want to use TLS we need to have the ssl
109
- # options explicitly set to false or the value provided to the
108
+ # decide that we don't want to use TLS we need to have the :ssl
109
+ # option explicitly set to false or the value provided to the
110
110
  # connection might be overwritten by the default inherited from
111
111
  # the client.
112
112
  {ssl: false}
@@ -766,6 +766,9 @@ module Mongo
766
766
  connection.disconnect!(reason: :error)
767
767
  raise
768
768
  end
769
+ rescue Error::SocketError, Error::SocketTimeoutError => exc
770
+ @server.unknown!(generation: exc.generation, stop_push_monitor: true)
771
+ raise
769
772
  end
770
773
 
771
774
  def check_invariants
@@ -254,7 +254,7 @@ module Mongo
254
254
  end
255
255
  rescue => exc
256
256
  msg = "Error running ismaster on #{server.address}"
257
- Utils.warn_monitor_exception(msg, exc,
257
+ Utils.warn_bg_exception(msg, exc,
258
258
  logger: options[:logger],
259
259
  log_prefix: options[:log_prefix],
260
260
  bg_error_backtrace: options[:bg_error_backtrace],
@@ -122,7 +122,7 @@ module Mongo
122
122
 
123
123
  # Sends a message and returns the result.
124
124
  #
125
- # @param [ Protocol::Message ] The message to send.
125
+ # @param [ Protocol::Message ] message The message to send.
126
126
  #
127
127
  # @return [ Protocol::Message ] The result.
128
128
  def dispatch(message)
@@ -131,7 +131,7 @@ module Mongo
131
131
 
132
132
  # Sends a preserialized message and returns the result.
133
133
  #
134
- # @param [ String ] The serialized message to send.
134
+ # @param [ String ] bytes The serialized message to send.
135
135
  #
136
136
  # @option opts [ Numeric ] :read_socket_timeout The timeout to use for
137
137
  # each read operation.
@@ -234,7 +234,7 @@ module Mongo
234
234
  reply
235
235
  rescue => exc
236
236
  msg = "Failed to handshake with #{address}"
237
- Utils.warn_monitor_exception(msg, exc,
237
+ Utils.warn_bg_exception(msg, exc,
238
238
  logger: options[:logger],
239
239
  log_prefix: options[:log_prefix],
240
240
  bg_error_backtrace: options[:bg_error_backtrace],
@@ -118,7 +118,7 @@ module Mongo
118
118
  end
119
119
  rescue => exc
120
120
  msg = "Failed to handshake with #{address}"
121
- Utils.warn_monitor_exception(msg, exc,
121
+ Utils.warn_bg_exception(msg, exc,
122
122
  logger: options[:logger],
123
123
  log_prefix: options[:log_prefix],
124
124
  bg_error_backtrace: options[:bg_error_backtrace],
@@ -158,7 +158,7 @@ module Mongo
158
158
  auth.login
159
159
  rescue => exc
160
160
  msg = "Failed to authenticate to #{address}"
161
- Utils.warn_monitor_exception(msg, exc,
161
+ Utils.warn_bg_exception(msg, exc,
162
162
  logger: options[:logger],
163
163
  log_prefix: options[:log_prefix],
164
164
  bg_error_backtrace: options[:bg_error_backtrace],
@@ -101,7 +101,7 @@ module Mongo
101
101
  end
102
102
  rescue Mongo::Error => exc
103
103
  msg = "Error running awaited ismaster on #{server.address}"
104
- Utils.warn_monitor_exception(msg, exc,
104
+ Utils.warn_bg_exception(msg, exc,
105
105
  logger: options[:logger],
106
106
  log_prefix: options[:log_prefix],
107
107
  bg_error_backtrace: options[:bg_error_backtrace],
@@ -265,7 +265,11 @@ module Mongo
265
265
  end
266
266
  end
267
267
 
268
- msg = "No #{name} server is available in cluster: #{cluster.summary} " +
268
+ msg = "No #{name} server"
269
+ if is_a?(ServerSelector::Secondary) && !tag_sets.empty?
270
+ msg += " with tag sets: #{tag_sets}"
271
+ end
272
+ msg += " is available in cluster: #{cluster.summary} " +
269
273
  "with timeout=#{server_selection_timeout}, " +
270
274
  "LT=#{local_threshold_with_cluster(cluster)}"
271
275
  msg += server_selection_diagnostic_message(cluster)
@@ -534,6 +534,7 @@ module Mongo
534
534
  #
535
535
  # @since 2.6.0
536
536
  def commit_transaction(options=nil)
537
+ QueryCache.clear
537
538
  check_if_ended!
538
539
  check_if_no_transaction!
539
540
 
@@ -602,6 +603,8 @@ module Mongo
602
603
  #
603
604
  # @since 2.6.0
604
605
  def abort_transaction
606
+ QueryCache.clear
607
+
605
608
  check_if_ended!
606
609
  check_if_no_transaction!
607
610
 
@@ -15,6 +15,8 @@
15
15
  require 'mongo/socket/ssl'
16
16
  require 'mongo/socket/tcp'
17
17
  require 'mongo/socket/unix'
18
+ require 'mongo/socket/ocsp_verifier'
19
+ require 'mongo/socket/ocsp_cache'
18
20
 
19
21
  module Mongo
20
22
 
@@ -25,10 +27,10 @@ module Mongo
25
27
  class Socket
26
28
  include ::Socket::Constants
27
29
 
28
- # Error message for SSL related exceptions.
30
+ # Error message for TLS related exceptions.
29
31
  #
30
32
  # @since 2.0.0
31
- SSL_ERROR = 'MongoDB may not be configured with SSL support'.freeze
33
+ SSL_ERROR = 'MongoDB may not be configured with TLS support'.freeze
32
34
 
33
35
  # Error message for timeouts on socket calls.
34
36
  #
@@ -129,7 +131,7 @@ module Mongo
129
131
  sock_arr = [ @socket ]
130
132
  if Kernel::select(sock_arr, nil, sock_arr, 0)
131
133
  # The eof? call is supposed to return immediately since select
132
- # indicated the socket is readable. However, if @socket is an SSL
134
+ # indicated the socket is readable. However, if @socket is a TLS
133
135
  # socket, eof? can block anyway - see RUBY-2140.
134
136
  begin
135
137
  Timeout.timeout(0.1) do
@@ -342,7 +344,7 @@ module Mongo
342
344
  end
343
345
 
344
346
  def read_buffer_size
345
- # Buffer size for non-SSL reads
347
+ # Buffer size for non-TLS reads
346
348
  # 64kb
347
349
  65536
348
350
  end
@@ -0,0 +1,97 @@
1
+ # Copyright (C) 2020 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
+ module Mongo
16
+ class Socket
17
+
18
+ # This module caches OCSP responses for their indicated validity time.
19
+ #
20
+ # The key is the CertificateId used for the OCSP request.
21
+ # The value is the SingleResponse on Ruby 2.4+, or the OpenStruct
22
+ # emulation of it on Ruby 2.3.
23
+ #
24
+ # @api private
25
+ module OcspCache
26
+ module_function def set(cert_id, response)
27
+ delete(cert_id)
28
+ responses << response
29
+ end
30
+
31
+ # Retrieves a cached SingleResponse for the specified CertificateId.
32
+ #
33
+ # This method may return expired responses if they are revoked.
34
+ # Such responses were valid when they were first received.
35
+ #
36
+ # This method may also return responses that are valid but that may
37
+ # expire by the time caller uses them. The caller should not perform
38
+ # update time checks on the returned response.
39
+ #
40
+ # @return [ OpenSSL::OCSP::SingleResponse | OpenStruct ] The previously
41
+ # retrieved response.
42
+ module_function def get(cert_id)
43
+ resp = responses.detect do |resp|
44
+ resp.certid.cmp(cert_id)
45
+ end
46
+ if resp
47
+ # Only expire responses with good status.
48
+ # Once a certificate is revoked, it should stay revoked forever,
49
+ # hence we should be able to cache revoked responses indefinitely.
50
+ if resp.cert_status == OpenSSL::OCSP::V_CERTSTATUS_GOOD &&
51
+ resp.next_update < Time.now
52
+ then
53
+ responses.delete(resp)
54
+ resp = nil
55
+ end
56
+ end
57
+
58
+ # If we have connected to a server and cached the OCSP response for it,
59
+ # and then never connect to that server again, the cached OCSP response
60
+ # is going to remain in memory indefinitely. Periodically remove all
61
+ # expired OCSP responses, not just the ones matching the certificate id
62
+ # we are querying by.
63
+ if rand < 0.01
64
+ responses.delete_if do |resp|
65
+ resp.next_update < Time.now
66
+ end
67
+ end
68
+
69
+ resp
70
+ end
71
+
72
+ module_function def delete(cert_id)
73
+ responses.delete_if do |resp|
74
+ resp.certid.cmp(cert_id)
75
+ end
76
+ end
77
+
78
+ # Clears the driver's OCSP response cache.
79
+ #
80
+ # @note Use Mongo.clear_ocsp_cache from applications instead of invoking
81
+ # this method directly.
82
+ module_function def clear
83
+ responses.replace([])
84
+ end
85
+
86
+ private
87
+
88
+ LOCK = Mutex.new
89
+
90
+ module_function def responses
91
+ LOCK.synchronize do
92
+ @responses ||= []
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,368 @@
1
+ # Copyright (C) 2020 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
+ module Net
16
+ autoload :HTTP, 'net/http'
17
+ end
18
+
19
+ module Mongo
20
+ class Socket
21
+
22
+ # OCSP endpoint verifier.
23
+ #
24
+ # After a TLS connection is established, this verifier inspects the
25
+ # certificate presented by the server, and if the certificate contains
26
+ # an OCSP URI, performs the OCSP status request to the specified URI
27
+ # (following up to 5 redirects) to verify the certificate status.
28
+ #
29
+ # @see https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/OCSP.html
30
+ #
31
+ # @api private
32
+ class OcspVerifier
33
+ include Loggable
34
+
35
+ # @param [ String ] host_name The host name being verified, for
36
+ # diagnostic output.
37
+ # @param [ OpenSSL::X509::Certificate ] cert The certificate presented by
38
+ # the server at host_name.
39
+ # @param [ OpenSSL::X509::Certificate ] ca_cert The CA certificate
40
+ # presented by the server or resolved locally from the server
41
+ # certificate.
42
+ # @param [ OpenSSL::X509::Store ] cert_store The certificate store to
43
+ # use for verifying OCSP response. This should be the same store as
44
+ # used in SSLContext used with the SSLSocket that we are verifying the
45
+ # certificate for. This must NOT be the CA certificate provided by
46
+ # the server (i.e. anything taken out of peer_cert) - otherwise the
47
+ # server would dictate which CA authorities the client trusts.
48
+ def initialize(host_name, cert, ca_cert, cert_store, **opts)
49
+ @host_name = host_name
50
+ @cert = cert
51
+ @ca_cert = ca_cert
52
+ @cert_store = cert_store
53
+ @options = opts
54
+ end
55
+
56
+ attr_reader :host_name
57
+ attr_reader :cert
58
+ attr_reader :ca_cert
59
+ attr_reader :cert_store
60
+ attr_reader :options
61
+
62
+ def timeout
63
+ options[:timeout] || 5
64
+ end
65
+
66
+ # @return [ Array<String> ] OCSP URIs in the specified server certificate.
67
+ def ocsp_uris
68
+ @ocsp_uris ||= begin
69
+ # https://tools.ietf.org/html/rfc3546#section-2.3
70
+ # prohibits multiple extensions with the same oid.
71
+ ext = cert.extensions.detect do |ext|
72
+ ext.oid == 'authorityInfoAccess'
73
+ end
74
+
75
+ if ext
76
+ # Our test certificates have multiple OCSP URIs.
77
+ ext.value.split("\n").select do |line|
78
+ line.start_with?('OCSP - URI:')
79
+ end.map do |line|
80
+ line.split(':', 2).last
81
+ end
82
+ else
83
+ []
84
+ end
85
+ end
86
+ end
87
+
88
+ def cert_id
89
+ @cert_id ||= OpenSSL::OCSP::CertificateId.new(
90
+ cert,
91
+ ca_cert,
92
+ OpenSSL::Digest::SHA1.new,
93
+ )
94
+ end
95
+
96
+ def verify_with_cache
97
+ handle_exceptions do
98
+ return false if ocsp_uris.empty?
99
+
100
+ resp = OcspCache.get(cert_id)
101
+ if resp
102
+ return return_ocsp_response(resp)
103
+ end
104
+
105
+ resp, errors = do_verify
106
+
107
+ if resp
108
+ OcspCache.set(cert_id, resp)
109
+ end
110
+
111
+ return_ocsp_response(resp, errors)
112
+ end
113
+ end
114
+
115
+ # @return [ true | false ] Whether the certificate was verified.
116
+ #
117
+ # @raise [ Error::ServerCertificateRevoked ] If the certificate was
118
+ # definitively revoked.
119
+ def verify
120
+ handle_exceptions do
121
+ return false if ocsp_uris.empty?
122
+
123
+ resp, errors = do_verify
124
+ return_ocsp_response(resp, errors)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def do_verify
131
+ # This synchronized array contains definitive pass/fail responses
132
+ # obtained from the responders. We'll take the first one but due to
133
+ # concurrency multiple responses may be produced and queued.
134
+ @resp_queue = Queue.new
135
+
136
+ # This synchronized array contains strings, one per responder, that
137
+ # explain why each responder hasn't produced a definitive response.
138
+ # These are concatenated and logged if none of the responders produced
139
+ # a definitive respnose, or if the main thread times out waiting for
140
+ # a definitive response (in which case some of the worker threads'
141
+ # diagnostics may be logged and some may not).
142
+ @resp_errors = Queue.new
143
+
144
+ @req = OpenSSL::OCSP::Request.new
145
+ @req.add_certid(cert_id)
146
+ @req.add_nonce
147
+ @serialized_req = @req.to_der
148
+
149
+ @outstanding_requests = ocsp_uris.count
150
+ @outstanding_requests_lock = Mutex.new
151
+
152
+ threads = ocsp_uris.map do |uri|
153
+ Thread.new do
154
+ verify_one_responder(uri)
155
+ end
156
+ end
157
+
158
+ resp = begin
159
+ ::Timeout.timeout(timeout) do
160
+ @resp_queue.shift
161
+ end
162
+ rescue ::Timeout::Error
163
+ nil
164
+ end
165
+
166
+ threads.map(&:kill)
167
+ threads.map(&:join)
168
+
169
+ [resp, @resp_errors]
170
+ end
171
+
172
+ def verify_one_responder(uri)
173
+ original_uri = uri
174
+ redirect_count = 0
175
+ http_response = nil
176
+ loop do
177
+ http_response = begin
178
+ uri = URI(uri)
179
+ Net::HTTP.start(uri.hostname, uri.port) do |http|
180
+ path = uri.path
181
+ if path.empty?
182
+ path = '/'
183
+ end
184
+ http.post(path, @serialized_req,
185
+ 'content-type' => 'application/ocsp-request')
186
+ end
187
+ rescue IOError, SystemCallError => e
188
+ @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed: #{e.class}: #{e}"
189
+ return false
190
+ end
191
+
192
+ code = http_response.code.to_i
193
+ if (300..399).include?(code)
194
+ redirected_uri = http_response.header['location']
195
+ uri = ::URI.join(uri, redirected_uri)
196
+ redirect_count += 1
197
+ if redirect_count > 5
198
+ @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed: too many redirects (6)"
199
+ return false
200
+ end
201
+ next
202
+ end
203
+
204
+ if code >= 400
205
+ @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed with HTTP status code #{http_response.code}" + report_response_body(http_response.body)
206
+ return false
207
+ end
208
+
209
+ if code != 200
210
+ # There must be a body provided with the response, if one isn't
211
+ # provided the response cannot be verified.
212
+ @resp_errors << "OCSP request to #{report_uri(original_uri, uri)} failed with unexpected HTTP status code #{http_response.code}" + report_response_body(http_response.body)
213
+ return false
214
+ end
215
+
216
+ break
217
+ end
218
+
219
+ resp = OpenSSL::OCSP::Response.new(http_response.body).basic
220
+ unless resp.verify([ca_cert], cert_store)
221
+ # Ruby's OpenSSL binding discards error information - see
222
+ # https://github.com/ruby/openssl/issues/395
223
+ @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} failed signature verification; set `OpenSSL.debug = true` to see why"
224
+ return false
225
+ end
226
+
227
+ if @req.check_nonce(resp) == 0
228
+ @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} included invalid nonce"
229
+ return false
230
+ end
231
+
232
+ if resp.respond_to?(:find_response)
233
+ # Ruby 2.4+
234
+ resp = resp.find_response(cert_id)
235
+ # TODO make a new class instead of patching the stdlib one?
236
+ resp.instance_variable_set('@uri', uri)
237
+ resp.instance_variable_set('@original_uri', original_uri)
238
+ class << resp
239
+ attr_reader :uri, :original_uri
240
+ end
241
+ else
242
+ # Ruby 2.3
243
+ found = nil
244
+ resp.status.each do |_cert_id, cert_status, revocation_reason, revocation_time, this_update, next_update, extensions|
245
+ if _cert_id.cmp(cert_id)
246
+ found = OpenStruct.new(
247
+ cert_status: cert_status,
248
+ certid: _cert_id,
249
+ next_update: next_update,
250
+ this_update: this_update,
251
+ revocation_reason: revocation_reason,
252
+ revocation_time: revocation_time,
253
+ extensions: extensions,
254
+ uri: uri,
255
+ original_uri: original_uri,
256
+ )
257
+ class << found
258
+ # Unlike the stdlib method, this one doesn't accept
259
+ # any arguments.
260
+ def check_validity
261
+ now = Time.now
262
+ this_update <= now && next_update >= now
263
+ end
264
+ end
265
+ break
266
+ end
267
+ end
268
+ resp = found
269
+ end
270
+
271
+ unless resp
272
+ @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} did not include information about the requested certificate"
273
+ return false
274
+ end
275
+
276
+ unless resp.check_validity
277
+ @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} was invalid: this_update was in the future or next_update time has passed"
278
+ return false
279
+ end
280
+
281
+ unless [
282
+ OpenSSL::OCSP::V_CERTSTATUS_GOOD,
283
+ OpenSSL::OCSP::V_CERTSTATUS_REVOKED,
284
+ ].include?(resp.cert_status)
285
+ @resp_errors << "OCSP response from #{report_uri(original_uri, uri)} had a non-definitive status: #{resp.cert_status}"
286
+ return false
287
+ end
288
+
289
+ # Note this returns the redirected URI
290
+ @resp_queue << resp
291
+ rescue => exc
292
+ Utils.warn_bg_exception("Error performing OCSP verification for '#{host_name}' via '#{uri}'", exc,
293
+ logger: options[:logger],
294
+ log_prefix: options[:log_prefix],
295
+ bg_error_backtrace: options[:bg_error_backtrace],
296
+ )
297
+ false
298
+ ensure
299
+ @outstanding_requests_lock.synchronize do
300
+ @outstanding_requests -= 1
301
+ if @outstanding_requests == 0
302
+ @resp_queue << nil
303
+ end
304
+ end
305
+ end
306
+
307
+ def return_ocsp_response(resp, errors = nil)
308
+ if resp
309
+ if resp.cert_status == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
310
+ raise_revoked_error(resp)
311
+ end
312
+ true
313
+ else
314
+ reasons = []
315
+ errors.length.times do
316
+ reasons << errors.shift
317
+ end
318
+ if reasons.empty?
319
+ msg = "No responses from responders: #{ocsp_uris.join(', ')} within #{timeout} seconds"
320
+ else
321
+ msg = "For responders #{ocsp_uris.join(', ')} with a timeout of #{timeout} seconds: #{reasons.join(', ')}"
322
+ end
323
+ log_warn("TLS certificate of '#{host_name}' could not be definitively verified via OCSP: #{msg}")
324
+ false
325
+ end
326
+ end
327
+
328
+ def handle_exceptions
329
+ begin
330
+ yield
331
+ rescue Error::ServerCertificateRevoked
332
+ raise
333
+ rescue => exc
334
+ Utils.warn_bg_exception(
335
+ "Error performing OCSP verification for '#{host_name}'",
336
+ exc,
337
+ **options)
338
+ false
339
+ end
340
+ end
341
+
342
+ def raise_revoked_error(resp)
343
+ if resp.uri == resp.original_uri
344
+ redirect = ''
345
+ else
346
+ redirect = " (redirected from #{resp.original_uri})"
347
+ end
348
+ raise Error::ServerCertificateRevoked, "TLS certificate of '#{host_name}' has been revoked according to '#{resp.uri}'#{redirect} for reason '#{resp.revocation_reason}' at '#{resp.revocation_time}'"
349
+ end
350
+
351
+ def report_uri(original_uri, uri)
352
+ if URI(uri) == URI(original_uri)
353
+ uri
354
+ else
355
+ "#{original_uri} (redirected to #{uri})"
356
+ end
357
+ end
358
+
359
+ def report_response_body(body)
360
+ if body
361
+ ": #{body}"
362
+ else
363
+ ''
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end