dalli 5.2.2 → 5.2.5

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.
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.2'
4
+ VERSION = '5.2.5'
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.2
4
+ version: 5.2.5
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: []