dalli 3.2.15 → 4.0.0

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.
@@ -15,33 +15,18 @@ module Dalli
15
15
  compress: true,
16
16
  compressor: ::Dalli::Compressor,
17
17
  # min byte size to attempt compression
18
- compression_min_size: 4 * 1024, # 4K
19
- # max size a stored value may decompress to (nil: no limit). Guards
20
- # against a small compressed value expanding into gigabytes on read.
21
- decompressed_max_bytes: 128 * 1024 * 1024 # 128 MiB
18
+ compression_min_size: 4 * 1024 # 4K
22
19
  }.freeze
23
20
 
24
21
  OPTIONS = DEFAULTS.keys.freeze
25
22
 
26
- KEYWORD_PARAMETER_TYPES = %i[key keyreq].freeze
27
- private_constant :KEYWORD_PARAMETER_TYPES
28
-
29
23
  # https://www.hjp.at/zettel/m/memcached_flags.rxml
30
24
  # Looks like most clients use bit 1 to indicate gzip compression.
31
25
  FLAG_COMPRESSED = 0x2
32
26
 
33
27
  def initialize(client_options)
34
- # Support the deprecated compression option, but don't allow it to override
35
- # an explicit compress
36
- # Remove this with 4.0
37
- if client_options.key?(:compression) && !client_options.key?(:compress)
38
- Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just 'compress: true'. " \
39
- 'Please update your configuration.'
40
- client_options[:compress] = client_options.delete(:compression)
41
- end
42
-
43
28
  @compression_options =
44
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
29
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
45
30
  end
46
31
 
47
32
  def store(value, req_options, bitflags)
@@ -53,16 +38,8 @@ module Dalli
53
38
  end
54
39
 
55
40
  def retrieve(value, bitflags)
56
- return value unless bitflags.anybits?(FLAG_COMPRESSED)
57
-
58
- max_bytes = @compression_options[:decompressed_max_bytes]
59
- # Custom compressors that only define decompress(data) keep working,
60
- # without the limit
61
- if max_bytes && compressor_accepts_max_bytes?
62
- compressor.decompress(value, max_bytes: max_bytes)
63
- else
64
- compressor.decompress(value)
65
- end
41
+ compressed = bitflags.anybits?(FLAG_COMPRESSED)
42
+ compressed ? compressor.decompress(value) : value
66
43
 
67
44
  # TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
68
45
  # itself, since not all compressors necessarily use Zlib. For now keep it here, so the behavior
@@ -71,19 +48,6 @@ module Dalli
71
48
  raise UnmarshalError, "Unable to uncompress value: #{$ERROR_INFO.message}"
72
49
  end
73
50
 
74
- def compressor_accepts_max_bytes?
75
- return @compressor_accepts_max_bytes if defined?(@compressor_accepts_max_bytes)
76
-
77
- @compressor_accepts_max_bytes =
78
- begin
79
- compressor.method(:decompress).parameters.any? do |type, name|
80
- name == :max_bytes && KEYWORD_PARAMETER_TYPES.include?(type)
81
- end
82
- rescue NameError # an object without a reflectable decompress method
83
- false
84
- end
85
- end
86
-
87
51
  def compress_by_default?
88
52
  @compression_options[:compress]
89
53
  end
@@ -97,13 +61,11 @@ module Dalli
97
61
  end
98
62
 
99
63
  # Checks whether we should apply compression when serializing a value
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.
64
+ # based on the specified options. Returns false unless the value
65
+ # is greater than the minimum compression size. Otherwise returns
66
+ # based on a method-level option if specified, falling back to the
67
+ # server default.
105
68
  def compress_value?(value, req_options)
106
- return false if req_options&.dig(:raw)
107
69
  return false unless value.bytesize >= compression_min_size
108
70
  return compress_by_default? unless req_options && !req_options[:compress].nil?
109
71
 
@@ -27,7 +27,7 @@ module Dalli
27
27
  @value_compressor = ValueCompressor.new(client_options)
28
28
 
29
29
  @marshal_options =
30
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
30
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
31
31
  end
32
32
 
33
33
  def store(key, value, options = nil)
@@ -18,57 +18,53 @@ module Dalli
18
18
  # https://www.hjp.at/zettel/m/memcached_flags.rxml
19
19
  # Looks like most clients use bit 0 to indicate native language serialization
20
20
  FLAG_SERIALIZED = 0x1
21
+ FLAG_UTF8 = 0x2
22
+
23
+ # Class variable to track whether the Marshal warning has been logged
24
+ @@marshal_warning_logged = false # rubocop:disable Style/ClassVars
21
25
 
22
26
  attr_accessor :serialization_options
23
27
 
24
28
  def initialize(protocol_options)
25
29
  @serialization_options =
26
- DEFAULTS.merge(protocol_options.select { |k, _| OPTIONS.include?(k) })
30
+ DEFAULTS.merge(protocol_options.slice(*OPTIONS))
31
+ warn_if_marshal_default(protocol_options) unless protocol_options[:silence_marshal_warning]
27
32
  end
28
33
 
29
34
  def store(value, req_options, bitflags)
30
- do_serialize = !(req_options && req_options[:raw])
31
- store_value = do_serialize ? serialize_value(value) : value.to_s
32
- bitflags |= FLAG_SERIALIZED if do_serialize
33
- [store_value, bitflags]
35
+ if req_options
36
+ return [value.to_s, bitflags] if req_options[:raw]
37
+
38
+ # If the value is a simple string, going through serialization is costly
39
+ # for no benefit other than preserving encoding.
40
+ # Assuming most strings are either UTF-8 or BINARY we can just store
41
+ # that information in the bitflags.
42
+ if req_options[:string_fastpath] && value.instance_of?(String)
43
+ case value.encoding
44
+ when Encoding::BINARY
45
+ return [value, bitflags]
46
+ when Encoding::UTF_8
47
+ return [value, bitflags | FLAG_UTF8]
48
+ end
49
+ end
50
+ end
51
+
52
+ [serialize_value(value), bitflags | FLAG_SERIALIZED]
34
53
  end
35
54
 
36
- # TODO: Some of these error messages need to be validated. It's not obvious
37
- # that all of them are actually generated by the invoked code
38
- # in current systems
39
- # rubocop:disable Layout/LineLength
40
- TYPE_ERR_REGEXP = %r{needs to have method `_load'|exception class/object expected|instance of IO needed|incompatible marshal file format}.freeze
41
- ARGUMENT_ERR_REGEXP = /undefined class|marshal data too short/.freeze
42
- NAME_ERR_STR = 'uninitialized constant'
43
- # rubocop:enable Layout/LineLength
44
-
45
55
  def retrieve(value, bitflags)
46
- serialized = (bitflags & FLAG_SERIALIZED) != 0
47
- serialized ? serializer.load(value) : value
48
- rescue TypeError => e
49
- filter_type_error(e)
50
- rescue ArgumentError => e
51
- filter_argument_error(e)
52
- rescue NameError => e
53
- filter_name_error(e)
54
- end
55
-
56
- def filter_type_error(err)
57
- raise err unless TYPE_ERR_REGEXP.match?(err.message)
58
-
59
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
60
- end
61
-
62
- def filter_argument_error(err)
63
- raise err unless ARGUMENT_ERR_REGEXP.match?(err.message)
64
-
65
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
66
- end
67
-
68
- def filter_name_error(err)
69
- raise err unless err.message.include?(NAME_ERR_STR)
70
-
71
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
56
+ serialized = bitflags.anybits?(FLAG_SERIALIZED)
57
+ if serialized
58
+ begin
59
+ serializer.load(value)
60
+ rescue StandardError
61
+ raise UnmarshalError, 'Unable to unmarshal value'
62
+ end
63
+ elsif bitflags.anybits?(FLAG_UTF8)
64
+ value.force_encoding(Encoding::UTF_8)
65
+ else
66
+ value
67
+ end
72
68
  end
73
69
 
74
70
  def serializer
@@ -86,6 +82,19 @@ module Dalli
86
82
  exc.set_backtrace e.backtrace
87
83
  raise exc
88
84
  end
85
+
86
+ private
87
+
88
+ def warn_if_marshal_default(protocol_options)
89
+ return if protocol_options.key?(:serializer)
90
+ return if @@marshal_warning_logged
91
+
92
+ Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
93
+ 'Marshal can execute arbitrary code during deserialization. ' \
94
+ 'If your memcached server could be compromised, consider using ' \
95
+ 'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
96
+ @@marshal_warning_logged = true # rubocop:disable Style/ClassVars
97
+ end
89
98
  end
90
99
  end
91
100
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'openssl'
4
3
  require 'timeout'
5
4
 
6
5
  module Dalli
@@ -16,11 +15,5 @@ module Dalli
16
15
  else
17
16
  [Timeout::Error]
18
17
  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
25
18
  end
26
19
  end
data/lib/dalli/socket.rb CHANGED
@@ -52,7 +52,7 @@ module Dalli
52
52
 
53
53
  FILTERED_OUT_OPTIONS = %i[username password].freeze
54
54
  def logged_options
55
- options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
55
+ options.except(*FILTERED_OUT_OPTIONS)
56
56
  end
57
57
  end
58
58
 
@@ -63,6 +63,7 @@ module Dalli
63
63
  ##
64
64
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
65
65
  include Dalli::Socket::InstanceMethods
66
+
66
67
  def options
67
68
  io.options
68
69
  end
@@ -85,6 +86,7 @@ module Dalli
85
86
  ##
86
87
  class TCP < TCPSocket
87
88
  include Dalli::Socket::InstanceMethods
89
+
88
90
  # options - supports enhanced logging in the case of a timeout
89
91
  attr_accessor :options
90
92
 
@@ -117,57 +119,41 @@ module Dalli
117
119
  end
118
120
 
119
121
  def self.init_socket_options(sock, options)
122
+ configure_tcp_options(sock, options)
123
+ configure_socket_buffers(sock, options)
124
+ configure_timeout(sock, options)
125
+ end
126
+
127
+ def self.configure_tcp_options(sock, options)
120
128
  sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
121
129
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
130
+ end
131
+
132
+ def self.configure_socket_buffers(sock, options)
122
133
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
123
134
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
135
+ end
124
136
 
137
+ def self.configure_timeout(sock, options)
125
138
  return unless options[:socket_timeout]
126
139
 
127
- seconds, fractional = options[:socket_timeout].divmod(1)
128
- microseconds = fractional * 1_000_000
129
- timeval = [seconds, microseconds].pack('l_2')
140
+ if sock.respond_to?(:timeout=)
141
+ sock.timeout = options[:socket_timeout]
142
+ else
143
+ seconds, fractional = options[:socket_timeout].divmod(1)
144
+ timeval = [seconds, fractional * 1_000_000].pack('l_2')
130
145
 
131
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
132
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
146
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
147
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
148
+ end
133
149
  end
134
150
 
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.
137
151
  def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
138
- warn_if_unverified(ssl_context)
139
- connected = false
140
152
  ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
141
153
  ssl_socket.hostname = host
142
154
  ssl_socket.sync_close = true
143
155
  ssl_socket.connect
144
- connected = true
145
156
  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
171
157
  end
172
158
  end
173
159
 
@@ -198,9 +184,16 @@ module Dalli
198
184
  Timeout.timeout(options[:socket_timeout]) do
199
185
  sock = new(path)
200
186
  sock.options = { path: path }.merge(options)
187
+ init_socket_options(sock, options)
201
188
  sock
202
189
  end
203
190
  end
191
+
192
+ def self.init_socket_options(sock, options)
193
+ # https://man7.org/linux/man-pages/man7/unix.7.html
194
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
195
+ sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
196
+ end
204
197
  end
205
198
  end
206
199
  end
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.15'
4
+ VERSION = '4.0.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -4,8 +4,6 @@
4
4
  # Namespace for all Dalli code.
5
5
  ##
6
6
  module Dalli
7
- autoload :Server, 'dalli/server'
8
-
9
7
  # generic error
10
8
  class DalliError < RuntimeError; end
11
9
 
@@ -27,6 +25,9 @@ module Dalli
27
25
  # operation is not permitted in a multi block
28
26
  class NotPermittedMultiOpError < DalliError; end
29
27
 
28
+ # raised when Memcached response with a SERVER_ERROR
29
+ class ServerError < DalliError; end
30
+
30
31
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
31
32
  class NilObject; end # rubocop:disable Lint/EmptyClass
32
33
  NOT_FOUND = NilObject.new
@@ -57,7 +58,6 @@ end
57
58
  require_relative 'dalli/version'
58
59
 
59
60
  require_relative 'dalli/compressor'
60
- require_relative 'dalli/json_serializer'
61
61
  require_relative 'dalli/client'
62
62
  require_relative 'dalli/key_manager'
63
63
  require_relative 'dalli/pipelined_getter'
@@ -9,6 +9,10 @@ module Rack
9
9
  module Session
10
10
  # Rack::Session::Dalli provides memcached based session management.
11
11
  class Dalli < Abstract::PersistedSecure
12
+ class MissingSessionError < StandardError; end
13
+
14
+ RACK_SESSION_PERSISTED = 'rack.session.persisted'
15
+
12
16
  attr_reader :data
13
17
 
14
18
  # Don't freeze this until we fix the specs/implementation
@@ -70,23 +74,37 @@ module Rack
70
74
  @data = build_data_source(options)
71
75
  end
72
76
 
73
- def find_session(_req, sid)
77
+ def call(*_args)
78
+ super
79
+ rescue MissingSessionError
80
+ [401, {}, ['Wrong session ID']]
81
+ end
82
+
83
+ def find_session(req, sid)
74
84
  with_dalli_client([nil, {}]) do |dc|
75
85
  existing_session = existing_session_for_sid(dc, sid)
76
- return [sid, existing_session] unless existing_session.nil?
86
+ if existing_session.nil?
87
+ sid = create_sid_with_empty_session(dc)
88
+ existing_session = {}
89
+ end
77
90
 
78
- [create_sid_with_empty_session(dc), {}]
91
+ update_session_persisted_data(req, { id: sid })
92
+ return [sid, existing_session]
79
93
  end
80
94
  end
81
95
 
82
- def write_session(_req, sid, session, options)
96
+ def write_session(req, sid, session, options)
83
97
  return false unless sid
84
98
 
85
99
  key = memcached_key_from_sid(sid)
86
100
  return false unless key
87
101
 
88
102
  with_dalli_client(false) do |dc|
89
- dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
103
+ write_session_safely!(
104
+ dc, sid, session_persisted_data(req),
105
+ write_args: [memcached_key_from_sid(sid), session, ttl(options[:expire_after])]
106
+ )
107
+
90
108
  sid
91
109
  end
92
110
  end
@@ -145,6 +163,15 @@ module Rack
145
163
  end
146
164
  end
147
165
 
166
+ def write_session_safely!(dalli_client, sid, persisted_data, write_args:)
167
+ if persisted_data && persisted_data[:id] == sid # That means that we update the existing session
168
+ # Override the session only if it still exists in the store!
169
+ raise MissingSessionError unless dalli_client.replace(*write_args)
170
+ else
171
+ dalli_client.set(*write_args)
172
+ end
173
+ end
174
+
148
175
  def extract_dalli_options(options)
149
176
  raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
150
177
 
@@ -175,8 +202,8 @@ module Rack
175
202
  raise e
176
203
  end
177
204
 
178
- def with_dalli_client(result_on_error = nil, &block)
179
- @data.with(&block)
205
+ def with_dalli_client(result_on_error = nil, &)
206
+ @data.with(&)
180
207
  rescue ::Dalli::DalliError, Errno::ECONNREFUSED
181
208
  raise if $ERROR_INFO.message.include?('undefined class')
182
209
 
@@ -190,6 +217,14 @@ module Rack
190
217
  def ttl(expire_after)
191
218
  expire_after.nil? ? 0 : expire_after + 1
192
219
  end
220
+
221
+ def session_persisted_data(req)
222
+ req.get_header RACK_SESSION_PERSISTED
223
+ end
224
+
225
+ def update_session_persisted_data(req, data)
226
+ req.set_header RACK_SESSION_PERSISTED, data
227
+ end
193
228
  end
194
229
  end
195
230
  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.15
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -40,7 +40,6 @@ files:
40
40
  - lib/dalli/cas/client.rb
41
41
  - lib/dalli/client.rb
42
42
  - lib/dalli/compressor.rb
43
- - lib/dalli/json_serializer.rb
44
43
  - lib/dalli/key_manager.rb
45
44
  - lib/dalli/options.rb
46
45
  - lib/dalli/pid_cache.rb
@@ -64,7 +63,6 @@ files:
64
63
  - lib/dalli/protocol/value_marshaller.rb
65
64
  - lib/dalli/protocol/value_serializer.rb
66
65
  - lib/dalli/ring.rb
67
- - lib/dalli/server.rb
68
66
  - lib/dalli/servers_arg_normalizer.rb
69
67
  - lib/dalli/socket.rb
70
68
  - lib/dalli/version.rb
@@ -83,14 +81,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
81
  requirements:
84
82
  - - ">="
85
83
  - !ruby/object:Gem::Version
86
- version: '2.6'
84
+ version: '3.1'
87
85
  required_rubygems_version: !ruby/object:Gem::Requirement
88
86
  requirements:
89
87
  - - ">="
90
88
  - !ruby/object:Gem::Version
91
89
  version: '0'
92
90
  requirements: []
93
- rubygems_version: 4.0.23
91
+ rubygems_version: 4.0.3
94
92
  specification_version: 4
95
93
  summary: High performance memcached client for Ruby
96
94
  test_files: []
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Dalli
6
- ##
7
- # A serializer that stores values as JSON and never creates Ruby objects
8
- # from what it reads.
9
- #
10
- # Passing the JSON module itself as the serializer (serializer: JSON) reads
11
- # values back with JSON.load, which on json gem versions before 3.0 honors
12
- # "json_class" and instantiates other classes when the json gem's additions
13
- # are loaded. This serializer reads with JSON.parse, so a value written to
14
- # memcached by someone else can only come back as plain hashes, arrays,
15
- # strings, numbers, booleans and nil.
16
- #
17
- # Dalli::Client.new(servers, serializer: Dalli::JSONSerializer)
18
- ##
19
- module JSONSerializer
20
- def self.dump(value)
21
- JSON.generate(value)
22
- end
23
-
24
- def self.load(data)
25
- JSON.parse(data)
26
- end
27
- end
28
- end
data/lib/dalli/server.rb DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dalli # rubocop:disable Style/Documentation
4
- warn 'Dalli::Server is deprecated, use Dalli::Protocol::Binary instead'
5
- Server = Protocol::Binary
6
- end