dalli 5.2.1 → 5.2.3

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.
@@ -84,11 +84,13 @@ module Dalli
84
84
  end
85
85
 
86
86
  # Checks whether we should apply compression when serializing a value
87
- # based on the specified options. Returns false unless the value
88
- # is greater than the minimum compression size. Otherwise returns
89
- # based on a method-level option if specified, falling back to the
90
- # server default.
87
+ # based on the specified options. Returns false for a raw request
88
+ # (raw reads ignore bitflags, so the value must be stored as given) or
89
+ # unless the value is greater than the minimum compression size.
90
+ # Otherwise returns based on a method-level option if specified,
91
+ # falling back to the server default.
91
92
  def compress_value?(value, req_options)
93
+ return false if req_options&.dig(:raw)
92
94
  return false unless value.bytesize >= compression_min_size
93
95
  return compress_by_default? unless req_options && !req_options[:compress].nil?
94
96
 
@@ -13,12 +13,30 @@ module Dalli
13
13
  extend Forwardable
14
14
 
15
15
  DEFAULTS = {
16
- # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
16
+ # max memcached item size in bytes: value + key + ITEM_OVERHEAD_BYTES. Set it to
17
+ # memcached's -I value (default 1 MB).
17
18
  value_max_bytes: 1024 * 1024
18
19
  }.freeze
19
20
 
20
21
  OPTIONS = DEFAULTS.keys.freeze
21
22
 
23
+ # memcached's -I limit applies to the whole item, not just the value. On
24
+ # 64-bit memcached 1.6 an item takes the value, the key, and 63 bytes of
25
+ # overhead: a 48-byte header, 8 bytes of CAS, 4 bytes of client flags,
26
+ # the key's NUL terminator and the value's trailing CRLF. (Measured
27
+ # against memcached 1.6.45 with -I 512k, 1m, 2m and 5m; an item stored
28
+ # with zero flags or with CAS disabled needs a few bytes less.)
29
+ ITEM_OVERHEAD_BYTES = 63
30
+
31
+ def self.error_if_over_max_value_bytes(key, value, value_max_bytes)
32
+ item_bytes = value.bytesize + key.bytesize + ITEM_OVERHEAD_BYTES
33
+ return if item_bytes <= value_max_bytes
34
+
35
+ message = "Value for #{key} over max size: #{value_max_bytes} <= #{item_bytes} " \
36
+ "(#{value.bytesize} value bytes + #{key.bytesize} key bytes + #{ITEM_OVERHEAD_BYTES} item overhead)"
37
+ raise Dalli::ValueOverMaxSize, message
38
+ end
39
+
22
40
  def_delegators :@value_serializer, :serializer
23
41
  def_delegators :@value_compressor, :compressor, :compression_min_size, :compress_by_default?
24
42
 
@@ -28,6 +46,7 @@ module Dalli
28
46
 
29
47
  @marshal_options =
30
48
  DEFAULTS.merge(client_options.slice(*OPTIONS))
49
+ @marshal_options[:value_max_bytes] = @marshal_options[:value_max_bytes].to_i
31
50
  end
32
51
 
33
52
  def store(key, value, options = nil)
@@ -49,10 +68,7 @@ module Dalli
49
68
  end
50
69
 
51
70
  def error_if_over_max_value_bytes(key, value)
52
- return if value.bytesize <= value_max_bytes
53
-
54
- message = "Value for #{key} over max size: #{value_max_bytes} <= #{value.bytesize}"
55
- raise Dalli::ValueOverMaxSize, message
71
+ self.class.error_if_over_max_value_bytes(key, value, value_max_bytes)
56
72
  end
57
73
  end
58
74
  end
@@ -16,9 +16,9 @@ module Dalli
16
16
  [Timeout::Error]
17
17
  end
18
18
 
19
- # SSL errors that occur during read/write operations (not during initial
20
- # handshake) should trigger reconnection. These indicate transient network
21
- # issues, not configuration problems.
19
+ # SSL errors during the handshake or during read/write operations count as
20
+ # a failed request: they trigger reconnection and, after
21
+ # socket_max_failures, mark the server down.
22
22
  SSL_ERRORS =
23
23
  if defined?(OpenSSL::SSL::SSLError)
24
24
  [OpenSSL::SSL::SSLError]
data/lib/dalli/socket.rb CHANGED
@@ -8,6 +8,12 @@ module Dalli
8
8
  # Various socket implementations used by Dalli.
9
9
  ##
10
10
  module Socket
11
+ # Timeout for establishing a connection: connect_timeout if set,
12
+ # otherwise socket_timeout.
13
+ def self.connect_timeout_for(options)
14
+ options[:connect_timeout] || options[:socket_timeout]
15
+ end
16
+
11
17
  ##
12
18
  # Common methods for all socket implementations.
13
19
  ##
@@ -162,11 +168,13 @@ module Dalli
162
168
  end
163
169
 
164
170
  def self.create_socket_with_timeout(host, port, options)
171
+ timeout = Dalli::Socket.connect_timeout_for(options)
165
172
  if supports_connect_timeout?
166
- sock = new(host, port, connect_timeout: options[:socket_timeout])
173
+ # resolv_timeout bounds the DNS lookup, which connect_timeout doesn't cover
174
+ sock = new(host, port, connect_timeout: timeout, resolv_timeout: timeout)
167
175
  yield(sock)
168
176
  else
169
- Timeout.timeout(options[:socket_timeout]) do
177
+ Timeout.timeout(timeout) do
170
178
  sock = new(host, port)
171
179
  yield(sock)
172
180
  end
@@ -228,12 +236,41 @@ module Dalli
228
236
  [seconds, microseconds].pack(timeval_pack_format(sock))
229
237
  end
230
238
 
239
+ # Closes the TCP socket if the handshake fails or times out, so a failed
240
+ # attempt doesn't hold a file descriptor until GC.
231
241
  def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
242
+ warn_if_unverified(ssl_context)
243
+ connected = false
232
244
  ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
233
245
  ssl_socket.hostname = host
234
246
  ssl_socket.sync_close = true
235
247
  ssl_socket.connect
248
+ connected = true
236
249
  ssl_socket
250
+ ensure
251
+ tcp_socket.close unless connected
252
+ end
253
+
254
+ # Class variable to track whether the TLS verification warning has been logged
255
+ @@ssl_verification_warning_logged = false # rubocop:disable Style/ClassVars
256
+
257
+ # Verification comes entirely from the caller's ssl_context, and a bare
258
+ # SSLContext.new verifies nothing, so warn (once per process) when the
259
+ # certificate or the hostname won't be checked.
260
+ def self.warn_if_unverified(ssl_context)
261
+ return if @@ssl_verification_warning_logged
262
+
263
+ problem = if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
264
+ 'verify_mode is VERIFY_NONE, so the server certificate is not checked'
265
+ elsif !ssl_context.verify_hostname
266
+ 'verify_hostname is false, so the certificate is not checked against the hostname'
267
+ end
268
+ return unless problem
269
+
270
+ Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
271
+ 'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
272
+ 'verify_hostname: true) to verify the server.'
273
+ @@ssl_verification_warning_logged = true # rubocop:disable Style/ClassVars
237
274
  end
238
275
  end
239
276
 
@@ -261,7 +298,7 @@ module Dalli
261
298
  attr_accessor :options
262
299
 
263
300
  def self.open(path, options = {})
264
- Timeout.timeout(options[:socket_timeout]) do
301
+ Timeout.timeout(Dalli::Socket.connect_timeout_for(options)) do
265
302
  sock = new(path)
266
303
  sock.options = { path: path }.merge(options)
267
304
  init_socket_options(sock, options)
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.2.1'
4
+ VERSION = '5.2.3'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -31,6 +31,10 @@ module Dalli
31
31
  # socket/server communication error that can be retried
32
32
  class RetryableNetworkError < NetworkError; end
33
33
 
34
+ # a multi-get reply named a key that wasn't requested, so the rest of that
35
+ # server's replies can't be trusted
36
+ class UnexpectedReplyKeyError < DalliError; end
37
+
34
38
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
35
39
  class NilObject; end # rubocop:disable Lint/EmptyClass
36
40
  NOT_FOUND = NilObject.new
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'rack/session/abstract/id'
4
4
  require 'dalli'
5
- require 'connection_pool'
6
5
  require 'English'
7
6
 
8
7
  module Rack
@@ -21,6 +20,11 @@ module Rack
21
20
  namespace: 'rack:session'
22
21
  }
23
22
 
23
+ # Our pool options and the ConnectionPool options they set. These are
24
+ # not passed on to Dalli::Client.
25
+ POOL_OPTIONS = { pool_size: :size, pool_timeout: :timeout }.freeze
26
+ private_constant :POOL_OPTIONS
27
+
24
28
  # Brings in a new Rack::Session::Dalli middleware with the given
25
29
  # `:memcache_server`. The server is either a hostname, or a
26
30
  # host-with-port string in the form of "host_name:port", or an array of
@@ -73,10 +77,18 @@ module Rack
73
77
  @data = build_data_source(options)
74
78
  end
75
79
 
76
- def call(*_args)
77
- super
80
+ # If the session is deleted from memcached while a request is running
81
+ # (for example, by a logout in another request), the session is not
82
+ # written back, so a logged-out session can't come back. The app's
83
+ # response is replaced with a 401 that expires the session cookie.
84
+ # The app has already run by then, so any side effects it had (database
85
+ # writes, emails, and so on) have happened.
86
+ def context(env, app = @app)
87
+ body = nil
88
+ super(env, ->(app_env) { app.call(app_env).tap { |response| body = response[2] } })
78
89
  rescue MissingSessionError
79
- [401, {}, ['Wrong session ID']]
90
+ body.close if body.respond_to?(:close)
91
+ missing_session_response(env)
80
92
  end
81
93
 
82
94
  def find_session(req, sid)
@@ -111,13 +123,23 @@ module Rack
111
123
  def delete_session(_req, sid, options)
112
124
  with_dalli_client do |dc|
113
125
  key = memcached_key_from_sid(sid)
114
- dc.delete(key) if key
126
+ delete_session_key(dc, key) if key
115
127
  generate_sid_with(dc) unless options[:drop]
116
128
  end
117
129
  end
118
130
 
119
131
  private
120
132
 
133
+ # A failed delete leaves the old session readable in memcached (e.g.
134
+ # after logout), so always log it, even though with_dalli_client then
135
+ # handles the error as before.
136
+ def delete_session_key(client, key)
137
+ client.delete(key)
138
+ rescue ::Dalli::DalliError, Errno::ECONNREFUSED => e
139
+ ::Dalli.logger.warn("#{self.class} failed to delete a session from memcached: #{e.class}: #{e.message}")
140
+ raise
141
+ end
142
+
121
143
  def memcached_key_from_sid(sid)
122
144
  sid.private_id if sid.respond_to?(:private_id)
123
145
  end
@@ -162,6 +184,12 @@ module Rack
162
184
  end
163
185
  end
164
186
 
187
+ def missing_session_response(env)
188
+ options = env[Rack::RACK_SESSION_OPTIONS] || @default_options
189
+ cookie = Rack::Utils.delete_set_cookie_header(key, options.slice(:path, :domain))
190
+ [401, { 'content-type' => 'text/plain', 'set-cookie' => cookie }, ['Wrong session ID']]
191
+ end
192
+
165
193
  def write_session_safely!(dalli_client, sid, persisted_data, write_args:)
166
194
  if persisted_data && persisted_data[:id] == sid # That means that we update the existing session
167
195
  # Override the session only if it still exists in the store!
@@ -181,15 +209,14 @@ module Rack
181
209
  end
182
210
 
183
211
  def retrieve_client_options(options)
184
- # Filter out Rack::Session-specific options and apply our defaults
185
- filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key? k }
212
+ # Filter out Rack::Session-specific and pool options and apply our defaults
213
+ filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key?(k) || POOL_OPTIONS.key?(k) }
186
214
  DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
187
215
  end
188
216
 
189
217
  def retrieve_pool_options(options)
190
- {}.tap do |pool_options|
191
- pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
192
- pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
218
+ POOL_OPTIONS.each_with_object({}) do |(option, pool_option), pool_options|
219
+ pool_options[pool_option] = options[option] if options[option]
193
220
  end
194
221
  end
195
222
 
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: 5.2.1
4
+ version: 5.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  requirements: []
92
- rubygems_version: 4.0.21
92
+ rubygems_version: 4.0.23
93
93
  specification_version: 4
94
94
  summary: High performance memcached client for Ruby
95
95
  test_files: []