mongo 2.13.3 → 2.14.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/mongo/address/ipv4.rb +1 -1
  4. data/lib/mongo/address/ipv6.rb +1 -1
  5. data/lib/mongo/address.rb +1 -1
  6. data/lib/mongo/bulk_write.rb +17 -0
  7. data/lib/mongo/caching_cursor.rb +74 -0
  8. data/lib/mongo/client.rb +47 -8
  9. data/lib/mongo/cluster/topology/single.rb +1 -1
  10. data/lib/mongo/cluster.rb +3 -3
  11. data/lib/mongo/collection/view/aggregation.rb +25 -4
  12. data/lib/mongo/collection/view/builder/find_command.rb +38 -18
  13. data/lib/mongo/collection/view/explainable.rb +27 -8
  14. data/lib/mongo/collection/view/iterable.rb +72 -12
  15. data/lib/mongo/collection/view/readable.rb +12 -2
  16. data/lib/mongo/collection/view/writable.rb +15 -1
  17. data/lib/mongo/collection/view.rb +24 -20
  18. data/lib/mongo/collection.rb +26 -2
  19. data/lib/mongo/crypt/encryption_io.rb +6 -6
  20. data/lib/mongo/cursor.rb +1 -0
  21. data/lib/mongo/database/view.rb +1 -1
  22. data/lib/mongo/database.rb +8 -14
  23. data/lib/mongo/error/invalid_read_concern.rb +28 -0
  24. data/lib/mongo/error/server_certificate_revoked.rb +22 -0
  25. data/lib/mongo/error/unsupported_option.rb +14 -12
  26. data/lib/mongo/error.rb +2 -0
  27. data/lib/mongo/grid/fs_bucket.rb +37 -37
  28. data/lib/mongo/lint.rb +2 -1
  29. data/lib/mongo/logger.rb +3 -3
  30. data/lib/mongo/operation/aggregate/result.rb +9 -8
  31. data/lib/mongo/operation/collections_info/command.rb +0 -5
  32. data/lib/mongo/operation/collections_info/result.rb +3 -16
  33. data/lib/mongo/operation/delete/bulk_result.rb +2 -0
  34. data/lib/mongo/operation/delete/result.rb +3 -0
  35. data/lib/mongo/operation/explain/command.rb +4 -0
  36. data/lib/mongo/operation/explain/legacy.rb +4 -0
  37. data/lib/mongo/operation/explain/op_msg.rb +6 -0
  38. data/lib/mongo/operation/explain/result.rb +3 -0
  39. data/lib/mongo/operation/find/legacy/result.rb +2 -0
  40. data/lib/mongo/operation/find/result.rb +3 -0
  41. data/lib/mongo/operation/get_more/result.rb +3 -0
  42. data/lib/mongo/operation/indexes/result.rb +5 -0
  43. data/lib/mongo/operation/insert/bulk_result.rb +5 -0
  44. data/lib/mongo/operation/insert/result.rb +5 -0
  45. data/lib/mongo/operation/list_collections/result.rb +5 -0
  46. data/lib/mongo/operation/map_reduce/result.rb +10 -0
  47. data/lib/mongo/operation/parallel_scan/command.rb +2 -1
  48. data/lib/mongo/operation/parallel_scan/result.rb +4 -0
  49. data/lib/mongo/operation/result.rb +35 -6
  50. data/lib/mongo/operation/shared/bypass_document_validation.rb +1 -0
  51. data/lib/mongo/operation/shared/causal_consistency_supported.rb +1 -0
  52. data/lib/mongo/operation/shared/collections_info_or_list_collections.rb +2 -0
  53. data/lib/mongo/operation/shared/executable.rb +1 -0
  54. data/lib/mongo/operation/shared/idable.rb +2 -1
  55. data/lib/mongo/operation/shared/limited.rb +1 -0
  56. data/lib/mongo/operation/shared/object_id_generator.rb +1 -0
  57. data/lib/mongo/operation/shared/read_preference_supported.rb +36 -38
  58. data/lib/mongo/operation/shared/result/aggregatable.rb +1 -0
  59. data/lib/mongo/operation/shared/sessions_supported.rb +3 -3
  60. data/lib/mongo/operation/shared/specifiable.rb +1 -0
  61. data/lib/mongo/operation/shared/write.rb +1 -0
  62. data/lib/mongo/operation/shared/write_concern_supported.rb +1 -0
  63. data/lib/mongo/operation/update/legacy/result.rb +7 -0
  64. data/lib/mongo/operation/update/result.rb +8 -0
  65. data/lib/mongo/operation/users_info/result.rb +3 -0
  66. data/lib/mongo/operation.rb +2 -0
  67. data/lib/mongo/protocol/msg.rb +2 -2
  68. data/lib/mongo/protocol/query.rb +11 -11
  69. data/lib/mongo/query_cache.rb +242 -0
  70. data/lib/mongo/retryable.rb +8 -1
  71. data/lib/mongo/server/connection_common.rb +2 -2
  72. data/lib/mongo/server/connection_pool.rb +3 -0
  73. data/lib/mongo/server/monitor/connection.rb +3 -3
  74. data/lib/mongo/server/monitor.rb +1 -1
  75. data/lib/mongo/server/pending_connection.rb +2 -2
  76. data/lib/mongo/server/push_monitor.rb +1 -1
  77. data/lib/mongo/server.rb +5 -1
  78. data/lib/mongo/server_selector/base.rb +5 -1
  79. data/lib/mongo/server_selector/secondary_preferred.rb +7 -2
  80. data/lib/mongo/session.rb +3 -0
  81. data/lib/mongo/socket/ocsp_cache.rb +97 -0
  82. data/lib/mongo/socket/ocsp_verifier.rb +368 -0
  83. data/lib/mongo/socket/ssl.rb +45 -24
  84. data/lib/mongo/socket.rb +6 -4
  85. data/lib/mongo/srv/monitor.rb +7 -13
  86. data/lib/mongo/srv/resolver.rb +14 -10
  87. data/lib/mongo/timeout.rb +2 -0
  88. data/lib/mongo/uri/options_mapper.rb +582 -0
  89. data/lib/mongo/uri/srv_protocol.rb +3 -2
  90. data/lib/mongo/uri.rb +21 -390
  91. data/lib/mongo/utils.rb +12 -1
  92. data/lib/mongo/version.rb +1 -1
  93. data/lib/mongo.rb +9 -0
  94. data/spec/NOTES.aws-auth.md +12 -7
  95. data/spec/README.md +56 -1
  96. data/spec/integration/bson_symbol_spec.rb +2 -4
  97. data/spec/integration/bulk_write_spec.rb +48 -0
  98. data/spec/integration/client_authentication_options_spec.rb +55 -28
  99. data/spec/integration/connection_pool_populator_spec.rb +3 -1
  100. data/spec/integration/cursor_reaping_spec.rb +53 -17
  101. data/spec/integration/ocsp_connectivity_spec.rb +26 -0
  102. data/spec/integration/ocsp_verifier_cache_spec.rb +188 -0
  103. data/spec/integration/ocsp_verifier_spec.rb +334 -0
  104. data/spec/integration/query_cache_spec.rb +1045 -0
  105. data/spec/integration/query_cache_transactions_spec.rb +179 -0
  106. data/spec/integration/retryable_writes/retryable_writes_40_and_newer_spec.rb +1 -0
  107. data/spec/integration/retryable_writes/shared/performs_legacy_retries.rb +2 -0
  108. data/spec/integration/sdam_error_handling_spec.rb +69 -18
  109. data/spec/integration/sdam_events_spec.rb +7 -8
  110. data/spec/integration/server_selection_spec.rb +36 -0
  111. data/spec/integration/srv_monitoring_spec.rb +38 -3
  112. data/spec/integration/srv_spec.rb +56 -0
  113. data/spec/lite_spec_helper.rb +4 -2
  114. data/spec/mongo/address_spec.rb +1 -1
  115. data/spec/mongo/caching_cursor_spec.rb +70 -0
  116. data/spec/mongo/client_construction_spec.rb +54 -1
  117. data/spec/mongo/client_encryption_spec.rb +10 -16
  118. data/spec/mongo/client_spec.rb +40 -0
  119. data/spec/mongo/cluster/topology/single_spec.rb +14 -5
  120. data/spec/mongo/cluster_spec.rb +3 -0
  121. data/spec/mongo/collection/view/explainable_spec.rb +87 -4
  122. data/spec/mongo/collection/view/map_reduce_spec.rb +2 -0
  123. data/spec/mongo/collection_spec.rb +60 -0
  124. data/spec/mongo/crypt/auto_decryption_context_spec.rb +1 -1
  125. data/spec/mongo/crypt/auto_encryption_context_spec.rb +1 -1
  126. data/spec/mongo/crypt/data_key_context_spec.rb +1 -1
  127. data/spec/mongo/crypt/explicit_decryption_context_spec.rb +1 -1
  128. data/spec/mongo/crypt/explicit_encryption_context_spec.rb +1 -1
  129. data/spec/mongo/database_spec.rb +44 -64
  130. data/spec/mongo/error/no_server_available_spec.rb +1 -1
  131. data/spec/mongo/index/view_spec.rb +2 -4
  132. data/spec/mongo/logger_spec.rb +13 -11
  133. data/spec/mongo/operation/read_preference_legacy_spec.rb +19 -9
  134. data/spec/mongo/operation/read_preference_op_msg_spec.rb +3 -3
  135. data/spec/mongo/query_cache_spec.rb +279 -0
  136. data/spec/mongo/server/app_metadata_shared.rb +7 -33
  137. data/spec/mongo/server/connection_pool_spec.rb +7 -3
  138. data/spec/mongo/server/connection_spec.rb +14 -7
  139. data/spec/mongo/server_selector/secondary_preferred_spec.rb +6 -6
  140. data/spec/mongo/socket/ssl_spec.rb +1 -1
  141. data/spec/mongo/socket_spec.rb +1 -1
  142. data/spec/mongo/uri/srv_protocol_spec.rb +64 -33
  143. data/spec/mongo/uri_option_parsing_spec.rb +11 -11
  144. data/spec/mongo/uri_spec.rb +68 -41
  145. data/spec/mongo/utils_spec.rb +39 -0
  146. data/spec/runners/auth.rb +3 -0
  147. data/spec/runners/connection_string.rb +35 -124
  148. data/spec/runners/transactions/operation.rb +2 -13
  149. data/spec/spec_tests/cmap_spec.rb +7 -3
  150. data/spec/spec_tests/data/change_streams/change-streams-errors.yml +0 -1
  151. data/spec/spec_tests/data/change_streams/change-streams.yml +0 -1
  152. data/spec/spec_tests/data/cmap/pool-checkout-connection.yml +6 -2
  153. data/spec/spec_tests/data/cmap/pool-create-min-size.yml +3 -0
  154. data/spec/spec_tests/data/connection_string/valid-warnings.yml +24 -0
  155. data/spec/spec_tests/data/sdam_monitoring/discovered_standalone.yml +1 -3
  156. data/spec/spec_tests/data/sdam_monitoring/standalone.yml +2 -2
  157. data/spec/spec_tests/data/sdam_monitoring/standalone_repeated.yml +2 -2
  158. data/spec/spec_tests/data/sdam_monitoring/standalone_suppress_equal_description_changes.yml +2 -2
  159. data/spec/spec_tests/data/sdam_monitoring/standalone_to_rs_with_me_mismatch.yml +2 -2
  160. data/spec/spec_tests/data/uri_options/auth-options.yml +25 -0
  161. data/spec/spec_tests/data/uri_options/compression-options.yml +6 -3
  162. data/spec/spec_tests/data/uri_options/read-preference-options.yml +24 -0
  163. data/spec/spec_tests/data/uri_options/ruby-connection-options.yml +1 -0
  164. data/spec/spec_tests/data/uri_options/tls-options.yml +160 -4
  165. data/spec/spec_tests/dns_seedlist_discovery_spec.rb +9 -1
  166. data/spec/spec_tests/uri_options_spec.rb +31 -33
  167. data/spec/support/certificates/atlas-ocsp-ca.crt +28 -0
  168. data/spec/support/certificates/atlas-ocsp.crt +41 -0
  169. data/spec/support/client_registry.rb +4 -8
  170. data/spec/support/client_registry_macros.rb +4 -4
  171. data/spec/support/common_shortcuts.rb +45 -0
  172. data/spec/support/constraints.rb +23 -0
  173. data/spec/support/lite_constraints.rb +24 -0
  174. data/spec/support/matchers.rb +16 -0
  175. data/spec/support/ocsp +1 -0
  176. data/spec/support/session_registry.rb +52 -0
  177. data/spec/support/spec_config.rb +22 -12
  178. data/spec/support/spec_setup.rb +38 -48
  179. data/spec/support/utils.rb +19 -1
  180. data.tar.gz.sig +1 -3
  181. metadata +938 -933
  182. metadata.gz.sig +0 -0
  183. data/spec/integration/secondary_reads_spec.rb +0 -102
  184. data/spec/shared/LICENSE +0 -20
  185. data/spec/shared/bin/get-mongodb-download-url +0 -17
  186. data/spec/shared/lib/mrss/child_process_helper.rb +0 -80
  187. data/spec/shared/lib/mrss/cluster_config.rb +0 -221
  188. data/spec/shared/lib/mrss/constraints.rb +0 -346
  189. data/spec/shared/lib/mrss/docker_runner.rb +0 -265
  190. data/spec/shared/lib/mrss/lite_constraints.rb +0 -191
  191. data/spec/shared/lib/mrss/server_version_registry.rb +0 -115
  192. data/spec/shared/lib/mrss/spec_organizer.rb +0 -152
  193. data/spec/shared/lib/mrss/utils.rb +0 -15
  194. data/spec/shared/share/Dockerfile.erb +0 -231
  195. data/spec/shared/shlib/distro.sh +0 -73
  196. data/spec/shared/shlib/server.sh +0 -290
  197. data/spec/shared/shlib/set_env.sh +0 -128
@@ -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
@@ -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],
@@ -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],
@@ -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],
data/lib/mongo/server.rb CHANGED
@@ -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
@@ -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)
@@ -86,8 +86,13 @@ module Mongo
86
86
  #
87
87
  # @since 2.0.0
88
88
  def to_mongos
89
- # Always send the read preference to mongos: DRIVERS-1642.
90
- to_doc
89
+ if tag_sets.empty? && max_staleness.nil? && hedge.nil?
90
+ # The server preference is not sent to mongos as part of the query
91
+ # selector if there are no tag sets, for maximum backwards compatibility.
92
+ nil
93
+ else
94
+ to_doc
95
+ end
91
96
  end
92
97
 
93
98
  private
data/lib/mongo/session.rb CHANGED
@@ -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
 
@@ -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