dalli 3.2.13 → 3.2.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f7bcbe37e631c3c733d1487e519501497702b873e81e6588d6dfad1ab96a65b
4
- data.tar.gz: d7a953d2c0052334d8edb7fa45306724ffa25cec64436dcceccc9847d057e8bd
3
+ metadata.gz: 1282508d83c234919dfb2c8a05c08d7069186663c83d4e3aaf040e6f503a2e9f
4
+ data.tar.gz: cb10108ea1d22c25630b5cfa36b6e038515e3f053e2354e3921718050138535b
5
5
  SHA512:
6
- metadata.gz: 8d7b0ed94102d333f611736c48d944b0f1010760ae646003db60cd02c90b1a047a5fd10f7a5ef077527a88594f8844cbe08f9647259749e78ade74d09d2e851b
7
- data.tar.gz: bf86e4604fc18eaf2bdf57d6abad82258745238112a9866e5da9e05c0f45b44d62f82ebe8b5519ecc83d00ec6ee94e2d11bb931aa14171081a0b731745633e6d
6
+ metadata.gz: 614a4eebdaa9cee1d9dc6afdfd45c96ff1ec7eda12f471a12c6c1eca7866af971d907a7c288aeb5a72734b3f4f72bfb685ba58337da2ac50146ed74a018c0d0a
7
+ data.tar.gz: 8f577fef557f593d9cb6327a54ec1b5d3df8e554a5935ba7a7faadf86259248a60f29204b3ad5a295024b676ebc481e925f81f330095a73e6649e11d182b1127
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 3.2.15
8
+ ==========
9
+
10
+ - Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
11
+ - A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
12
+ - Affects every supported line, with both protocols; with the binary protocol the connection stayed out of step after the block ended. Fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15
13
+ - With `protocol: :meta`, read values by their size instead of up to the first CRLF (GHSA-5xvx-r8rg-5qm2)
14
+ - `get`, `gat` and `cas` read a value as one line, so a value containing CR/LF was cut short and the rest was parsed as the next reply. A stored value containing a crafted reply was then returned for a later read of a different key, with flags that could cause it to be deserialized
15
+ - Affects 3.2.0 to 3.2.14 with `protocol: :meta`; fixed in 3.2.15. The binary protocol (3.2's default) and 4.0 and later aren't affected
16
+ - A per-request `raw: true` write no longer compresses the value (backport of #1213)
17
+ - Values at or above the compression threshold (4 KB by default) were compressed and flagged even with `raw: true`. Raw reads ignore flags, so reading them back with `raw: true` returned the compressed bytes
18
+ - Raw values are now stored exactly as given. Callers that pass `raw: true` without `compress: false` store large values uncompressed; Rails' `MemCacheStore` passes `compress: false` and isn't affected
19
+ - Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (backport of #1215)
20
+ - A failed or timed-out handshake left the TCP socket open until GC and raised `OpenSSL::SSL::SSLError` directly, so the server was never marked down. The socket is now closed, and the error goes through the usual retry and `socket_max_failures` handling, raising `Dalli::NetworkError` (or `Dalli::RingError` once no server is available)
21
+ - Certificate and hostname checks come only from the `ssl_context` you pass, and a bare `OpenSSL::SSL::SSLContext.new` checks neither. Dalli now logs a warning once per process when `verify_mode` doesn't include `VERIFY_PEER` or `verify_hostname` is false; see the new TLS section in the README
22
+ - A reply line longer than 8 KiB without a CRLF, or one cut short by the connection closing, is now a network error that closes the connection. Before, the line had no length limit (backport of #1216)
23
+
24
+ 3.2.14
25
+ ==========
26
+
27
+ - Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
28
+ - memcached stores an item made by auto-create (`ma` with `N`/`J`, or `mg` with `N`) for a base64-encoded key without its binary-key flag, and later replies name the item by its raw key bytes. A key containing spaces or CR/LF could then make a multi-get read the replies after it as different ones
29
+ - `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
30
+ - The new `verify_reply_keys` option makes `get_multi` ignore replies for keys they didn't request, which guards against items created by older Dalli versions or other clients until memcached is upgraded. It's off by default, since it costs a multi-get about 15%
31
+ - Affects `protocol: :meta` since 3.2.0; fixed in 5.2.3, 5.1.5, 5.0.11, 4.3.8 and 3.2.14. memcached 1.6.45 and earlier still name these items by their raw key bytes
32
+
7
33
  3.2.13
8
34
  ==========
9
35
 
data/README.md CHANGED
@@ -37,6 +37,29 @@ Dalli::Client.new('localhost:11211', serializer: Dalli::JSONSerializer)
37
37
 
38
38
  Per-request `raw: true` returns the stored bytes without deserializing or decompressing them, on every read method that accepts it (`get` and `fetch`).
39
39
 
40
+ ### Counters and verify_reply_keys
41
+
42
+ memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies (with `protocol: :meta`). Since 3.2.14, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
43
+
44
+ ## TLS
45
+
46
+ Pass an `OpenSSL::SSL::SSLContext` as `ssl_context:` to connect over TLS. Certificate and hostname verification come only from that context: Dalli adds no checks of its own. A bare `OpenSSL::SSL::SSLContext.new` verifies nothing, so it accepts any certificate from any server. Configure the context to verify both:
47
+
48
+ ```ruby
49
+ ssl_context = OpenSSL::SSL::SSLContext.new
50
+ ssl_context.set_params(
51
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
52
+ verify_hostname: true,
53
+ ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
54
+ )
55
+
56
+ Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
57
+ ```
58
+
59
+ Dalli logs a warning once per process when the context's `verify_mode` is `VERIFY_NONE`, or when it is `VERIFY_PEER` but `verify_hostname` is false.
60
+
61
+ A failed TLS handshake counts as a failed connection attempt, like a refused connection: once `socket_max_failures` attempts have failed, the server is marked down and requests fail over to the other servers.
62
+
40
63
  ## Development
41
64
 
42
65
  After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/dalli/client.rb CHANGED
@@ -45,6 +45,11 @@ module Dalli
45
45
  # useful for injecting a FIPS compliant hash object.
46
46
  # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
47
47
  # to communicate with memcached.
48
+ # - :verify_reply_keys - defaults to false. When true, get_multi only accepts replies for the keys it requested;
49
+ # after a reply naming another key, the rest of that server's replies are treated as misses.
50
+ # With the meta protocol, memcached can name an item by its raw key bytes when the item was
51
+ # created by auto-create from a base64-encoded key, which Dalli no longer does. Costs a
52
+ # multi-get about 15%.
48
53
  #
49
54
  def initialize(servers = nil, options = {})
50
55
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -187,6 +192,11 @@ module Dalli
187
192
  # Any error replies will be discarded at the end of the block, and
188
193
  # Dalli client methods invoked inside the block will not
189
194
  # have return values
195
+ #
196
+ # The block is quiet only for the thread or fiber that runs it. Another
197
+ # thread or fiber sharing this client can make requests meanwhile; a
198
+ # non-quiet one first reads the block's pending replies with a noop and
199
+ # discards them, so it reads only its own.
190
200
  def quiet
191
201
  old = Thread.current[::Dalli::QUIET]
192
202
  Thread.current[::Dalli::QUIET] = true
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'set'
4
5
  require 'socket'
5
6
  require 'timeout'
6
7
 
@@ -23,6 +24,7 @@ module Dalli
23
24
  def initialize(attribs, client_options = {})
24
25
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
25
26
  @options = client_options.merge(user_creds)
27
+ @verify_reply_keys = client_options[:verify_reply_keys] ? true : false
26
28
  @value_marshaller = ValueMarshaller.new(@options)
27
29
  @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
28
30
  end
@@ -39,7 +41,11 @@ module Dalli
39
41
 
40
42
  begin
41
43
  @connection_manager.start_request!
44
+ drain_pending_quiet_responses(opkey)
42
45
  response = send(opkey, *args)
46
+ # Quiet requests can leave replies on the socket; remember that so a
47
+ # later non-quiet request knows to read them first
48
+ @connection_manager.quiet_responses_pending = true if quiet? && opkey != :noop
43
49
 
44
50
  # pipelined_get emit query but doesn't read the response(s)
45
51
  @connection_manager.finish_request! unless opkey == :pipelined_get
@@ -105,7 +111,7 @@ module Dalli
105
111
 
106
112
  # If the status is ok and the key is not nil, then this is a
107
113
  # getkq response with a value that we want to set in the response hash
108
- values[key] = [value, cas] unless key.nil?
114
+ break unless keep_reading?(values, key, value, cas)
109
115
 
110
116
  # Get the next response from the buffer
111
117
  status, cas, key, value = response_buffer.process_single_getk_response
@@ -163,6 +169,18 @@ module Dalli
163
169
  private
164
170
 
165
171
  ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
172
+
173
+ # Reads the replies still pending from quiet requests before any
174
+ # non-quiet request, which reads a reply of its own. A quiet block reads
175
+ # them at its end, but the connection is shared: another thread or fiber
176
+ # (quiet? is fiber-local) can make a request while the block is still
177
+ # open, and would otherwise read the block's replies as its own.
178
+ def drain_pending_quiet_responses(opkey)
179
+ return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
180
+
181
+ noop
182
+ end
183
+
166
184
  def verify_allowed_quiet!(opkey)
167
185
  return if ALLOWED_QUIET_OPS.include?(opkey)
168
186
 
@@ -220,6 +238,7 @@ module Dalli
220
238
  end
221
239
 
222
240
  def pipelined_get(keys)
241
+ expect_reply_keys(keys)
223
242
  req = +''
224
243
  keys.each do |key|
225
244
  req << quiet_get_request(key)
@@ -232,6 +251,50 @@ module Dalli
232
251
  @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
233
252
  end
234
253
 
254
+ # A multi-get only accepts replies for the keys it asked for. With the
255
+ # meta protocol, memcached names an item created by auto-create (ma or mg
256
+ # with N) for a base64-encoded key by its raw bytes, without the b flag;
257
+ # such a key can contain spaces or CR/LF, which split the reply header and
258
+ # make the replies after it parse as different ones. A key that wasn't
259
+ # requested is how that shows up. Only with the verify_reply_keys option,
260
+ # since building the set costs a multi-get about 15%.
261
+ def expect_reply_keys(keys)
262
+ @expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
263
+ end
264
+
265
+ # Stores a getkq reply's value and returns true, unless its key wasn't
266
+ # requested: then the replies are out of step, so what was read so far is
267
+ # kept, the rest dropped, and false returned to stop reading
268
+ def keep_reading?(values, key, value, cas)
269
+ return true if key.nil?
270
+
271
+ if unexpected_reply_key?(key)
272
+ abandon_pipeline
273
+ return false
274
+ end
275
+
276
+ values[key] = [value, cas]
277
+ true
278
+ end
279
+
280
+ def unexpected_reply_key?(key)
281
+ !key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
282
+ end
283
+
284
+ # Keys compared as bytes; ASCII keys compare equal in any encoding as is
285
+ def reply_key_form(key)
286
+ key.ascii_only? ? key : key.b
287
+ end
288
+
289
+ # Drops the rest of this server's replies and closes the connection so
290
+ # nothing unread is left on it. Not a socket failure, so it doesn't count
291
+ # toward socket_max_failures; the keys not read are simply missing.
292
+ def abandon_pipeline
293
+ Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
294
+ response_buffer.clear
295
+ @connection_manager.close
296
+ end
297
+
235
298
  # Called after the noop response is received at the end of a set
236
299
  # of pipelined gets
237
300
  def finish_pipeline
@@ -139,7 +139,10 @@ module Dalli
139
139
  # We need to read all the responses at once.
140
140
  def noop
141
141
  write_noop
142
- response_processor.consume_all_responses_until_noop
142
+ result = response_processor.consume_all_responses_until_noop
143
+ # Everything up to and including the noop reply has been read
144
+ @connection_manager.quiet_responses_pending = false
145
+ result
143
146
  end
144
147
 
145
148
  def stats(info = '')
@@ -28,6 +28,7 @@ module Dalli
28
28
 
29
29
  attr_accessor :hostname, :port, :socket_type, :options
30
30
  attr_reader :sock
31
+ attr_writer :quiet_responses_pending
31
32
 
32
33
  def initialize(hostname, port, socket_type, client_options)
33
34
  @hostname = hostname
@@ -35,6 +36,7 @@ module Dalli
35
36
  @socket_type = socket_type
36
37
  @options = DEFAULTS.merge(client_options)
37
38
  @request_in_progress = false
39
+ @quiet_responses_pending = false
38
40
  @sock = nil
39
41
  @pid = nil
40
42
 
@@ -56,8 +58,9 @@ module Dalli
56
58
  @sock = memcached_socket
57
59
  @pid = Process.pid
58
60
  @request_in_progress = false
59
- rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
60
- # SocketError = DNS resolution failure
61
+ @quiet_responses_pending = false
62
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
63
+ # SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
61
64
  error_on_request!(e)
62
65
  end
63
66
 
@@ -122,6 +125,7 @@ module Dalli
122
125
  end
123
126
  @sock = nil
124
127
  @pid = nil
128
+ @quiet_responses_pending = false
125
129
  abort_request!
126
130
  end
127
131
 
@@ -136,6 +140,13 @@ module Dalli
136
140
  !@sock.nil?
137
141
  end
138
142
 
143
+ # True when a quiet request was written on this connection and its
144
+ # replies (memcached only suppresses some of them) haven't been
145
+ # consumed by a noop yet.
146
+ def quiet_responses_pending?
147
+ @quiet_responses_pending
148
+ end
149
+
139
150
  def request_in_progress?
140
151
  @request_in_progress
141
152
  end
@@ -161,9 +172,21 @@ module Dalli
161
172
  @request_in_progress = false
162
173
  end
163
174
 
175
+ # The longest reply line Dalli reads, in bytes, CRLF included. A meta
176
+ # reply header is at most about 500 bytes: "VA <size>" plus its flags,
177
+ # the largest being a 250-byte key base64-encoded to 336 bytes, a
178
+ # 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
179
+ # stats line (stats settings) carries a file path such as ext_path, up
180
+ # to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
181
+ # never sends CRLF can't grow the read buffer without bound.
182
+ MAX_LINE_BYTES = 8 * 1024
183
+
164
184
  def read_line
165
- data = @sock.gets("\r\n")
185
+ data = @sock.gets("\r\n", MAX_LINE_BYTES)
166
186
  error_on_request!('EOF in read_line') if data.nil?
187
+ # Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
188
+ # short by EOF; either way the connection is out of step
189
+ error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
167
190
  data
168
191
  rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
169
192
  error_on_request!(e)
@@ -34,7 +34,7 @@ module Dalli
34
34
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
35
35
  return true unless tokens.first == VA
36
36
 
37
- value = read_line
37
+ value = read_data(tokens[1].to_i)
38
38
  raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens))
39
39
  end
40
40
 
@@ -46,7 +46,7 @@ module Dalli
46
46
  cas = cas_from_tokens(tokens)
47
47
  return [nil, cas] unless tokens.first == VA
48
48
 
49
- value = read_line
49
+ value = read_data(tokens[1].to_i)
50
50
  [raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens)), cas]
51
51
  end
52
52
 
@@ -242,6 +242,15 @@ module Dalli
242
242
  line = read_line
243
243
  line&.split || []
244
244
  end
245
+
246
+ # Values are read by the size in the VA line, not up to the next CRLF:
247
+ # a value can contain CRLF and can be longer than MAX_LINE_BYTES
248
+ def read_data(data_size)
249
+ # Checked before the terminator is added: -1 or -2 would otherwise
250
+ # read 1 or 0 bytes and leave the connection out of step
251
+ check_value_size!(data_size)
252
+ @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
253
+ end
245
254
  end
246
255
  end
247
256
  end
@@ -13,6 +13,7 @@ module Dalli
13
13
  ##
14
14
  class Meta < Base
15
15
  TERMINATOR = "\r\n"
16
+ COUNTER_ADD_OPTIONS = { raw: true }.freeze # counters are stored raw
16
17
 
17
18
  def response_processor
18
19
  @response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
@@ -129,11 +130,42 @@ module Dalli
129
130
  def decr_incr(incr, key, delta, ttl, initial)
130
131
  ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
131
132
  encoded_key, base64 = KeyRegularizer.encode(key)
133
+ # memcached stores an item created by ma's auto-create (N/J) for a
134
+ # base64-encoded key without its binary-key flag, and later replies
135
+ # name it with the raw key bytes. Create such counters with an add.
136
+ return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && base64
137
+
132
138
  write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
133
139
  quiet: quiet?, base64: base64))
134
140
  response_processor.decr_incr unless quiet?
135
141
  end
136
142
 
143
+ # Like decr_incr with an initial value, without memcached's auto-create:
144
+ # on a miss the counter is added with the initial value (which is
145
+ # returned, as auto-create would), and the add losing a race to another
146
+ # client means the counter now exists, so the incr is tried again. In a
147
+ # quiet block both requests are sent and whichever applies takes effect.
148
+ def decr_incr_with_add(incr, key, delta, ttl, initial)
149
+ return quiet_decr_incr_with_add(incr, key, delta, ttl, initial) if quiet?
150
+
151
+ 2.times do
152
+ value = decr_incr(incr, key, delta, nil, nil)
153
+ return value unless value.nil?
154
+
155
+ write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
156
+ return initial if response_processor.meta_set_with_cas
157
+ end
158
+ decr_incr(incr, key, delta, nil, nil)
159
+ end
160
+
161
+ def quiet_decr_incr_with_add(incr, key, delta, ttl, initial)
162
+ encoded_key, base64 = KeyRegularizer.encode(key)
163
+ write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: nil, incr: incr, ttl: nil,
164
+ quiet: true, base64: base64))
165
+ # Sent with the rest of the quiet block, by its closing noop
166
+ write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
167
+ end
168
+
137
169
  # Other Commands
138
170
  def flush(delay = 0)
139
171
  write(RequestFormatter.flush(delay: delay))
@@ -144,7 +176,10 @@ module Dalli
144
176
  # We need to read all the responses at once.
145
177
  def noop
146
178
  write_noop
147
- response_processor.consume_all_responses_until_mn
179
+ result = response_processor.consume_all_responses_until_mn
180
+ # Everything up to and including the MN has been read
181
+ @connection_manager.quiet_responses_pending = false
182
+ result
148
183
  end
149
184
 
150
185
  def stats(info = nil)
@@ -97,11 +97,13 @@ module Dalli
97
97
  end
98
98
 
99
99
  # Checks whether we should apply compression when serializing a value
100
- # based on the specified options. Returns false unless the value
101
- # is greater than the minimum compression size. Otherwise returns
102
- # based on a method-level option if specified, falling back to the
103
- # server default.
100
+ # based on the specified options. Returns false for a raw request
101
+ # (raw reads ignore bitflags, so the value must be stored as given) or
102
+ # unless the value is greater than the minimum compression size.
103
+ # Otherwise returns based on a method-level option if specified,
104
+ # falling back to the server default.
104
105
  def compress_value?(value, req_options)
106
+ return false if req_options&.dig(:raw)
105
107
  return false unless value.bytesize >= compression_min_size
106
108
  return compress_by_default? unless req_options && !req_options[:compress].nil?
107
109
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
3
4
  require 'timeout'
4
5
 
5
6
  module Dalli
@@ -15,5 +16,11 @@ module Dalli
15
16
  else
16
17
  [Timeout::Error]
17
18
  end
19
+
20
+ # SSL errors during the TLS handshake count as a failed request: they
21
+ # trigger reconnection and, after socket_max_failures, mark the server
22
+ # down. openssl is required above because this file loads before
23
+ # dalli/socket, so OpenSSL may not be defined yet.
24
+ SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
18
25
  end
19
26
  end
data/lib/dalli/socket.rb CHANGED
@@ -132,12 +132,42 @@ module Dalli
132
132
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
133
133
  end
134
134
 
135
+ # Closes the TCP socket if the handshake fails or times out, so a failed
136
+ # attempt doesn't hold a file descriptor until GC.
135
137
  def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
138
+ warn_if_unverified(ssl_context)
139
+ connected = false
136
140
  ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
137
141
  ssl_socket.hostname = host
138
142
  ssl_socket.sync_close = true
139
143
  ssl_socket.connect
144
+ connected = true
140
145
  ssl_socket
146
+ ensure
147
+ tcp_socket.close unless connected
148
+ end
149
+
150
+ # Verification comes entirely from the caller's ssl_context, and a bare
151
+ # SSLContext.new verifies nothing, so warn (once per process) when the
152
+ # certificate or the hostname won't be checked.
153
+ def self.warn_if_unverified(ssl_context)
154
+ return if @ssl_verification_warning_logged
155
+
156
+ problem = unverified_problem(ssl_context)
157
+ return unless problem
158
+
159
+ Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
160
+ 'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
161
+ 'verify_hostname: true) to verify the server.'
162
+ @ssl_verification_warning_logged = true
163
+ end
164
+
165
+ def self.unverified_problem(ssl_context)
166
+ if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
167
+ 'verify_mode is VERIFY_NONE, so the server certificate is not checked'
168
+ elsif !ssl_context.verify_hostname
169
+ 'verify_hostname is false, so the certificate is not checked against the hostname'
170
+ end
141
171
  end
142
172
  end
143
173
 
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.2.13'
4
+ VERSION = '3.2.15'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.13
4
+ version: 3.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
90
  - !ruby/object:Gem::Version
91
91
  version: '0'
92
92
  requirements: []
93
- rubygems_version: 4.0.21
93
+ rubygems_version: 4.0.23
94
94
  specification_version: 4
95
95
  summary: High performance memcached client for Ruby
96
96
  test_files: []