mongo 2.13.1 → 2.14.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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